diff --git a/apps/cli/commands/export.ts b/apps/cli/commands/export.ts index f602466f2b..cad42f9451 100644 --- a/apps/cli/commands/export.ts +++ b/apps/cli/commands/export.ts @@ -8,7 +8,6 @@ import { ExportEvents } from 'cli/lib/import-export/export/events'; import { exportBackup } from 'cli/lib/import-export/export/export-manager'; import { BackupCreateProgressEventData, ExportOptions } from 'cli/lib/import-export/export/types'; import { ImportExportEventData } from 'cli/lib/import-export/handle-events'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { untildify } from 'cli/lib/utils'; import { Logger, LoggerError } from 'cli/logger'; import { StudioArgv } from 'cli/types'; @@ -87,13 +86,6 @@ export async function runCommand( const site = await getSiteByFolder( siteFolder ); logger.reportSuccess( __( 'Site loaded' ) ); - logger.reportStart( - LoggerAction.INSTALL_SQLITE, - __( 'Setting up SQLite integration, if needed…' ) - ); - await keepSqliteIntegrationUpdated( siteFolder ); - logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); - const includes: ExportOptions[ 'includes' ] = { database: true, wpContent: true }; if ( mode === 'db' ) { diff --git a/apps/cli/commands/import.ts b/apps/cli/commands/import.ts index c3c1ca66c9..bb3611a20a 100644 --- a/apps/cli/commands/import.ts +++ b/apps/cli/commands/import.ts @@ -23,7 +23,6 @@ import { ImportWpContentProgressEventData, } from 'cli/lib/import-export/import/types'; import { getBackupFileType } from 'cli/lib/import-export/utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { untildify } from 'cli/lib/utils'; import { isServerRunning, @@ -236,13 +235,6 @@ export async function runCommand( siteFolder: string, importFile: string ): Prom } finally { try { if ( site && wasServerRunning ) { - logger.reportStart( - LoggerAction.INSTALL_SQLITE, - __( 'Setting up SQLite integration, if needed…' ) - ); - await keepSqliteIntegrationUpdated( siteFolder ); - logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); - logger.reportStart( LoggerAction.START_SITE, __( 'Starting WordPress server…' ) ); await startWordPressServer( site, logger ); logger.reportSuccess( __( 'WordPress server started' ) ); diff --git a/apps/cli/commands/pull.ts b/apps/cli/commands/pull.ts index ee2746e7ac..678ada22d3 100644 --- a/apps/cli/commands/pull.ts +++ b/apps/cli/commands/pull.ts @@ -18,7 +18,6 @@ import { DEFAULT_IMPORTER_OPTIONS, importBackup, } from 'cli/lib/import-export/import/import-manager'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { checkBackupSize, fetchSyncableSites, @@ -204,13 +203,6 @@ export async function runCommand( } finally { try { if ( site && wasServerRunning ) { - logger.reportStart( - LoggerAction.INSTALL_SQLITE, - __( 'Setting up SQLite integration, if needed…' ) - ); - await keepSqliteIntegrationUpdated( siteFolder ); - logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); - logger.reportStart( LoggerAction.START_SITE, __( 'Starting WordPress server…' ) ); await startWordPressServer( site, logger ); logger.reportSuccess( __( 'WordPress server started' ) ); diff --git a/apps/cli/commands/push.ts b/apps/cli/commands/push.ts index 1e068111ed..95a74a6a0a 100644 --- a/apps/cli/commands/push.ts +++ b/apps/cli/commands/push.ts @@ -16,7 +16,6 @@ import { __, sprintf } from '@wordpress/i18n'; import { getSiteByFolder } from 'cli/lib/cli-config/sites'; import { exportBackup } from 'cli/lib/import-export/export/export-manager'; import { ExportOptions } from 'cli/lib/import-export/export/types'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { fetchSyncableSites, initiateImport, @@ -47,13 +46,6 @@ export async function runCommand( const site = await getSiteByFolder( siteFolder ); logger.reportSuccess( __( 'Site loaded' ) ); - logger.reportStart( - LoggerAction.INSTALL_SQLITE, - __( 'Setting up SQLite integration, if needed…' ) - ); - await keepSqliteIntegrationUpdated( siteFolder ); - logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); - logger.reportStart( LoggerAction.FETCH_REMOTE_SITES, __( 'Fetching WordPress.com sites…' ) ); const remoteSites = await fetchSyncableSites( token.accessToken ); logger.spinner.stop(); diff --git a/apps/cli/commands/site/create.ts b/apps/cli/commands/site/create.ts index 7b0b3afd00..0d909fba5f 100644 --- a/apps/cli/commands/site/create.ts +++ b/apps/cli/commands/site/create.ts @@ -73,7 +73,6 @@ import { getPreferredSiteLanguage } from 'cli/lib/site-language'; import { generateSiteName } from 'cli/lib/site-name'; import { getDefaultSitePath } from 'cli/lib/site-paths'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { StatsGroup } from 'cli/lib/types/bump-stats'; import { untildify } from 'cli/lib/utils'; import { ValidationError } from 'cli/lib/validation-error'; @@ -250,12 +249,6 @@ export async function runCommand( ); } - logger.reportStart( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration…' ) ); - const isSqliteUpdated = await keepSqliteIntegrationUpdated( sitePath ); - logger.reportSuccess( - isSqliteUpdated ? __( 'SQLite integration configured' ) : __( 'SQLite integration skipped' ) - ); - try { const sharedConfig = await readSharedConfig(); const selectedSkills = sharedConfig.selectedSkills ?? []; diff --git a/apps/cli/commands/site/start.ts b/apps/cli/commands/site/start.ts index 57d9126e7f..4f397c65b7 100644 --- a/apps/cli/commands/site/start.ts +++ b/apps/cli/commands/site/start.ts @@ -9,7 +9,6 @@ import { import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { getAiInstructionsPath } from 'cli/lib/server-files'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager'; import { Logger, LoggerError } from 'cli/logger'; import { StudioArgv } from 'cli/types'; @@ -47,13 +46,6 @@ export async function runCommand( await setupCustomDomain( site, logger ); - logger.reportStart( - LoggerAction.INSTALL_SQLITE, - __( 'Setting up SQLite integration, if needed…' ) - ); - await keepSqliteIntegrationUpdated( sitePath ); - logger.reportSuccess( __( 'SQLite integration configured as needed' ) ); - try { await updateManagedInstructionFiles( sitePath, getAiInstructionsPath() ); } catch ( error ) { diff --git a/apps/cli/commands/site/tests/create.test.ts b/apps/cli/commands/site/tests/create.test.ts index 172611d5ee..fd5754d52d 100644 --- a/apps/cli/commands/site/tests/create.test.ts +++ b/apps/cli/commands/site/tests/create.test.ts @@ -29,7 +29,6 @@ import { updateServerFiles } from 'cli/lib/dependency-management/setup'; import { copyLanguagePackToSite } from 'cli/lib/language-packs'; import { getPreferredSiteLanguage } from 'cli/lib/site-language'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { ProcessDescription } from 'cli/lib/types/process-manager-ipc'; import { runBlueprint, startWordPressServer } from 'cli/lib/wordpress-server-manager'; import { Logger } from 'cli/logger'; @@ -80,7 +79,6 @@ vi.mock( import( '@studio/common/lib/well-known-paths' ), async ( importOriginal vi.mock( 'cli/lib/site-language' ); vi.mock( 'cli/lib/site-utils' ); vi.mock( '@studio/common/lib/agent-skills' ); -vi.mock( 'cli/lib/sqlite-integration' ); vi.mock( 'cli/lib/wordpress-server-manager' ); describe( 'CLI: studio site create', () => { @@ -121,7 +119,6 @@ describe( 'CLI: studio site create', () => { let consoleLogSpy: MockInstance; let fsMkdirSyncSpy: MockInstance; - let loggerReportSuccessSpy: MockInstance; const createPathExistsMock = ( sitePathExists = false ) => { const path = require( 'path' ); @@ -143,7 +140,6 @@ describe( 'CLI: studio site create', () => { consoleLogSpy = vi.spyOn( console, 'log' ).mockImplementation( () => {} ); fsMkdirSyncSpy = vi.spyOn( fs, 'mkdirSync' ).mockReturnValue( undefined ); - loggerReportSuccessSpy = vi.spyOn( Logger.prototype, 'reportSuccess' ); vi.mocked( getServerFilesPath ).mockReturnValue( '/test/server-files' ); createPathExistsMock( false ); vi.mocked( isEmptyDir ).mockResolvedValue( true ); @@ -158,7 +154,6 @@ describe( 'CLI: studio site create', () => { vi.mocked( saveCliConfig ).mockResolvedValue( undefined ); vi.mocked( lockCliConfig ).mockResolvedValue( undefined ); vi.mocked( unlockCliConfig ).mockResolvedValue( undefined ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( true ); vi.mocked( connectToDaemon ).mockResolvedValue( undefined ); vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined ); vi.mocked( updateServerFiles ).mockResolvedValue( undefined ); @@ -252,18 +247,6 @@ describe( 'CLI: studio site create', () => { expect( disconnectFromDaemon ).toHaveBeenCalled(); } ); - - it( 'should error if SQLite integration is not available', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( - new Error( 'SQLite integration files not found. Please ensure Studio is installed.' ) - ); - - await expect( runCommand( mockSitePath, { ...defaultTestOptions } ) ).rejects.toThrow( - 'SQLite integration files not found' - ); - - expect( disconnectFromDaemon ).toHaveBeenCalled(); - } ); } ); describe( 'Success Cases', () => { @@ -271,8 +254,6 @@ describe( 'CLI: studio site create', () => { await runCommand( mockSitePath, { ...defaultTestOptions } ); expect( fsMkdirSyncSpy ).toHaveBeenCalledWith( mockSitePath, { recursive: true } ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( mockSitePath ); - expect( loggerReportSuccessSpy ).toHaveBeenCalledWith( 'SQLite integration configured' ); expect( portFinder.getOpenPort ).toHaveBeenCalled(); expect( lockCliConfig ).toHaveBeenCalled(); expect( saveCliConfig ).toHaveBeenCalled(); @@ -284,15 +265,6 @@ describe( 'CLI: studio site create', () => { expect( disconnectFromDaemon ).toHaveBeenCalled(); } ); - it( 'should skip SQLite integration when it is already configured', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( false ); - - await runCommand( mockSitePath, { ...defaultTestOptions } ); - - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( mockSitePath ); - expect( loggerReportSuccessSpy ).toHaveBeenCalledWith( 'SQLite integration skipped' ); - } ); - it( 'should create site with custom name', async () => { await runCommand( mockSitePath, { ...defaultTestOptions, @@ -789,16 +761,6 @@ describe( 'CLI: studio site create', () => { expect( disconnectFromDaemon ).toHaveBeenCalled(); } ); - - it( 'should handle SQLite setup failure', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( - new Error( 'SQLite setup failed' ) - ); - - await expect( runCommand( mockSitePath, { ...defaultTestOptions } ) ).rejects.toThrow(); - - expect( disconnectFromDaemon ).toHaveBeenCalled(); - } ); } ); describe( 'Cleanup', () => { diff --git a/apps/cli/commands/site/tests/export.test.ts b/apps/cli/commands/site/tests/export.test.ts index 479454c6c7..342b5bd590 100644 --- a/apps/cli/commands/site/tests/export.test.ts +++ b/apps/cli/commands/site/tests/export.test.ts @@ -5,7 +5,6 @@ import { getSiteByFolder } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { ExportEvents } from 'cli/lib/import-export/export/events'; import { exportBackup } from 'cli/lib/import-export/export/export-manager'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { Logger, LoggerError } from 'cli/logger'; import { registerCommand, runCommand } from '../../export'; import type { SiteData } from 'cli/lib/cli-config/core'; @@ -22,7 +21,6 @@ vi.mock( 'cli/lib/cli-config/sites', () => ( { getSiteByFolder: vi.fn(), } ) ); vi.mock( 'cli/lib/daemon-client' ); -vi.mock( 'cli/lib/sqlite-integration' ); vi.mock( 'cli/lib/import-export/export/export-manager', () => ( { exportBackup: vi.fn(), } ) ); @@ -46,7 +44,6 @@ describe( 'CLI: studio export', () => { vi.mocked( connectToDaemon ).mockResolvedValue( undefined ); vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined ); vi.mocked( getSiteByFolder ).mockResolvedValue( testSite ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( false ); vi.mocked( exportBackup ).mockResolvedValue( true ); } ); @@ -59,7 +56,6 @@ describe( 'CLI: studio export', () => { expect( connectToDaemon ).toHaveBeenCalled(); expect( getSiteByFolder ).toHaveBeenCalledWith( testSitePath ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( testSitePath ); expect( exportBackup ).toHaveBeenCalledWith( { site: testSite, diff --git a/apps/cli/commands/site/tests/import.test.ts b/apps/cli/commands/site/tests/import.test.ts index 6d47c49177..781534ab52 100644 --- a/apps/cli/commands/site/tests/import.test.ts +++ b/apps/cli/commands/site/tests/import.test.ts @@ -11,7 +11,6 @@ import { DEFAULT_IMPORTER_OPTIONS, importBackup, } from 'cli/lib/import-export/import/import-manager'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { isServerRunning, stopWordPressServer } from 'cli/lib/wordpress-server-manager'; import { Logger, LoggerError } from 'cli/logger'; import { runCommand } from '../../import'; @@ -23,9 +22,6 @@ vi.mock( 'cli/lib/cli-config/sites', () => ( { getSiteUrl: vi.fn(), } ) ); vi.mock( 'cli/lib/daemon-client' ); -vi.mock( 'cli/lib/sqlite-integration', () => ( { - keepSqliteIntegrationUpdated: vi.fn(), -} ) ); vi.mock( 'cli/lib/wordpress-server-manager', () => ( { isServerRunning: vi.fn(), stopWordPressServer: vi.fn(), @@ -61,7 +57,6 @@ describe( 'CLI: studio import', () => { vi.mocked( getSiteByFolder ).mockResolvedValue( testSite ); vi.mocked( isServerRunning ).mockResolvedValue( undefined ); vi.mocked( stopWordPressServer ).mockResolvedValue( undefined ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( false ); vi.mocked( isWordPressDirectory ).mockReturnValue( true ); vi.mocked( getServerFilesPath ).mockReturnValue( '/server-files' ); vi.mocked( recursiveCopyDirectory ).mockResolvedValue( undefined ); @@ -167,7 +162,6 @@ describe( 'CLI: studio import', () => { it( 'preserves import error when restore steps fail', async () => { vi.mocked( isServerRunning ).mockResolvedValue( { pid: 1234 } as never ); vi.mocked( importBackup ).mockRejectedValue( new Error( 'import failed' ) ); - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( new Error( 'restart failed' ) ); await expect( runCommand( testSitePath, testImportPath ) ).rejects.toThrow( 'import failed' ); expect( stopWordPressServer ).toHaveBeenCalledWith( testSite.id ); diff --git a/apps/cli/commands/site/tests/start.test.ts b/apps/cli/commands/site/tests/start.test.ts index 8b9a96913b..14d45d3bbd 100644 --- a/apps/cli/commands/site/tests/start.test.ts +++ b/apps/cli/commands/site/tests/start.test.ts @@ -7,7 +7,6 @@ import { } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils'; -import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration'; import { ProcessDescription } from 'cli/lib/types/process-manager-ipc'; import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager'; import { Logger } from 'cli/logger'; @@ -22,7 +21,6 @@ vi.mock( 'cli/lib/cli-config/sites', async () => ( { vi.mock( 'cli/lib/daemon-client' ); vi.mock( 'cli/lib/site-utils' ); vi.mock( 'cli/lib/wordpress-server-manager' ); -vi.mock( 'cli/lib/sqlite-integration' ); describe( 'CLI: studio site start', () => { // Simple test data @@ -56,7 +54,6 @@ describe( 'CLI: studio site start', () => { vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined ); vi.mocked( isServerRunning ).mockResolvedValue( undefined ); vi.mocked( setupCustomDomain ).mockResolvedValue( undefined ); - vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( false ); vi.mocked( startWordPressServer ).mockResolvedValue( testProcessDescription ); vi.mocked( updateSiteLatestCliPid ).mockResolvedValue( undefined ); vi.mocked( logSiteDetails ).mockImplementation( () => {} ); @@ -105,15 +102,6 @@ describe( 'CLI: studio site start', () => { expect( disconnectFromDaemon ).toHaveBeenCalled(); } ); - it( 'should throw when SQLite integration setup fails', async () => { - vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue( - new Error( 'SQLite setup failed' ) - ); - - await expect( runCommand( '/test/site' ) ).rejects.toThrow( 'SQLite setup failed' ); - expect( disconnectFromDaemon ).toHaveBeenCalled(); - } ); - it( 'should throw when browser fails', async () => { vi.mocked( openSiteInBrowser ).mockRejectedValue( new Error( 'Browser error' ) ); @@ -138,7 +126,6 @@ describe( 'CLI: studio site start', () => { expect( connectToDaemon ).toHaveBeenCalled(); expect( isServerRunning ).toHaveBeenCalledWith( testSite.id ); expect( setupCustomDomain ).toHaveBeenCalledWith( testSite, expect.any( Logger ) ); - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( '/test/site' ); expect( startWordPressServer ).toHaveBeenCalledWith( testSite, expect.any( Logger ) ); expect( updateSiteLatestCliPid ).toHaveBeenCalledWith( testSite.id, @@ -179,12 +166,6 @@ describe( 'CLI: studio site start', () => { expect( disconnectFromDaemon ).toHaveBeenCalled(); } ); - it( 'should update SQLite integration', async () => { - await runCommand( '/test/site' ); - - expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( '/test/site' ); - } ); - it( 'should skip browser when skipBrowser is true', async () => { await runCommand( '/test/site', true ); diff --git a/apps/cli/lib/dependency-management/setup.ts b/apps/cli/lib/dependency-management/setup.ts index 493772c591..5153a059da 100644 --- a/apps/cli/lib/dependency-management/setup.ts +++ b/apps/cli/lib/dependency-management/setup.ts @@ -2,13 +2,11 @@ import fs from 'fs'; import path from 'path'; import { recursiveCopyDirectory } from '@studio/common/lib/fs-utils'; import semver from 'semver'; -import { getSqliteVersionFromInstallation } from 'cli/lib/sqlite-integration'; import { getAiInstructionsPath, getLanguagePacksPath, getPhpMyAdminPath, getSqliteCommandPath, - getSqlitePluginPath, getWordPressVersionPath, getWpCliPharPath, getWpFilesPath, @@ -97,26 +95,6 @@ async function copyBundledLatestWpVersion() { } } -const SQLITE_FILENAME = 'sqlite-database-integration'; - -async function copyBundledSqlite() { - const sourceSqlitePath = path.join( getWpFilesPath(), SQLITE_FILENAME ); - const targetSqlitePath = getSqlitePluginPath(); - - await copySourceDirectoryIfNewerOrMissing( { - sourceDirectoryPath: sourceSqlitePath, - targetDirectoryPath: targetSqlitePath, - readSourceVersion: async () => - semver.coerce( await getSqliteVersionFromInstallation( sourceSqlitePath ), { - includePrerelease: true, - } ), - readTargetVersion: async () => - semver.coerce( await getSqliteVersionFromInstallation( targetSqlitePath ), { - includePrerelease: true, - } ), - } ); -} - async function copyBundledWpCli() { const sourceWpCLIPath = path.join( getWpFilesPath(), 'wp-cli', 'wp-cli.phar' ); const sourceStats = await fs.promises.lstat( sourceWpCLIPath ); @@ -139,6 +117,95 @@ async function copyBundledWpCli() { } } +async function patchSqliteCommandForStudio() { + // Patch the installed SQLiteDatabaseIntegrationLoader.php to also check + // Playground's in-memory VFS path for the SQLite integration plugin. + // Playground CLI mounts the plugin at /internal/shared/sqlite-database-integration + // inside its virtual filesystem, so it is never written to the site directory on disk. + // This patch is applied after copying the bundled sqlite-command to the server-files + // directory. The upstream sqlite-command repo will be updated separately. + const loaderPath = path.join( + getSqliteCommandPath(), + 'src', + 'SQLiteDatabaseIntegrationLoader.php' + ); + + if ( ! fs.existsSync( loaderPath ) ) { + return; + } + + let content = await fs.promises.readFile( loaderPath, 'utf8' ); + let modified = false; + + // 1. Add /internal/shared/ path to get_plugin_directory() if not already present. + // Playground CLI mounts the SQLite plugin at this in-memory VFS path, so it is + // never written to the site directory on disk. + const internalSharedPath = '/internal/shared/sqlite-database-integration'; + const muPluginsLine = "\t\t\tABSPATH . '/wp-content/mu-plugins/sqlite-database-integration',"; + if ( ! content.includes( internalSharedPath ) && content.includes( muPluginsLine ) ) { + content = content.replace( + muPluginsLine, + muPluginsLine + `\n\t\t\t'${ internalSharedPath }',` + ); + modified = true; + } + + // 2. Replace get_plugin_version() with a simpler implementation that reads the version + // directly from wp-includes/database/version.php (which defines SQLITE_DRIVER_VERSION). + // The original implementation requires db.php on disk, which Playground never writes. + const originalGetPluginVersionMethod = + '\tpublic static function get_plugin_version() {\n' + + '\t\t// Check if there is a db.php file in the wp-content directory.\n' + + "\t\tif ( ! file_exists( ABSPATH . '/wp-content/db.php' ) ) {\n" + + '\t\t\treturn false;\n' + + '\t\t}\n' + + '\n' + + '\t\t// If the file is found, we need to check that it is the sqlite integration plugin.\n' + + "\t\t$plugin_file = file_get_contents( ABSPATH . '/wp-content/db.php' );\n" + + "\t\tif ( ! preg_match( '/define\\( \\'SQLITE_DB_DROPIN_VERSION\\', \\'([0-9.]+)\\' \\)/', $plugin_file ) ) {\n" + + '\t\t\treturn false;\n' + + '\t\t}\n' + + '\n' + + '\t\t$plugin_path = self::get_plugin_directory();\n' + + '\t\tif ( ! $plugin_path ) {\n' + + '\t\t\treturn false;\n' + + '\t\t}\n' + + '\n' + + '\t\t// Try to get the version number from readme.txt\n' + + "\t\t$plugin_file = file_get_contents( $plugin_path . '/readme.txt' );\n" + + '\n' + + "\t\tpreg_match( '/^Stable tag:\\s*?(.+)$/m', $plugin_file, $matches );\n" + + '\n' + + '\t\treturn isset( $matches[1] ) ? trim( $matches[1] ) : false;\n' + + '\t}'; + + const patchedGetPluginVersionMethod = + '\tpublic static function get_plugin_version() {\n' + + '\t\t$plugin_path = self::get_plugin_directory();\n' + + '\t\tif ( ! $plugin_path ) {\n' + + '\t\t\treturn false;\n' + + '\t\t}\n' + + '\n' + + "\t\t$version_file = $plugin_path . '/wp-includes/database/version.php';\n" + + '\t\tif ( ! file_exists( $version_file ) ) {\n' + + '\t\t\treturn false;\n' + + '\t\t}\n' + + '\n' + + '\t\trequire_once $version_file;\n' + + '\n' + + "\t\treturn defined( 'SQLITE_DRIVER_VERSION' ) ? SQLITE_DRIVER_VERSION : false;\n" + + '\t}'; + + if ( content.includes( originalGetPluginVersionMethod ) ) { + content = content.replace( originalGetPluginVersionMethod, patchedGetPluginVersionMethod ); + modified = true; + } + + if ( modified ) { + await fs.promises.writeFile( loaderPath, content, 'utf8' ); + } +} + async function copyBundledSqliteCommand() { await copySourceDirectoryIfNewerOrMissing( { sourceDirectoryPath: path.join( getWpFilesPath(), 'sqlite-command' ), @@ -152,6 +219,7 @@ async function copyBundledSqliteCommand() { return semver.coerce( fs.readFileSync( versionFilePath, 'utf8' ) ); }, } ); + await patchSqliteCommandForStudio(); } async function copyBundledTranslations() { @@ -245,7 +313,6 @@ async function copyBundledLanguagePacks() { export async function setupServerFiles() { const steps: [ string, () => Promise< void > ][] = [ [ 'WordPress version', copyBundledLatestWpVersion ], - [ 'SQLite integration', copyBundledSqlite ], [ 'WP-CLI', copyBundledWpCli ], [ 'SQLite command', copyBundledSqliteCommand ], [ 'translations', copyBundledTranslations ], diff --git a/apps/cli/lib/run-wp-cli-command.ts b/apps/cli/lib/run-wp-cli-command.ts index 54bcdd8ab7..409b5220d5 100644 --- a/apps/cli/lib/run-wp-cli-command.ts +++ b/apps/cli/lib/run-wp-cli-command.ts @@ -1,3 +1,5 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; import { rootCertificates } from 'node:tls'; import { loadNodeRuntime, createNodeFsMountHandler } from '@php-wasm/node'; import { @@ -7,14 +9,129 @@ import { setPhpIniEntries, ProcessIdAllocator, } from '@php-wasm/universal'; -import { createSpawnHandler } from '@php-wasm/util'; +import { createSpawnHandler, phpVar } from '@php-wasm/util'; import { IS_JSPI_AVAILABLE } from '@studio/common/lib/jspi'; import { cleanupLegacyMuPlugins, getMuPlugins } from '@studio/common/lib/mu-plugins'; import { LatestSupportedPHPVersion } from '@studio/common/types/php-versions'; import { __ } from '@wordpress/i18n'; import { setupPlatformLevelMuPlugins } from '@wp-playground/wordpress'; +import yauzl from 'yauzl'; import { getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files'; +const require = createRequire( import.meta.url ); + +function getSqliteIntegrationZipPath(): string { + return path.join( + path.dirname( require.resolve( '@wp-playground/cli/package.json' ) ), + 'sqlite-database-integration.zip' + ); +} + +/** + * Node.js reimplementation of preloadSqliteIntegration from @wp-playground/wordpress. + * The upstream version uses php.run() internally (via unzipFile) to extract the zip, + * which corrupts the PHP WASM runtime state for subsequent php.cli() calls. This version + * extracts the zip in Node.js and writes files directly into the PHP VFS instead. + */ +async function setupSqliteIntegration( php: PHP ): Promise< void > { + const pluginVfsPath = '/internal/shared/sqlite-database-integration'; + + // Extract zip in Node.js and write files directly into the PHP VFS. + // We use yauzl (already a transitive dependency) instead of php.run()-based + // extraction to avoid corrupting the PHP WASM runtime state before php.cli(). + const zipPath = getSqliteIntegrationZipPath(); + await new Promise< void >( ( resolve, reject ) => { + yauzl.open( zipPath, { lazyEntries: true }, ( err, zipfile ) => { + if ( err || ! zipfile ) { + return reject( err ); + } + zipfile.readEntry(); + zipfile.on( 'entry', ( entry: yauzl.Entry ) => { + // Strip the top-level directory (e.g. plugin-sqlite-database-integration/) + const relativePath = entry.fileName.replace( /^[^/]+\//, '' ); + if ( ! relativePath || entry.fileName.endsWith( '/' ) ) { + zipfile.readEntry(); + return; + } + const vfsFilePath = `${ pluginVfsPath }/${ relativePath }`; + const dir = path.posix.dirname( vfsFilePath ); + if ( ! php.isDir( dir ) ) { + php.mkdir( dir ); + } + zipfile.openReadStream( entry, ( streamErr, readStream ) => { + if ( streamErr || ! readStream ) { + return reject( streamErr ); + } + const chunks: Buffer[] = []; + readStream.on( 'data', ( chunk: Buffer ) => chunks.push( chunk ) ); + readStream.on( 'end', () => { + php.writeFile( vfsFilePath, new Uint8Array( Buffer.concat( chunks ) ) ); + zipfile.readEntry(); + } ); + readStream.on( 'error', reject ); + } ); + } ); + zipfile.on( 'end', resolve ); + zipfile.on( 'error', reject ); + } ); + } ); + + // Write SQLITE_MAIN_FILE constant + php.defineConstant( 'SQLITE_MAIN_FILE', '1' ); + + // Generate the mu-plugin and preload files from db.copy (same logic as upstream) + const dbCopyContent = php + .readFileAsText( `${ pluginVfsPath }/db.copy` ) + .replace( "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", phpVar( pluginVfsPath ) ) + .replace( "'{SQLITE_PLUGIN}'", phpVar( `${ pluginVfsPath }/load.php` ) ); + + const dbPhpGuard = ``; + + const muPluginPath = '/internal/shared/mu-plugins/sqlite-database-integration.php'; + php.writeFile( muPluginPath, dbPhpGuard + dbCopyContent ); + + const preloadContent = `load_sqlite_integration(); + if($GLOBALS['wpdb'] === $this) { + throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); + } + return call_user_func_array(array($GLOBALS['wpdb'], $name), $arguments); + } + public function __get($name) { + $this->load_sqlite_integration(); + if($GLOBALS['wpdb'] === $this) { + throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); + } + return $GLOBALS['wpdb']->$name; + } + public function __set($name, $value) { + $this->load_sqlite_integration(); + if($GLOBALS['wpdb'] === $this) { + throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); + } + $GLOBALS['wpdb']->$name = $value; + } + protected function load_sqlite_integration() { + require_once ${ phpVar( muPluginPath ) }; + } +} +$GLOBALS['wpdb'] = new Playground_SQLite_Integration_Loader(); +`; + + php.writeFile( '/internal/shared/preload/0-sqlite.php', dbPhpGuard + preloadContent ); +} + const processIdAllocator = new ProcessIdAllocator(); const PLAYGROUND_INTERNAL_SHARED_FOLDER = '/internal/shared'; @@ -97,6 +214,7 @@ export async function runWpCliCommand( await php.mount( '/tmp/sqlite-command', createNodeFsMountHandler( getSqliteCommandPath() ) ); await setupPlatformLevelMuPlugins( php ); + await setupSqliteIntegration( php ); const response = await php.cli( [ 'php', '/tmp/wp-cli.phar', '--path=/wordpress', ...args ] ); diff --git a/apps/cli/lib/server-files.ts b/apps/cli/lib/server-files.ts index cb3b36b4f5..c552c1dbf8 100644 --- a/apps/cli/lib/server-files.ts +++ b/apps/cli/lib/server-files.ts @@ -6,7 +6,6 @@ import { LoggerError } from 'cli/logger'; const WP_CLI_PHAR_FILENAME = 'wp-cli.phar'; const SQLITE_COMMAND_DIRNAME = 'sqlite-command'; -const SQLITE_PLUGIN_DIRNAME = 'sqlite-database-integration'; export function getAppdataDirectory(): string { if ( process.env.E2E && process.env.E2E_APP_DATA_PATH ) { @@ -38,10 +37,6 @@ export function getWpCliPharPath(): string { return path.join( getServerFilesPath(), WP_CLI_PHAR_FILENAME ); } -export function getSqlitePluginPath(): string { - return path.join( getServerFilesPath(), SQLITE_PLUGIN_DIRNAME ); -} - export function getSqliteCommandPath(): string { return path.join( getServerFilesPath(), SQLITE_COMMAND_DIRNAME ); } diff --git a/apps/cli/lib/sqlite-integration.ts b/apps/cli/lib/sqlite-integration.ts deleted file mode 100644 index 92b62a9331..0000000000 --- a/apps/cli/lib/sqlite-integration.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { SqliteIntegrationProvider } from '@studio/common/lib/sqlite-integration'; -import { getServerFilesPath } from '@studio/common/lib/well-known-paths'; - -const SQLITE_FILENAME = 'sqlite-database-integration'; - -class CliSqliteProvider extends SqliteIntegrationProvider { - getServerFilesPath(): string { - return getServerFilesPath(); - } - - getSqliteDirname(): string { - return SQLITE_FILENAME; - } -} - -const provider = new CliSqliteProvider(); - -export async function isSqliteIntegrationAvailable() { - return provider.isSqliteIntegrationAvailable(); -} - -export async function needsSqliteSetup( sitePath: string ) { - return provider.needsSqliteSetup( sitePath ); -} - -export async function installSqliteIntegration( sitePath: string ) { - return provider.installSqliteIntegration( sitePath ); -} - -export async function keepSqliteIntegrationUpdated( sitePath: string ) { - return provider.keepSqliteIntegrationUpdated( sitePath ); -} - -export async function isSqliteIntegrationInstalled( sitePath: string ) { - return provider.isSqliteInstalled( sitePath ); -} - -export async function getSqliteVersionFromInstallation( sqlitePath: string ) { - return provider.getSqliteVersionFromInstallation( sqlitePath ); -} diff --git a/apps/cli/wordpress-server-child.ts b/apps/cli/wordpress-server-child.ts index 881a9574de..35e9782880 100644 --- a/apps/cli/wordpress-server-child.ts +++ b/apps/cli/wordpress-server-child.ts @@ -12,9 +12,13 @@ */ import { dirname } from 'path'; import { DEFAULT_PHP_VERSION } from '@studio/common/constants'; -import { isWordPressDirectory } from '@studio/common/lib/fs-utils'; +import { hasMysqlWpConfig, isWordPressDirectory } from '@studio/common/lib/fs-utils'; import { IS_JSPI_AVAILABLE } from '@studio/common/lib/jspi'; -import { cleanupLegacyMuPlugins, getMuPlugins } from '@studio/common/lib/mu-plugins'; +import { + cleanupLegacyMuPlugins, + cleanupLegacySqliteFiles, + getMuPlugins, +} from '@studio/common/lib/mu-plugins'; import { decodePassword } from '@studio/common/lib/passwords'; import { formatPlaygroundCliMessage } from '@studio/common/lib/playground-cli-messages'; import { sequential } from '@studio/common/lib/sequential'; @@ -33,7 +37,6 @@ import { z } from 'zod'; import { sanitizeRunCLIArgs } from 'cli/lib/cli-args-sanitizer'; import { rewriteWpCliPostContentToFile } from 'cli/lib/rewrite-wp-cli-post-content'; import { getPhpMyAdminPath, getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files'; -import { isSqliteIntegrationInstalled } from 'cli/lib/sqlite-integration'; import { ServerConfig, managerMessageSchema, @@ -114,26 +117,27 @@ async function setAdminCredentials( } /** - * Gets the WordPress installation mode based on whether WordPress files - * and SQLite integration are present. + * Gets the WordPress installation mode based on whether WordPress files exist + * and whether the site is configured to use MySQL. + * + * - No WordPress files yet → download-and-install (Playground installs WP + SQLite) + * - WordPress + MySQL wp-config.php → do-not-attempt-installing (respect existing MySQL setup) + * - WordPress + SQLite (or no wp-config.php) → install-from-existing-files-if-needed * * @param sitePath - The path to the site * @returns The WordPressInstallMode to use for the site */ -async function getWordPressInstallMode( sitePath: string ): Promise< WordPressInstallMode > { - const hasWordPress = isWordPressDirectory( sitePath ); - const hasSqlite = await isSqliteIntegrationInstalled( sitePath ); - - if ( ! hasWordPress ) { +function getWordPressInstallMode( sitePath: string ): WordPressInstallMode { + if ( ! isWordPressDirectory( sitePath ) ) { return 'download-and-install'; } - if ( hasSqlite ) { - return 'install-from-existing-files-if-needed'; + if ( hasMysqlWpConfig( sitePath ) ) { + // We don't want Playground to attempt installing WordPress when site is using MySQL. + return 'do-not-attempt-installing'; } - // We don't want playground to attempt installing WordPress when site is using MySQL. - return 'do-not-attempt-installing'; + return 'install-from-existing-files-if-needed'; } function getBaseRunCLIArgs( @@ -148,9 +152,13 @@ async function getBaseRunCLIArgs( command: RunCLIArgs[ 'command' ], config: ServerConfig ): Promise< RunCLIArgs > { - const wordpressInstallMode = await getWordPressInstallMode( config.sitePath ); + const wordpressInstallMode = getWordPressInstallMode( config.sitePath ); + const skipSqliteSetup = hasMysqlWpConfig( config.sitePath ); await cleanupLegacyMuPlugins( config.sitePath ); + if ( ! skipSqliteSetup ) { + await cleanupLegacySqliteFiles( config.sitePath ); + } const [ studioMuPluginsHostPath, loaderMuPluginHostPath ] = await getMuPlugins( { isWpAutoUpdating: config.isWpAutoUpdating, @@ -239,7 +247,7 @@ async function getBaseRunCLIArgs( internalCookieStore: false, login: false, followSymlinks: true, - skipSqliteSetup: true, + skipSqliteSetup, port: config.port, 'mount-before-install': mounts, 'site-url': config.absoluteUrl || `http://localhost:${ config.port }`, diff --git a/apps/studio/src/constants.ts b/apps/studio/src/constants.ts index 2730daa16b..2112bcf91a 100644 --- a/apps/studio/src/constants.ts +++ b/apps/studio/src/constants.ts @@ -55,11 +55,6 @@ export const WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS = 6; export const WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT = WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT_IN_HRS * 60 * 60 * 1000; // 6hr -// SQLite -const SQLITE_DATABASE_INTEGRATION_VERSION = 'v2.2.23'; - -export const SQLITE_DATABASE_INTEGRATION_RELEASE_URL = `https://github.com/WordPress/sqlite-database-integration/releases/download/${ SQLITE_DATABASE_INTEGRATION_VERSION }/plugin-sqlite-database-integration.zip`; - // IPC handlers that don't return anything (i.e. that are called with `ipcRenderer.send`) export const IPC_VOID_HANDLERS = < const >[ 'addSyncOperation', diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index e9550caf22..93f138ca1b 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -77,7 +77,6 @@ import { setSentryWpcomUserIdMain } from 'src/lib/main-sentry-utils'; import * as oauthClient from 'src/lib/oauth'; import { getAiInstructionsPath } from 'src/lib/server-files-paths'; import { shellOpenExternalWrapper } from 'src/lib/shell-open-external-wrapper'; -import { installSqliteIntegration, keepSqliteIntegrationUpdated } from 'src/lib/sqlite-versions'; import * as windowsHelpers from 'src/lib/windows-helpers'; import { setupWordPressFilesOnly } from 'src/lib/wordpress-setup'; import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging'; @@ -370,10 +369,6 @@ export async function importSite( await setupWordPressFilesOnly( site.details.path ); } - if ( ! ( await site.hasSQLitePlugin() ) ) { - await installSqliteIntegration( site.details.path ); - } - const onEvent = ( data: ImportExportEventData ) => { const parentWindow = BrowserWindow.fromWebContents( event.sender ); sendIpcEventToRendererWithWindow( parentWindow, 'on-import', data, id ); @@ -838,8 +833,6 @@ export async function exportSite( options: ExportOptions ): Promise< boolean > { try { - await keepSqliteIntegrationUpdated( options.site.path ); - const onEvent = ( data: ImportExportEventData ) => { const parentWindow = BrowserWindow.fromWebContents( event.sender ); sendIpcEventToRendererWithWindow( parentWindow, 'on-export', data, options.site.id ); @@ -1334,7 +1327,7 @@ export async function isImportExportSupported( _event: IpcMainInvokeEvent, siteI if ( ! site ) { throw new Error( 'Site not found.' ); } - return site.hasSQLitePlugin(); + return site.isSqliteSite(); } export function getDirectorySize( _event: IpcMainInvokeEvent, siteId: string, subdir: string[] ) { diff --git a/apps/studio/src/lib/server-files-paths.ts b/apps/studio/src/lib/server-files-paths.ts index cf40c89471..9f70fe5011 100644 --- a/apps/studio/src/lib/server-files-paths.ts +++ b/apps/studio/src/lib/server-files-paths.ts @@ -7,9 +7,6 @@ import os from 'os'; import path from 'path'; import { getServerFilesPath } from '@studio/common/lib/well-known-paths'; -// SQLite integration folder name -const SQLITE_FILENAME = 'sqlite-database-integration'; - /** * Get a temporary path for tests */ @@ -41,13 +38,6 @@ export function getWordPressVersionPath( version: string ): string { return path.join( getWordPressVersionsPath(), version ); } -/** - * The full path to the "SQLite database integration" folder. - */ -export function getSqlitePath(): string { - return path.join( getBasePath(), SQLITE_FILENAME ); -} - /** * The path where bundled AI instructions and skills are stored. */ diff --git a/apps/studio/src/lib/sqlite-versions.ts b/apps/studio/src/lib/sqlite-versions.ts deleted file mode 100644 index 40c30a46f8..0000000000 --- a/apps/studio/src/lib/sqlite-versions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SQLITE_FILENAME } from '@studio/common/constants'; -import { SqliteIntegrationProvider } from '@studio/common/lib/sqlite-integration'; -import { getServerFilesPath } from '@studio/common/lib/well-known-paths'; - -class ElectronSqliteProvider extends SqliteIntegrationProvider { - getServerFilesPath(): string { - return getServerFilesPath(); - } - - getSqliteDirname(): string { - return SQLITE_FILENAME; - } -} - -const provider = new ElectronSqliteProvider(); - -export const getSqliteVersionFromInstallation = ( sqliteMuPluginPath: string ) => - provider.getSqliteVersionFromInstallation( sqliteMuPluginPath ); - -export const keepSqliteIntegrationUpdated = ( sitePath: string ) => - provider.keepSqliteIntegrationUpdated( sitePath ); - -export const installSqliteIntegration = ( sitePath: string ) => - provider.installSqliteIntegration( sitePath ); diff --git a/apps/studio/src/modules/sync/lib/ipc-handlers.ts b/apps/studio/src/modules/sync/lib/ipc-handlers.ts index 801d710868..80d75eab37 100644 --- a/apps/studio/src/modules/sync/lib/ipc-handlers.ts +++ b/apps/studio/src/modules/sync/lib/ipc-handlers.ts @@ -20,7 +20,6 @@ import { getSyncBackupTempPath } from 'src/lib/get-sync-backup-temp-path'; import { exportBackup } from 'src/lib/import-export/export/export-manager'; import { ExportOptions } from 'src/lib/import-export/export/types'; import { getAuthenticationToken } from 'src/lib/oauth'; -import { keepSqliteIntegrationUpdated } from 'src/lib/sqlite-versions'; import { SiteServer } from 'src/site-server'; import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; import { SyncOption } from 'src/types'; @@ -161,8 +160,6 @@ export async function exportSiteForPush( throw new Error( 'Export aborted' ); } - await keepSqliteIntegrationUpdated( site.details.path ); - const shouldIncludeSyncOption = ( optionsToSync: SyncOption[] | undefined, option: SyncOption diff --git a/apps/studio/src/site-server.ts b/apps/studio/src/site-server.ts index 3ddc32dadb..4328f3f1a5 100644 --- a/apps/studio/src/site-server.ts +++ b/apps/studio/src/site-server.ts @@ -1,10 +1,9 @@ import fs from 'fs'; import nodePath from 'path'; import * as Sentry from '@sentry/electron/main'; -import { SQLITE_FILENAME } from '@studio/common/constants'; import { siteListSchema, type SiteListItem } from '@studio/common/lib/cli-events'; +import { hasMysqlWpConfig } from '@studio/common/lib/fs-utils'; import { parseJsonFromPhpOutput } from '@studio/common/lib/php-output-parser'; -import fsExtra from 'fs-extra'; import { parse } from 'shell-quote'; import { z } from 'zod'; import { @@ -437,33 +436,7 @@ export class SiteServer { } } - async hasSQLitePlugin(): Promise< boolean > { - const wpContentPath = nodePath.join( this.details.path, 'wp-content' ); - - const sqliteIntegrationPaths = { - muPlugin: nodePath.join( wpContentPath, 'mu-plugins', SQLITE_FILENAME ), - muPluginLegacy: nodePath.join( wpContentPath, 'mu-plugins', `${ SQLITE_FILENAME }-main` ), - regularPlugin: nodePath.join( wpContentPath, 'plugins', SQLITE_FILENAME ), - }; - - const requiredConfigPaths = { - wpConfig: nodePath.join( this.details.path, 'wp-config.php' ), - dbConfig: nodePath.join( wpContentPath, 'db.php' ), - dbSqlite: nodePath.join( wpContentPath, 'database', '.ht.sqlite' ), - }; - - const anyIntegrationExists = await Promise.all( [ - fsExtra.pathExists( sqliteIntegrationPaths.muPlugin ), - fsExtra.pathExists( sqliteIntegrationPaths.muPluginLegacy ), - fsExtra.pathExists( sqliteIntegrationPaths.regularPlugin ), - ] ).then( ( results ) => results.some( Boolean ) ); - - const configFilesExist = await Promise.all( [ - fsExtra.pathExists( requiredConfigPaths.wpConfig ), - fsExtra.pathExists( requiredConfigPaths.dbConfig ), - fsExtra.pathExists( requiredConfigPaths.dbSqlite ), - ] ).then( ( results ) => results.every( Boolean ) ); - - return anyIntegrationExists && configFilesExist; + isSqliteSite(): boolean { + return ! hasMysqlWpConfig( this.details.path ); } } diff --git a/scripts/download-wp-server-files.ts b/scripts/download-wp-server-files.ts index fe9ca6cc2d..c95d0c9235 100644 --- a/scripts/download-wp-server-files.ts +++ b/scripts/download-wp-server-files.ts @@ -6,7 +6,6 @@ import { getPhpMyAdminInstallSteps, } from '@wp-playground/tools'; import fs from 'fs-extra'; -import { SQLITE_DATABASE_INTEGRATION_RELEASE_URL } from '../apps/studio/src/constants'; import { getLatestSQLiteCommandRelease } from '../apps/studio/src/lib/sqlite-command-release'; import { extractZip } from '../tools/common/lib/extract-zip'; @@ -27,11 +26,6 @@ const FILES_TO_DOWNLOAD: FileToDownload[] = [ getUrl: () => 'https://wordpress.org/latest.zip', destinationPath: path.join( WP_SERVER_FILES_PATH, 'latest' ), }, - { - name: 'sqlite', - description: 'SQLite files', - getUrl: () => SQLITE_DATABASE_INTEGRATION_RELEASE_URL, - }, { name: 'wp-cli', description: 'WP-CLI phar file', diff --git a/tools/common/lib/fs-utils.ts b/tools/common/lib/fs-utils.ts index 77f9e79c73..67d9af0f15 100644 --- a/tools/common/lib/fs-utils.ts +++ b/tools/common/lib/fs-utils.ts @@ -72,6 +72,27 @@ export function isWordPressDirectory( projectPath: string ): boolean { ); } +/** + * Returns true if the site's wp-config.php defines real MySQL credentials + * (non-placeholder DB_NAME and DB_USER), indicating the site uses MySQL + * rather than SQLite. + * + * Mirrors the detection logic used internally by Playground's boot process. + */ +export function hasMysqlWpConfig( sitePath: string ): boolean { + const wpConfigPath = path.join( sitePath, 'wp-config.php' ); + if ( ! fs.existsSync( wpConfigPath ) ) { + return false; + } + const wpConfig = fs.readFileSync( wpConfigPath, 'utf8' ); + const dbNameMatch = wpConfig.match( /define\s*\(\s*['"]DB_NAME['"]\s*,\s*['"]([^'"]*)['"]\s*\)/ ); + const dbUserMatch = wpConfig.match( /define\s*\(\s*['"]DB_USER['"]\s*,\s*['"]([^'"]*)['"]\s*\)/ ); + if ( ! dbNameMatch || ! dbUserMatch ) { + return false; + } + return dbNameMatch[ 1 ] !== 'database_name_here' && dbUserMatch[ 1 ] !== 'username_here'; +} + // Compare paths, preferring inode comparison when both paths exist on disk. // `fs.Stats.dev` signifies the device ID, and `fs.Stats.ino` signifies the inode number // that uniquely identifies the file or directory. This approach respects the current file diff --git a/tools/common/lib/mu-plugins.ts b/tools/common/lib/mu-plugins.ts index 584e6a5052..94efc4ce00 100644 --- a/tools/common/lib/mu-plugins.ts +++ b/tools/common/lib/mu-plugins.ts @@ -5,7 +5,7 @@ * available to WordPress instances. Shared between desktop app and CLI. */ -import { mkdtemp, readdir, unlink, writeFile } from 'fs/promises'; +import { access, mkdtemp, readdir, rm, unlink, writeFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; @@ -83,8 +83,11 @@ function getStandardMuPlugins( options: MuPluginOptions ): MuPlugin[] { content: ` { + const wpContentPath = join( sitePath, 'wp-content' ); + + const pathsToRemove = [ + join( wpContentPath, 'mu-plugins', 'sqlite-database-integration' ), + join( wpContentPath, 'db.php' ), + ]; + + await Promise.all( + pathsToRemove.map( async ( targetPath ) => { + try { + await access( targetPath ); + } catch { + // Path doesn't exist — nothing to do + return; + } + try { + await rm( targetPath, { recursive: true, force: true } ); + } catch { + // Best-effort: ignore errors (file locked, permissions, etc.) + } + } ) + ); +} diff --git a/tools/common/lib/sqlite-integration.ts b/tools/common/lib/sqlite-integration.ts deleted file mode 100644 index ecf54f7388..0000000000 --- a/tools/common/lib/sqlite-integration.ts +++ /dev/null @@ -1,82 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -// Abstract base class for SQLite integration across different contexts -export abstract class SqliteIntegrationProvider { - abstract getServerFilesPath(): string; - abstract getSqliteDirname(): string; - - protected getSqlitePluginSourcePath(): string { - return path.join( this.getServerFilesPath(), this.getSqliteDirname() ); - } - - async isSqliteIntegrationAvailable(): Promise< boolean > { - const sqliteSourcePath = this.getSqlitePluginSourcePath(); - const dbCopyPath = path.join( sqliteSourcePath, 'db.copy' ); - return fs.existsSync( sqliteSourcePath ) && fs.existsSync( dbCopyPath ); - } - - // Returns true if site has db.php or no wp-config.php - async needsSqliteSetup( sitePath: string ): Promise< boolean > { - const hasDbPhp = fs.existsSync( path.join( sitePath, 'wp-content', 'db.php' ) ); - const hasWpConfig = fs.existsSync( path.join( sitePath, 'wp-config.php' ) ); - return hasDbPhp || ! hasWpConfig; - } - - async getSqliteVersionFromInstallation( sqliteMuPluginPath: string ): Promise< string > { - try { - const versionFileContent = await fs.promises.readFile( - path.join( sqliteMuPluginPath, 'load.php' ), - 'utf8' - ); - const matches = versionFileContent.match( /\s\*\sVersion:\s*([0-9a-zA-Z.-]+)/ ); - return matches?.[ 1 ] || ''; - } catch ( err ) { - return ''; - } - } - - async installSqliteIntegration( sitePath: string ): Promise< void > { - if ( ! ( await this.isSqliteIntegrationAvailable() ) ) { - throw new Error( 'SQLite integration files not found.' ); - } - - const wpContentPath = path.join( sitePath, 'wp-content' ); - const databasePath = path.join( wpContentPath, 'database' ); - - await fs.promises.mkdir( databasePath, { recursive: true } ); - - const sqliteSourcePath = this.getSqlitePluginSourcePath(); - const dbCopyContent = await fs.promises.readFile( - path.join( sqliteSourcePath, 'db.copy' ), - 'utf8' - ); - const sqliteDirname = this.getSqliteDirname(); - const updatedContent = dbCopyContent.replace( - "'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", - `realpath( __DIR__ . '/mu-plugins/${ sqliteDirname }' )` - ); - await fs.promises.writeFile( path.join( wpContentPath, 'db.php' ), updatedContent ); - - const sqliteDestPath = path.join( wpContentPath, 'mu-plugins', sqliteDirname ); - await fs.promises.cp( sqliteSourcePath, sqliteDestPath, { - recursive: true, - verbatimSymlinks: true, - } ); - } - - async keepSqliteIntegrationUpdated( sitePath: string ): Promise< boolean > { - if ( await this.needsSqliteSetup( sitePath ) ) { - await this.installSqliteIntegration( sitePath ); - return true; - } - return false; - } - - async isSqliteInstalled( sitePath: string ): Promise< boolean > { - return ( - fs.existsSync( path.join( sitePath, 'wp-content', 'mu-plugins', this.getSqliteDirname() ) ) && - fs.existsSync( path.join( sitePath, 'wp-content', 'db.php' ) ) - ); - } -} diff --git a/tools/common/lib/tests/sqlite-integration.test.ts b/tools/common/lib/tests/sqlite-integration.test.ts deleted file mode 100644 index d5c3190aff..0000000000 --- a/tools/common/lib/tests/sqlite-integration.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { vol } from 'memfs'; -import { vi } from 'vitest'; -import { SqliteIntegrationProvider } from '../sqlite-integration'; -import { platformTestSuite } from './utils/platform-test-suite'; - -// memfs path resolution uses path.sep. platformTestSuite overrides path.sep to -// '\\' for Windows tests, which breaks vol operations. Restore POSIX sep -// temporarily around every vol call so memfs parses paths correctly. -function volFromJSON( files: Record< string, string > ): void { - const savedSep = path.sep; - // @ts-expect-error — Temporarily restore POSIX separator for memfs compatibility - path.sep = '/'; - try { - const posixFiles: Record< string, string > = {}; - for ( const [ key, value ] of Object.entries( files ) ) { - posixFiles[ key.replace( /\\/g, '/' ) ] = value; - } - vol.fromJSON( posixFiles ); - } finally { - // @ts-expect-error — Restore original separator - path.sep = savedSep; - } -} - -const SQLITE_DIRNAME = 'sqlite-database-integration'; -const MOCK_SITE_PATH = 'mock-site-path'; - -vi.mock( 'fs' ); - -class TestSqliteProvider extends SqliteIntegrationProvider { - getServerFilesPath(): string { - return 'server-files'; - } - - getSqliteDirname(): string { - return SQLITE_DIRNAME; - } -} - -platformTestSuite( 'SqliteIntegrationProvider', ( { normalize } ) => { - let provider: TestSqliteProvider; - - beforeEach( () => { - provider = new TestSqliteProvider(); - vi.clearAllMocks(); - vol.reset(); - } ); - - describe( 'needsSqliteSetup', () => { - it( 'should return true when db.php exists', async () => { - volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: '' } ); - - const result = await provider.needsSqliteSetup( MOCK_SITE_PATH ); - - expect( result ).toBe( true ); - } ); - - it( 'should return true when wp-config.php does not exist', async () => { - const result = await provider.needsSqliteSetup( MOCK_SITE_PATH ); - - expect( result ).toBe( true ); - } ); - - it( 'should return false when wp-config.php exists and db.php does not', async () => { - volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-config.php` ) ]: 'config' } ); - - const result = await provider.needsSqliteSetup( MOCK_SITE_PATH ); - - expect( result ).toBe( false ); - } ); - - it( 'should return true when both files exist (db.php takes precedence)', async () => { - volFromJSON( { - [ normalize( `${ MOCK_SITE_PATH }/wp-config.php` ) ]: 'config', - [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: 'db-content', - } ); - - const result = await provider.needsSqliteSetup( MOCK_SITE_PATH ); - - expect( result ).toBe( true ); - } ); - } ); - - describe( 'installSqliteIntegration', () => { - beforeEach( () => { - volFromJSON( { - [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: - "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", - } ); - } ); - - it( 'should create database directory', async () => { - await provider.installSqliteIntegration( MOCK_SITE_PATH ); - - expect( vi.mocked( fs.promises.mkdir ) ).toHaveBeenCalledWith( - normalize( `${ MOCK_SITE_PATH }/wp-content/database` ), - { recursive: true } - ); - } ); - - it( 'should write db.php with correct path', async () => { - await provider.installSqliteIntegration( MOCK_SITE_PATH ); - - expect( vi.mocked( fs.promises.writeFile ) ).toHaveBeenCalledWith( - normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ), - `SQLIntegration path: realpath( __DIR__ . '/mu-plugins/${ SQLITE_DIRNAME }' )` - ); - } ); - - it( 'should copy SQLite plugin to mu-plugins', async () => { - await provider.installSqliteIntegration( MOCK_SITE_PATH ); - - expect( vi.mocked( fs.promises.cp ) ).toHaveBeenCalledWith( - normalize( `server-files/${ SQLITE_DIRNAME }` ), - normalize( `${ MOCK_SITE_PATH }/wp-content/mu-plugins/${ SQLITE_DIRNAME }` ), - expect.any( Object ) - ); - } ); - - it( 'should throw error when SQLite files not available', async () => { - provider.isSqliteIntegrationAvailable = vi.fn().mockResolvedValue( false ); - - await expect( provider.installSqliteIntegration( MOCK_SITE_PATH ) ).rejects.toThrow( - 'SQLite integration files not found' - ); - } ); - } ); - - describe( 'keepSqliteIntegrationUpdated', () => { - beforeEach( () => { - volFromJSON( { - [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: - "SQLIntegration path: '{SQLITE_IMPLEMENTATION_FOLDER_PATH}'", - } ); - } ); - - it( 'should install when db.php exists', async () => { - volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-content/db.php` ) ]: '' } ); - - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); - - expect( vi.mocked( fs.promises.cp ) ).toHaveBeenCalledWith( - normalize( `server-files/${ SQLITE_DIRNAME }` ), - normalize( `${ MOCK_SITE_PATH }/wp-content/mu-plugins/${ SQLITE_DIRNAME }` ), - expect.any( Object ) - ); - } ); - - it( 'should install when wp-config.php does not exist', async () => { - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); - - expect( vi.mocked( fs.promises.cp ) ).toHaveBeenCalledWith( - normalize( `server-files/${ SQLITE_DIRNAME }` ), - normalize( `${ MOCK_SITE_PATH }/wp-content/mu-plugins/${ SQLITE_DIRNAME }` ), - expect.any( Object ) - ); - } ); - - it( 'should not install when wp-config.php exists and db.php does not', async () => { - volFromJSON( { [ normalize( `${ MOCK_SITE_PATH }/wp-config.php` ) ]: 'config' } ); - - await provider.keepSqliteIntegrationUpdated( MOCK_SITE_PATH ); - - expect( vi.mocked( fs.promises.cp ) ).not.toHaveBeenCalled(); - } ); - } ); - - describe( 'getSqliteVersionFromInstallation', () => { - it( 'should extract version from load.php', async () => { - const loadPhpContent = ` { - const version = await provider.getSqliteVersionFromInstallation( - normalize( 'non-existent' ) - ); - - expect( version ).toBe( '' ); - } ); - - it( 'should return empty string if version cannot be parsed', async () => { - volFromJSON( { - [ normalize( 'mu-plugins/sqlite/load.php' ) ]: ' { - it( 'should return true when both plugin and db.copy exist', async () => { - volFromJSON( { - [ normalize( `server-files/${ SQLITE_DIRNAME }/db.copy` ) ]: 'content', - } ); - - const available = await provider.isSqliteIntegrationAvailable(); - - expect( available ).toBe( true ); - } ); - - it( 'should return false when plugin does not exist', async () => { - const available = await provider.isSqliteIntegrationAvailable(); - - expect( available ).toBe( false ); - } ); - - it( 'should return false when db.copy does not exist', async () => { - volFromJSON( { - [ normalize( `server-files/${ SQLITE_DIRNAME }/other-file` ) ]: 'content', - } ); - - const available = await provider.isSqliteIntegrationAvailable(); - - expect( available ).toBe( false ); - } ); - } ); -} );