diff --git a/apps/cli/commands/site/set.ts b/apps/cli/commands/site/set.ts index e4894151e4..e98ece0add 100644 --- a/apps/cli/commands/site/set.ts +++ b/apps/cli/commands/site/set.ts @@ -54,6 +54,7 @@ export interface SetCommandOptions { adminEmail?: string; debugLog?: boolean; debugDisplay?: boolean; + phpmyadmin?: boolean; } export async function runCommand( sitePath: string, options: SetCommandOptions ): Promise< void > { @@ -68,6 +69,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) adminPassword, debugLog, debugDisplay, + phpmyadmin, } = options; let { adminEmail } = options; @@ -82,11 +84,12 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) adminPassword === undefined && adminEmail === undefined && debugLog === undefined && - debugDisplay === undefined + debugDisplay === undefined && + phpmyadmin === undefined ) { throw new LoggerError( __( - 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display, --phpmyadmin) is required.' ) ); } @@ -171,6 +174,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) const debugLogChanged = debugLog !== undefined && debugLog !== site.enableDebugLog; const debugDisplayChanged = debugDisplay !== undefined && debugDisplay !== site.enableDebugDisplay; + const phpmyadminChanged = phpmyadmin !== undefined && phpmyadmin !== site.enablePhpMyAdmin; const hasChanges = nameChanged || @@ -181,7 +185,8 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) xdebugChanged || credentialsChanged || debugLogChanged || - debugDisplayChanged; + debugDisplayChanged || + phpmyadminChanged; if ( ! hasChanges ) { throw new LoggerError( __( 'No changes to apply. The site already has the specified settings.' ) @@ -197,6 +202,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) credentialsChanged, debugLogChanged, debugDisplayChanged, + phpmyadminChanged, } ); const oldDomain = site.customDomain; @@ -238,6 +244,9 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) if ( debugDisplayChanged ) { foundSite.enableDebugDisplay = debugDisplay; } + if ( phpmyadminChanged ) { + foundSite.enablePhpMyAdmin = phpmyadmin; + } await saveAppdata( appdata ); site = foundSite; @@ -392,6 +401,10 @@ export const registerCommand = ( yargs: StudioArgv ) => { .option( 'debug-display', { type: 'boolean', description: __( 'Enable WP_DEBUG_DISPLAY' ), + } ) + .option( 'phpmyadmin', { + type: 'boolean', + description: __( 'Enable phpMyAdmin' ), } ); }, handler: async ( argv ) => { @@ -408,6 +421,7 @@ export const registerCommand = ( yargs: StudioArgv ) => { adminEmail: argv.adminEmail, debugLog: argv.debugLog, debugDisplay: argv.debugDisplay, + phpmyadmin: argv.phpmyadmin, } ); } catch ( error ) { if ( error instanceof LoggerError ) { diff --git a/apps/cli/commands/site/tests/set.test.ts b/apps/cli/commands/site/tests/set.test.ts index 2d72ae6d6a..4893bf913a 100644 --- a/apps/cli/commands/site/tests/set.test.ts +++ b/apps/cli/commands/site/tests/set.test.ts @@ -100,7 +100,7 @@ describe( 'CLI: studio site set', () => { describe( 'Validation', () => { it( 'should throw when no options provided', async () => { await expect( runCommand( testSitePath, {} ) ).rejects.toThrow( - 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display, --phpmyadmin) is required.' ); } ); diff --git a/apps/cli/lib/server-files.ts b/apps/cli/lib/server-files.ts index b148eb4733..4d9aef3a49 100644 --- a/apps/cli/lib/server-files.ts +++ b/apps/cli/lib/server-files.ts @@ -23,3 +23,7 @@ export function getLanguagePacksPath(): string { export function getAgentSkillsPath(): string { return path.join( getServerFilesPath(), 'agent-skills' ); } + +export function getPhpMyAdminPath(): string { + return path.join( getServerFilesPath(), 'phpmyadmin' ); +} diff --git a/apps/cli/lib/types/wordpress-server-ipc.ts b/apps/cli/lib/types/wordpress-server-ipc.ts index 0108b5890f..e6a60c7b95 100644 --- a/apps/cli/lib/types/wordpress-server-ipc.ts +++ b/apps/cli/lib/types/wordpress-server-ipc.ts @@ -17,6 +17,7 @@ const serverConfig = z.object( { enableXdebug: z.boolean().optional(), enableDebugLog: z.boolean().optional(), enableDebugDisplay: z.boolean().optional(), + enablePhpMyAdmin: z.boolean().optional(), blueprint: z .object( { contents: z.any(), // Blueprint type is complex, allow any for now diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index e74249d887..c6a8790ef2 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -115,6 +115,10 @@ export async function startWordPressServer( serverConfig.enableDebugDisplay = true; } + if ( site.enablePhpMyAdmin ) { + serverConfig.enablePhpMyAdmin = true; + } + const processDesc = await startProcess( processName, wordPressServerChildPath ); await waitForReadyMessage( processDesc.pmId ); await sendMessage( @@ -366,6 +370,10 @@ export async function runBlueprint( serverConfig.enableDebugDisplay = true; } + if ( site.enablePhpMyAdmin ) { + serverConfig.enablePhpMyAdmin = true; + } + const processDesc = await startProcess( processName, wordPressServerChildPath ); try { await waitForReadyMessage( processDesc.pmId ); diff --git a/apps/cli/patches/@wp-playground+cli+3.1.12.patch b/apps/cli/patches/@wp-playground+cli+3.1.12.patch new file mode 100644 index 0000000000..375ef92809 --- /dev/null +++ b/apps/cli/patches/@wp-playground+cli+3.1.12.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js b/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js +index 1314a71..92c2e31 100644 +--- a/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js ++++ b/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js +@@ -1259,10 +1259,7 @@ async function Lt(e) { + w.setSeverityFilterLevel(c); + } + if (e.intl || (e.intl = !0), e.redis === void 0 && (e.redis = await ie()), e.memcached === void 0 && (e.memcached = await ie()), e.phpmyadmin) { +- if (e.phpmyadmin === !0 && (e.phpmyadmin = "/phpmyadmin"), e.skipSqliteSetup) +- throw new Error( +- "--phpmyadmin requires SQLite. Cannot be used with --skip-sqlite-setup." +- ); ++ if (e.phpmyadmin === !0 && (e.phpmyadmin = "/phpmyadmin")); + e.pathAliases = [ + { + urlPrefix: e.phpmyadmin, +diff --git a/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs b/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs +index 9b7bc95..e16e51d 100644 +--- a/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs ++++ b/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs +@@ -44,7 +44,7 @@ Examples: + wp-playground start --wp=6.7 --php=8.3 # Use specific versions + wp-playground start --skip-browser # Skip opening browser + wp-playground start --no-auto-mount # Disable auto-detection`).options(o)).command("server","Start a local WordPress server (advanced, low-level)",i=>i.options({...t,...r})).command("run-blueprint","Execute a Blueprint without starting a server",i=>i.options({...t})).command("build-snapshot","Build a ZIP snapshot of a WordPress site based on a Blueprint",i=>i.options({...t,...s})).command("php","Run a PHP script",i=>i.options({...t})).demandCommand(1,"Please specify a command").strictCommands().conflicts("experimental-unsafe-ide-integration","experimental-devtools").showHelpOnFail(!1).fail((i,h,g)=>{if(h)throw h;i&&i.includes("Please specify a command")&&(g.showHelp(),console.error(` +-`+i),process.exit(1)),console.error(i),process.exit(1)}).strictOptions().check(async i=>{if(i["skip-wordpress-install"]===!0&&(i["wordpress-install-mode"]="do-not-attempt-installing",i.wordpressInstallMode="do-not-attempt-installing"),i.wp!==void 0&&typeof i.wp=="string"&&!je(i.wp))try{new URL(i.wp)}catch{throw new Error('Unrecognized WordPress version. Please use "latest", a URL, or a numeric version such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"')}const h=i["site-url"];if(typeof h=="string"&&h.trim()!=="")try{new URL(h)}catch{throw new Error(`Invalid site-url "${h}". Please provide a valid URL (e.g., http://localhost:8080 or https://example.com)`)}if(i["auto-mount"]){let g=!1;try{g=c.statSync(i["auto-mount"]).isDirectory()}catch{g=!1}if(!g)throw new Error(`The specified --auto-mount path is not a directory: '${i["auto-mount"]}'.`)}if(i["experimental-blueprints-v2-runner"]===!0)throw new Error("Blueprints v2 are temporarily disabled while we rework their runtime implementation.");if(i.mode!==void 0)throw new Error("The --mode option requires the --experimentalBlueprintsV2Runner flag.");return!0});n.wrap(n.terminalWidth());const a=await n.argv,l=a._[0];["start","run-blueprint","server","build-snapshot","php"].includes(l)||(n.showHelp(),process.exit(1));const u=a.define||{};!("WP_DEBUG"in u)&&!("WP_DEBUG_LOG"in u)&&!("WP_DEBUG_DISPLAY"in u)&&(u.WP_DEBUG="true",u.WP_DEBUG_LOG="true",u.WP_DEBUG_DISPLAY="true");const b={...a,define:u,command:l,mount:[...a.mount||[],...a["mount-dir"]||[]],"mount-before-install":[...a["mount-before-install"]||[],...a["mount-dir-before-install"]||[]]},T=await fe(b);T===void 0&&process.exit(0);const p=(()=>{let i;return async()=>{i===void 0&&(i=T[Symbol.asyncDispose]()),await i,process.exit(0)}})();return process.on("SIGINT",p),process.on("SIGTERM",p),{[Symbol.asyncDispose]:async()=>{process.off("SIGINT",p),process.off("SIGTERM",p),await T[Symbol.asyncDispose]()},[K]:{cliServer:T}}}catch(t){if(console.error(t),!(t instanceof Error))throw t;if(process.argv.includes("--debug"))y.printDebugDetails(t);else{const o=[];let s=t;do o.push(s.message),s=s.cause;while(s instanceof Error);console.error("\x1B[1m"+o.join(" caused by: ")+"\x1B[0m")}process.exit(1)}}function le(e,t){return e.find(r=>r.vfsPath.replace(/\/$/,"")===t.replace(/\/$/,""))}const K=Symbol("playground-cli-testing"),C=e=>process.stdout.isTTY?"\x1B[1m"+e+"\x1B[0m":e,nt=e=>process.stdout.isTTY?"\x1B[31m"+e+"\x1B[0m":e,ue=e=>process.stdout.isTTY?`\x1B[2m${e}\x1B[0m`:e,H=e=>process.stdout.isTTY?`\x1B[3m${e}\x1B[0m`:e,z=e=>process.stdout.isTTY?`\x1B[33m${e}\x1B[0m`:e;async function fe(e){let t;const r=e.internalCookieStore?new y.HttpCookieStore:void 0,o=[],s=new Map;if(e.command==="start"&&(e=it(e)),e.autoMount!==void 0&&(e.autoMount===""&&(e={...e,autoMount:process.cwd()}),e=pe(e)),e.wordpressInstallMode===void 0&&(e.wordpressInstallMode="download-and-install"),e.quiet&&(e.verbosity="quiet",delete e.quiet),e.debug&&(e.verbosity="debug",delete e.debug),e.verbosity){const p=Object.values(J).find(i=>i.name===e.verbosity).severity;m.logger.setSeverityFilterLevel(p)}if(e.intl||(e.intl=!0),e.redis===void 0&&(e.redis=await ne.jspi()),e.memcached===void 0&&(e.memcached=await ne.jspi()),e.phpmyadmin){if(e.phpmyadmin===!0&&(e.phpmyadmin="/phpmyadmin"),e.skipSqliteSetup)throw new Error("--phpmyadmin requires SQLite. Cannot be used with --skip-sqlite-setup.");e.pathAliases=[{urlPrefix:e.phpmyadmin,fsPath:_.PHPMYADMIN_INSTALL_PATH}]}const n=new ot({verbosity:e.verbosity||"normal"});e.command==="server"&&(n.printBanner(),n.printConfig({phpVersion:e.php||B.RecommendedPHPVersion,wpVersion:e.wp||"latest",port:e.port??9400,xdebug:!!e.xdebug,intl:!!e.intl,redis:!!e.redis,memcached:!!e.memcached,mounts:[...e.mount||[],...e["mount-before-install"]||[]],blueprint:typeof e.blueprint=="string"?e.blueprint:void 0}));const a=e.command==="server"?e.port??9400:0,l=new y.FileLockManagerInMemory;let u=!1,b=!0;const T=await Ne({port:e.port?e.port:await He(a)?0:a,onBind:async(p,i)=>{const h="127.0.0.1",g=`http://${h}:${i}`,F=e["site-url"]||g,O=6,ee="-playground-cli-site-",R=await Je(ee);m.logger.debug(`Native temp dir for VFS root: ${R.path}`);const L="WP Playground CLI - Listen for Xdebug",te=".playground-xdebug-root",q=d.join(process.cwd(),te);if(await A.removeTempDirSymlink(q),e.xdebug){const f={hostPath:d.join(".",d.sep,te),vfsPath:"/"};if(y.SupportedPHPVersions.indexOf(e.php||B.RecommendedPHPVersion)<=y.SupportedPHPVersions.indexOf("8.5"))await A.createTempDirSymlink(R.path,q,process.platform),e.xdebug=A.makeXdebugConfig({cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],pathSkippings:["/dev/","/home/","/internal/","/request/","/proc/"]}),console.log(C("Xdebug configured successfully")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")));else if(e.experimentalUnsafeIdeIntegration){await A.createTempDirSymlink(R.path,q,process.platform);try{await A.clearXdebugIDEConfig(L,process.cwd());const w=typeof e.xdebug=="object"?e.xdebug:{},P=await A.addXdebugIDEConfig({name:L,host:h,port:i,ides:e.experimentalUnsafeIdeIntegration,cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],ideKey:w.ideKey||"WPPLAYGROUNDCLI"}),S=e.experimentalUnsafeIdeIntegration,x=S.includes("vscode"),E=S.includes("phpstorm"),U=Object.values(P);console.log(""),U.length>0?(console.log(C("Xdebug configured successfully")),console.log(z("Updated IDE config: ")+U.join(" ")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")))):(console.log(C("Xdebug configuration failed.")),console.log("No IDE-specific project settings directory was found in the current working directory.")),console.log(""),x&&P.vscode&&(console.log(C("VS Code / Cursor instructions:")),console.log(" 1. Ensure you have installed an IDE extension for PHP Debugging"),console.log(` (The ${C("PHP Debug")} extension by ${C("Xdebug")} has been a solid option)`),console.log(" 2. Open the Run and Debug panel on the left sidebar"),console.log(` 3. Select "${H(L)}" from the dropdown`),console.log(' 3. Click "start debugging"'),console.log(" 5. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 6. Visit Playground in your browser to hit the breakpoint"),E&&console.log("")),E&&P.phpstorm&&(console.log(C("PhpStorm instructions:")),console.log(` 1. Choose "${H(L)}" debug configuration in the toolbar`),console.log(" 2. Click the debug button (bug icon)`"),console.log(" 3. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 4. Visit Playground in your browser to hit the breakpoint")),console.log("")}catch(w){throw new Error("Could not configure Xdebug",{cause:w})}}}const ye=d.dirname(R.path),be=2*24*60*60*1e3;Ke(ee,be,ye);const re=d.join(R.path,"internal");c.mkdirSync(re);const ge=["wordpress","tools","tmp","home"];for(const f of ge){const v=P=>P.vfsPath===`/${f}`;if(!(e["mount-before-install"]?.some(v)||e.mount?.some(v))){const P=d.join(R.path,f);c.mkdirSync(P),e["mount-before-install"]===void 0&&(e["mount-before-install"]=[]),e["mount-before-install"].unshift({vfsPath:`/${f}`,hostPath:P})}}if(e["mount-before-install"])for(const f of e["mount-before-install"])m.logger.debug(`Mount before WP install: ${f.vfsPath} -> ${f.hostPath}`);if(e.mount)for(const f of e.mount)m.logger.debug(`Mount after WP install: ${f.vfsPath} -> ${f.hostPath}`);let M;e["experimental-blueprints-v2-runner"]?M=new ze(e,{siteUrl:F,cliOutput:n}):(M=new Ze(e,{siteUrl:F,cliOutput:n}),typeof e.blueprint=="string"&&(e.blueprint=await Ye({sourceString:e.blueprint,blueprintMayReadAdjacentFiles:e["blueprint-may-read-adjacent-files"]===!0})));let V=!1;const D=async function(){V||(V=!0,await Promise.all(o.map(async v=>{await s.get(v)?.dispose(),await v.worker.terminate()})),p&&await new Promise(v=>{p.close(v),p.closeAllConnections()}),await R.cleanup())};try{const f=[],v=M.getWorkerType();for(let w=0;w{V||S===0&&m.logger.error(`Worker ${w} exited with code ${S} ++`+i),process.exit(1)),console.error(i),process.exit(1)}).strictOptions().check(async i=>{if(i["skip-wordpress-install"]===!0&&(i["wordpress-install-mode"]="do-not-attempt-installing",i.wordpressInstallMode="do-not-attempt-installing"),i.wp!==void 0&&typeof i.wp=="string"&&!je(i.wp))try{new URL(i.wp)}catch{throw new Error('Unrecognized WordPress version. Please use "latest", a URL, or a numeric version such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"')}const h=i["site-url"];if(typeof h=="string"&&h.trim()!=="")try{new URL(h)}catch{throw new Error(`Invalid site-url "${h}". Please provide a valid URL (e.g., http://localhost:8080 or https://example.com)`)}if(i["auto-mount"]){let g=!1;try{g=c.statSync(i["auto-mount"]).isDirectory()}catch{g=!1}if(!g)throw new Error(`The specified --auto-mount path is not a directory: '${i["auto-mount"]}'.`)}if(i["experimental-blueprints-v2-runner"]===!0)throw new Error("Blueprints v2 are temporarily disabled while we rework their runtime implementation.");if(i.mode!==void 0)throw new Error("The --mode option requires the --experimentalBlueprintsV2Runner flag.");return!0});n.wrap(n.terminalWidth());const a=await n.argv,l=a._[0];["start","run-blueprint","server","build-snapshot","php"].includes(l)||(n.showHelp(),process.exit(1));const u=a.define||{};!("WP_DEBUG"in u)&&!("WP_DEBUG_LOG"in u)&&!("WP_DEBUG_DISPLAY"in u)&&(u.WP_DEBUG="true",u.WP_DEBUG_LOG="true",u.WP_DEBUG_DISPLAY="true");const b={...a,define:u,command:l,mount:[...a.mount||[],...a["mount-dir"]||[]],"mount-before-install":[...a["mount-before-install"]||[],...a["mount-dir-before-install"]||[]]},T=await fe(b);T===void 0&&process.exit(0);const p=(()=>{let i;return async()=>{i===void 0&&(i=T[Symbol.asyncDispose]()),await i,process.exit(0)}})();return process.on("SIGINT",p),process.on("SIGTERM",p),{[Symbol.asyncDispose]:async()=>{process.off("SIGINT",p),process.off("SIGTERM",p),await T[Symbol.asyncDispose]()},[K]:{cliServer:T}}}catch(t){if(console.error(t),!(t instanceof Error))throw t;if(process.argv.includes("--debug"))y.printDebugDetails(t);else{const o=[];let s=t;do o.push(s.message),s=s.cause;while(s instanceof Error);console.error("\x1B[1m"+o.join(" caused by: ")+"\x1B[0m")}process.exit(1)}}function le(e,t){return e.find(r=>r.vfsPath.replace(/\/$/,"")===t.replace(/\/$/,""))}const K=Symbol("playground-cli-testing"),C=e=>process.stdout.isTTY?"\x1B[1m"+e+"\x1B[0m":e,nt=e=>process.stdout.isTTY?"\x1B[31m"+e+"\x1B[0m":e,ue=e=>process.stdout.isTTY?`\x1B[2m${e}\x1B[0m`:e,H=e=>process.stdout.isTTY?`\x1B[3m${e}\x1B[0m`:e,z=e=>process.stdout.isTTY?`\x1B[33m${e}\x1B[0m`:e;async function fe(e){let t;const r=e.internalCookieStore?new y.HttpCookieStore:void 0,o=[],s=new Map;if(e.command==="start"&&(e=it(e)),e.autoMount!==void 0&&(e.autoMount===""&&(e={...e,autoMount:process.cwd()}),e=pe(e)),e.wordpressInstallMode===void 0&&(e.wordpressInstallMode="download-and-install"),e.quiet&&(e.verbosity="quiet",delete e.quiet),e.debug&&(e.verbosity="debug",delete e.debug),e.verbosity){const p=Object.values(J).find(i=>i.name===e.verbosity).severity;m.logger.setSeverityFilterLevel(p)}if(e.intl||(e.intl=!0),e.redis===void 0&&(e.redis=await ne.jspi()),e.memcached===void 0&&(e.memcached=await ne.jspi()),e.phpmyadmin){if(e.phpmyadmin===!0)e.phpmyadmin="/phpmyadmin";e.pathAliases=[{urlPrefix:e.phpmyadmin,fsPath:_.PHPMYADMIN_INSTALL_PATH}]}const n=new ot({verbosity:e.verbosity||"normal"});e.command==="server"&&(n.printBanner(),n.printConfig({phpVersion:e.php||B.RecommendedPHPVersion,wpVersion:e.wp||"latest",port:e.port??9400,xdebug:!!e.xdebug,intl:!!e.intl,redis:!!e.redis,memcached:!!e.memcached,mounts:[...e.mount||[],...e["mount-before-install"]||[]],blueprint:typeof e.blueprint=="string"?e.blueprint:void 0}));const a=e.command==="server"?e.port??9400:0,l=new y.FileLockManagerInMemory;let u=!1,b=!0;const T=await Ne({port:e.port?e.port:await He(a)?0:a,onBind:async(p,i)=>{const h="127.0.0.1",g=`http://${h}:${i}`,F=e["site-url"]||g,O=6,ee="-playground-cli-site-",R=await Je(ee);m.logger.debug(`Native temp dir for VFS root: ${R.path}`);const L="WP Playground CLI - Listen for Xdebug",te=".playground-xdebug-root",q=d.join(process.cwd(),te);if(await A.removeTempDirSymlink(q),e.xdebug){const f={hostPath:d.join(".",d.sep,te),vfsPath:"/"};if(y.SupportedPHPVersions.indexOf(e.php||B.RecommendedPHPVersion)<=y.SupportedPHPVersions.indexOf("8.5"))await A.createTempDirSymlink(R.path,q,process.platform),e.xdebug=A.makeXdebugConfig({cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],pathSkippings:["/dev/","/home/","/internal/","/request/","/proc/"]}),console.log(C("Xdebug configured successfully")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")));else if(e.experimentalUnsafeIdeIntegration){await A.createTempDirSymlink(R.path,q,process.platform);try{await A.clearXdebugIDEConfig(L,process.cwd());const w=typeof e.xdebug=="object"?e.xdebug:{},P=await A.addXdebugIDEConfig({name:L,host:h,port:i,ides:e.experimentalUnsafeIdeIntegration,cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],ideKey:w.ideKey||"WPPLAYGROUNDCLI"}),S=e.experimentalUnsafeIdeIntegration,x=S.includes("vscode"),E=S.includes("phpstorm"),U=Object.values(P);console.log(""),U.length>0?(console.log(C("Xdebug configured successfully")),console.log(z("Updated IDE config: ")+U.join(" ")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")))):(console.log(C("Xdebug configuration failed.")),console.log("No IDE-specific project settings directory was found in the current working directory.")),console.log(""),x&&P.vscode&&(console.log(C("VS Code / Cursor instructions:")),console.log(" 1. Ensure you have installed an IDE extension for PHP Debugging"),console.log(` (The ${C("PHP Debug")} extension by ${C("Xdebug")} has been a solid option)`),console.log(" 2. Open the Run and Debug panel on the left sidebar"),console.log(` 3. Select "${H(L)}" from the dropdown`),console.log(' 3. Click "start debugging"'),console.log(" 5. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 6. Visit Playground in your browser to hit the breakpoint"),E&&console.log("")),E&&P.phpstorm&&(console.log(C("PhpStorm instructions:")),console.log(` 1. Choose "${H(L)}" debug configuration in the toolbar`),console.log(" 2. Click the debug button (bug icon)`"),console.log(" 3. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 4. Visit Playground in your browser to hit the breakpoint")),console.log("")}catch(w){throw new Error("Could not configure Xdebug",{cause:w})}}}const ye=d.dirname(R.path),be=2*24*60*60*1e3;Ke(ee,be,ye);const re=d.join(R.path,"internal");c.mkdirSync(re);const ge=["wordpress","tools","tmp","home"];for(const f of ge){const v=P=>P.vfsPath===`/${f}`;if(!(e["mount-before-install"]?.some(v)||e.mount?.some(v))){const P=d.join(R.path,f);c.mkdirSync(P),e["mount-before-install"]===void 0&&(e["mount-before-install"]=[]),e["mount-before-install"].unshift({vfsPath:`/${f}`,hostPath:P})}}if(e["mount-before-install"])for(const f of e["mount-before-install"])m.logger.debug(`Mount before WP install: ${f.vfsPath} -> ${f.hostPath}`);if(e.mount)for(const f of e.mount)m.logger.debug(`Mount after WP install: ${f.vfsPath} -> ${f.hostPath}`);let M;e["experimental-blueprints-v2-runner"]?M=new ze(e,{siteUrl:F,cliOutput:n}):(M=new Ze(e,{siteUrl:F,cliOutput:n}),typeof e.blueprint=="string"&&(e.blueprint=await Ye({sourceString:e.blueprint,blueprintMayReadAdjacentFiles:e["blueprint-may-read-adjacent-files"]===!0})));let V=!1;const D=async function(){V||(V=!0,await Promise.all(o.map(async v=>{await s.get(v)?.dispose(),await v.worker.terminate()})),p&&await new Promise(v=>{p.close(v),p.closeAllConnections()}),await R.cleanup())};try{const f=[],v=M.getWorkerType();for(let w=0;w{V||S===0&&m.logger.error(`Worker ${w} exited with code ${S} + `)}}).then(async S=>{o.push(S);const x=await at(l),E=await M.bootRequestHandler({worker:S,fileLockManagerPort:x,nativeInternalDirPath:re});return s.set(S,E),[S,E]});f.push(P),w===0&&await P}await Promise.all(f),t=y.createObjectPoolProxy(o.map(w=>s.get(w)));{const w=new N.MessageChannel,P=w.port1,S=w.port2;if(await y.exposeAPI({applyPostInstallMountsToAllWorkers:async()=>{await Promise.all(Array.from(s.values()).map(x=>x.mountAfterWordPressInstall(e.mount||[])))}},void 0,P),await M.bootWordPress(t,S),P.close(),u=!0,!e["experimental-blueprints-v2-runner"]){const x=await M.compileInputBlueprint(e["additional-blueprint-steps"]||[]);x&&await I.runBlueprintV1Steps(x,t)}if(e.phpmyadmin&&!await t.fileExists(`${_.PHPMYADMIN_INSTALL_PATH}/index.php`)){const x=await _.getPhpMyAdminInstallSteps(),E=await I.compileBlueprintV1({steps:x});await I.runBlueprintV1Steps(E,t)}if(e.command==="build-snapshot"){await ut(t,e.outfile),n.printStatus(`Exported to ${e.outfile}`),await D();return}else if(e.command==="run-blueprint"){n.finishProgress("Done"),await D();return}else if(e.command==="php"){const x=["/internal/shared/bin/php",...(e._||[]).slice(1)],E=await t.cli(x),[U]=await Promise.all([E.exitCode,E.stdout.pipeTo(new WritableStream({write(j){process.stdout.write(j)}})),E.stderr.pipeTo(new WritableStream({write(j){process.stderr.write(j)}}))]);await D(),process.exit(U)}}if(n.finishProgress(),n.printReady(g,O),e.phpmyadmin){const w=d.join(e.phpmyadmin,_.PHPMYADMIN_ENTRY_PATH);n.printPhpMyAdminUrl(new URL(w,g).toString())}return e.xdebug&&e.experimentalDevtools&&(await Ie.startBridge({phpInstance:t,phpRoot:"/wordpress"})).start(),{playground:t,server:p,serverUrl:g,[Symbol.asyncDispose]:D,[K]:{workerThreadCount:O}}}catch(f){if(e.verbosity!=="debug")throw f;let v="";throw await t?.fileExists(m.errorLogPath)&&(v=await t.readFileAsText(m.errorLogPath)),await D(),new Error(v,{cause:f})}},async handleRequest(p){if(!u)return y.StreamedPHPResponse.forHttpCode(502,"WordPress is not ready yet");if(b){b=!1;const h={"Content-Type":["text/plain"],"Content-Length":["0"],Location:[p.url]};return p.headers?.cookie?.includes("playground_auto_login_already_happened")&&(h["Set-Cookie"]=["playground_auto_login_already_happened=1; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/"]),y.StreamedPHPResponse.fromPHPResponse(new y.PHPResponse(302,h,new Uint8Array))}r&&(p={...p,headers:{...p.headers,cookie:r.getCookieRequestHeader()}});const i=await t.requestStreamed(p);if(r){const h=await i.headers;r.rememberCookiesFromResponseHeaders(h),delete h["set-cookie"]}return i}}).catch(p=>{n.printError(p.message),process.exit(1)});return T&&e.command==="start"&&!e.skipBrowser&<(T.serverUrl),T}function it(e){let t={...e,command:"server"};e.noAutoMount||(t.autoMount=d.resolve(process.cwd(),t.path??""),t=pe(t),delete t.autoMount);const r=le(t["mount-before-install"]||[],"/wordpress")||le(t.mount||[],"/wordpress");if(r)console.log("Site files stored at:",r?.hostPath),e.reset&&(console.log(""),console.log(nt("This site is not managed by Playground CLI and cannot be reset.")),console.log("(It's not stored in the ~/.wordpress-playground/sites/ directory.)"),console.log(""),console.log("You may still remove the site's directory manually if you wish."),process.exit(1));else{const o=t.autoMount||process.cwd(),s=ke.createHash("sha256").update(o).digest("hex"),n=X.homedir(),a=d.join(n,".wordpress-playground/sites",s);console.log("Site files stored at:",a),c.existsSync(a)&&e.reset&&(console.log("Resetting site..."),c.rmdirSync(a,{recursive:!0})),c.mkdirSync(a,{recursive:!0}),t["mount-before-install"]=[...t["mount-before-install"]||[],{vfsPath:"/wordpress",hostPath:a}],t.wordpressInstallMode=c.readdirSync(a).length===0?"download-and-install":"install-from-existing-files-if-needed"}return t}const G=new y.ProcessIdAllocator;function we(e,{onExit:t}={}){let r;return e==="v1"?r=new N.Worker(new URL("./worker-thread-v1.cjs",typeof document>"u"?require("url").pathToFileURL(__filename).href:$&&$.tagName.toUpperCase()==="SCRIPT"&&$.src||new URL("run-cli-C-eCY5Ux.cjs",document.baseURI).href)):r=new N.Worker(new URL("./worker-thread-v2.cjs",typeof document>"u"?require("url").pathToFileURL(__filename).href:$&&$.tagName.toUpperCase()==="SCRIPT"&&$.src||new URL("run-cli-C-eCY5Ux.cjs",document.baseURI).href)),new Promise((o,s)=>{const n=G.claim();r.once("message",function(l){l.command==="worker-script-initialized"&&o({processId:n,worker:r,phpPort:l.phpPort})}),r.once("error",function(l){G.release(n),console.error(l);const u=new Error(`Worker failed to load worker. ${l.message?`Original error: ${l.message}`:""}`);s(u)});let a=!1;r.once("spawn",()=>{a=!0}),r.once("exit",l=>{G.release(n),a||s(new Error(`Worker exited before spawning: ${l}`)),t?.(l)})})}async function at(e){const{port1:t,port2:r}=new N.MessageChannel;return await y.exposeSyncAPI(e,t),r}function lt(e){const t=X.platform();let r;switch(t){case"darwin":r=`open "${e}"`;break;case"win32":r=`start "" "${e}"`;break;default:r=`xdg-open "${e}"`;break}de.exec(r,o=>{o&&m.logger.debug(`Could not open browser: ${o.message}`)})}async function ut(e,t){await e.run({code:`open('/tmp/build.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE)) { diff --git a/apps/cli/patches/@wp-playground+tools+3.1.12.patch b/apps/cli/patches/@wp-playground+tools+3.1.12.patch new file mode 100644 index 0000000000..edcd04069d --- /dev/null +++ b/apps/cli/patches/@wp-playground+tools+3.1.12.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js b/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js +index f17cbe9..a8ac6d7 100644 +--- a/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js ++++ b/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js +@@ -23,7 +23,7 @@ use WP_SQLite_Connection; + use WP_SQLite_Driver; + + // Load the SQLite driver. +-require_once '/internal/shared/sqlite-database-integration/wp-pdo-mysql-on-sqlite.php'; ++require_once '/wordpress/wp-content/mu-plugins/sqlite-database-integration/wp-pdo-mysql-on-sqlite.php'; + + // Supress the following phpMyAdmin warning: + // "The mysqlnd extension is missing. Please check your PHP configuration." diff --git a/apps/cli/wordpress-server-child.ts b/apps/cli/wordpress-server-child.ts index fda5a5821c..26d1cf5200 100644 --- a/apps/cli/wordpress-server-child.ts +++ b/apps/cli/wordpress-server-child.ts @@ -27,9 +27,10 @@ import { InMemoryFilesystem, } from '@wp-playground/storage'; import { WordPressInstallMode } from '@wp-playground/wordpress'; +import fs from 'fs-extra'; import { z } from 'zod'; import { sanitizeRunCLIArgs } from 'cli/lib/cli-args-sanitizer'; -import { getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files'; +import { getPhpMyAdminPath, getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files'; import { isSqliteIntegrationInstalled } from 'cli/lib/sqlite-integration'; import { ServerConfig, @@ -260,6 +261,19 @@ async function getBaseRunCLIArgs( args.xdebug = true; } + if ( config.enablePhpMyAdmin ) { + const phpMyAdminHostPath = getPhpMyAdminPath(); + if ( await fs.pathExists( phpMyAdminHostPath ) ) { + mounts.push( { + hostPath: phpMyAdminHostPath, + vfsPath: '/tools/phpmyadmin', + } ); + logToConsole( 'Mounting bundled phpMyAdmin' ); + } + logToConsole( 'Enabling phpMyAdmin support' ); + args.phpmyadmin = true; + } + return args; } diff --git a/apps/studio/src/components/content-tab-overview.tsx b/apps/studio/src/components/content-tab-overview.tsx index 6055a8931b..0133bcc9b4 100644 --- a/apps/studio/src/components/content-tab-overview.tsx +++ b/apps/studio/src/components/content-tab-overview.tsx @@ -4,6 +4,7 @@ import { archive, code, desktop, + grid, pencil, layout, navigation, @@ -134,6 +135,8 @@ function CustomizeSection( { function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'selectedSite' > ) { const { data: editor } = useGetUserEditorQuery(); const { data: terminal } = useGetUserTerminalQuery(); + const { startServer, loadingServer } = useSiteDetails(); + const isServerLoading = loadingServer[ selectedSite.id ]; const buttonsArray: ButtonsSectionProps[ 'buttonsArray' ] = [ { @@ -176,6 +179,23 @@ function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'sel } }, } ); + + buttonsArray.push( { + label: __( 'phpMyAdmin' ), + className: 'text-nowrap', + icon: grid, + disabled: ! selectedSite.enablePhpMyAdmin || isServerLoading, + onClick: async () => { + if ( ! selectedSite.running ) { + await startServer( selectedSite ); + } + getIpcApi().openSiteURL( + selectedSite.id, + '/phpmyadmin/index.php?route=/database/structure&db=wordpress' + ); + }, + } ); + return ; } diff --git a/apps/studio/src/components/content-tab-settings.tsx b/apps/studio/src/components/content-tab-settings.tsx index 87b6ee6532..2f97ac3b82 100644 --- a/apps/studio/src/components/content-tab-settings.tsx +++ b/apps/studio/src/components/content-tab-settings.tsx @@ -182,6 +182,9 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) { selectedSite.enableDebugDisplay ? __( 'Enabled' ) : __( 'Disabled' ) } + + { selectedSite.enablePhpMyAdmin ? __( 'Enabled' ) : __( 'Disabled' ) } + diff --git a/apps/studio/src/components/tests/content-tab-settings.test.tsx b/apps/studio/src/components/tests/content-tab-settings.test.tsx index fbd3e3bee1..6134e6f2e8 100644 --- a/apps/studio/src/components/tests/content-tab-settings.test.tsx +++ b/apps/studio/src/components/tests/content-tab-settings.test.tsx @@ -158,8 +158,8 @@ describe( 'ContentTabSettings', () => { ).toHaveTextContent( 'localhost:8881' ); expect( screen.getByText( 'HTTPS' ) ).toBeVisible(); expect( screen.getByText( 'Xdebug' ) ).toBeVisible(); - // HTTPS, Xdebug, Debug log, and Debug display show "Disabled" - expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 4 ); + // HTTPS, Xdebug, Debug log, Debug display, and phpMyAdmin show "Disabled" + expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 5 ); expect( screen.getByRole( 'button', { name: 'Copy local path to clipboard' } ) ).toBeVisible(); expect( screen.getByText( '7.7.7' ) ).toBeVisible(); expect( diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 1b34ed5e9b..8d409f3014 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -491,6 +491,10 @@ export async function updateSite( options.debugDisplay = updatedSite.enableDebugDisplay ?? false; } + if ( updatedSite.enablePhpMyAdmin !== currentSite.enablePhpMyAdmin ) { + options.phpmyadmin = updatedSite.enablePhpMyAdmin ?? false; + } + const hasCliChanges = Object.keys( options ).length > 2; if ( hasCliChanges ) { diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts index e07e54d5e5..129bf45d64 100644 --- a/apps/studio/src/ipc-types.d.ts +++ b/apps/studio/src/ipc-types.d.ts @@ -35,6 +35,7 @@ interface StoppedSiteDetails { enableXdebug?: boolean; enableDebugLog?: boolean; enableDebugDisplay?: boolean; + enablePhpMyAdmin?: boolean; sortOrder?: number; } diff --git a/apps/studio/src/lib/server-files-paths.ts b/apps/studio/src/lib/server-files-paths.ts index b394931b7d..5e5d226fff 100644 --- a/apps/studio/src/lib/server-files-paths.ts +++ b/apps/studio/src/lib/server-files-paths.ts @@ -78,3 +78,10 @@ export function getLanguagePacksPath(): string { export function getAgentSkillsPath(): string { return path.join( getBasePath(), 'agent-skills' ); } + +/** + * The path where bundled phpMyAdmin files are stored. + */ +export function getPhpMyAdminPath(): string { + return path.join( getBasePath(), 'phpmyadmin' ); +} diff --git a/apps/studio/src/modules/cli/lib/cli-site-editor.ts b/apps/studio/src/modules/cli/lib/cli-site-editor.ts index 1b978ed654..b02ef9e15a 100644 --- a/apps/studio/src/modules/cli/lib/cli-site-editor.ts +++ b/apps/studio/src/modules/cli/lib/cli-site-editor.ts @@ -22,6 +22,7 @@ export interface EditSiteOptions { adminEmail?: string; debugLog?: boolean; debugDisplay?: boolean; + phpmyadmin?: boolean; } export async function editSiteViaCli( options: EditSiteOptions ): Promise< void > { @@ -104,5 +105,9 @@ function buildCliArgs( options: EditSiteOptions ): string[] { args.push( options.debugDisplay ? '--debug-display' : '--no-debug-display' ); } + if ( options.phpmyadmin !== undefined ) { + args.push( options.phpmyadmin ? '--phpmyadmin' : '--no-phpmyadmin' ); + } + return args; } diff --git a/apps/studio/src/modules/site-settings/edit-site-details.tsx b/apps/studio/src/modules/site-settings/edit-site-details.tsx index e65e2b403c..5b0289013c 100644 --- a/apps/studio/src/modules/site-settings/edit-site-details.tsx +++ b/apps/studio/src/modules/site-settings/edit-site-details.tsx @@ -1,4 +1,8 @@ -import { DEFAULT_PHP_VERSION, DEFAULT_WORDPRESS_VERSION } from '@studio/common/constants'; +import { + DEFAULT_ENABLE_PHPMYADMIN, + DEFAULT_PHP_VERSION, + DEFAULT_WORDPRESS_VERSION, +} from '@studio/common/constants'; import { generateCustomDomainFromSiteName, getDomainNameValidationError, @@ -45,6 +49,9 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const [ enableDebugDisplay, setEnableDebugDisplay ] = useState( selectedSite?.enableDebugDisplay ?? false ); + const [ enablePhpMyAdmin, setEnablePhpMyAdmin ] = useState( + selectedSite?.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN + ); const [ xdebugEnabledSite, setXdebugEnabledSite ] = useState< SiteDetails | null >( null ); const [ adminUsername, setAdminUsername ] = useState( selectedSite?.adminUsername ?? 'admin' ); const [ adminPassword, setAdminPassword ] = useState( @@ -146,7 +153,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = ( decodePassword( selectedSite.adminPassword ?? '' ) || 'password' ) === adminPassword && ( selectedSite.adminEmail || 'admin@localhost.com' ) === adminEmail && !! selectedSite.enableDebugLog === enableDebugLog && - !! selectedSite.enableDebugDisplay === enableDebugDisplay; + !! selectedSite.enableDebugDisplay === enableDebugDisplay && + ( selectedSite.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN ) === enablePhpMyAdmin; const hasValidationErrors = ! selectedSite || ! siteName.trim() || @@ -173,6 +181,7 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = setAdminEmail( selectedSite.adminEmail || 'admin@localhost.com' ); setEnableDebugLog( selectedSite.enableDebugLog ?? false ); setEnableDebugDisplay( selectedSite.enableDebugDisplay ?? false ); + setEnablePhpMyAdmin( selectedSite.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN ); }, [ selectedSite, getEffectiveWpVersion ] ); const onSiteEdit = async ( event: FormEvent ) => { @@ -189,6 +198,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const hasDebugLogChanged = enableDebugLog !== ( selectedSite.enableDebugLog ?? false ); const hasDebugDisplayChanged = enableDebugDisplay !== ( selectedSite.enableDebugDisplay ?? false ); + const hasPhpMyAdminChanged = + enablePhpMyAdmin !== ( selectedSite.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN ); const hasDomainChanged = Boolean( selectedSite.customDomain ) !== useCustomDomain || ( useCustomDomain && customDomain !== selectedSite.customDomain ); @@ -210,6 +221,7 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = credentialsChanged: hasCredentialsChanged, debugLogChanged: hasDebugLogChanged, debugDisplayChanged: hasDebugDisplayChanged, + phpmyadminChanged: hasPhpMyAdminChanged, } ); setNeedsRestart( needsRestart ); @@ -235,6 +247,7 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = adminEmail, enableDebugLog, enableDebugDisplay, + enablePhpMyAdmin, }, hasWpVersionChanged ? selectedWpVersion : undefined ); @@ -589,6 +602,32 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = ) } + +
+
+ setEnablePhpMyAdmin( e.target.checked ) } + disabled={ isEditingSite } + /> + +
+
+ { __( 'Access phpMyAdmin to browse and manage your site database.' ) } +
+
) } diff --git a/apps/studio/src/setup-wp-server-files.ts b/apps/studio/src/setup-wp-server-files.ts index 37fead5be9..8f30c51446 100644 --- a/apps/studio/src/setup-wp-server-files.ts +++ b/apps/studio/src/setup-wp-server-files.ts @@ -6,6 +6,7 @@ import { updateLatestWPCliVersion } from 'src/lib/download-utils'; import { getAgentSkillsPath, getLanguagePacksPath, + getPhpMyAdminPath, getWordPressVersionPath, getSqlitePath, getWpCliPath, @@ -122,6 +123,15 @@ async function copyBundledAgentSkills() { await recursiveCopyDirectory( bundledAgentSkillsPath, getAgentSkillsPath() ); } +async function copyBundledPhpMyAdmin() { + const bundledPath = path.join( getResourcesPath(), 'wp-files', 'phpmyadmin' ); + if ( ! ( await fs.pathExists( bundledPath ) ) ) { + return; + } + // Always copy to ensure files are complete and up-to-date + await recursiveCopyDirectory( bundledPath, getPhpMyAdminPath() ); +} + async function copyBundledLanguagePacks() { const bundledLanguagePacksPath = path.join( getResourcesPath(), @@ -146,6 +156,7 @@ export async function setupWPServerFiles() { [ 'translations', copyBundledTranslations ], [ 'language packs', copyBundledLanguagePacks ], [ 'agent skills', copyBundledAgentSkills ], + [ 'phpMyAdmin', copyBundledPhpMyAdmin ], ]; for ( const [ name, step ] of steps ) { diff --git a/apps/studio/src/storage/user-data.ts b/apps/studio/src/storage/user-data.ts index 6ca0ca6985..cfa849c1df 100644 --- a/apps/studio/src/storage/user-data.ts +++ b/apps/studio/src/storage/user-data.ts @@ -201,6 +201,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { enableXdebug, enableDebugLog, enableDebugDisplay, + enablePhpMyAdmin, sortOrder, } ) => { // No object spreading allowed. TypeScript's structural typing is too permissive and @@ -223,6 +224,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { enableXdebug, enableDebugLog, enableDebugDisplay, + enablePhpMyAdmin, sortOrder, themeDetails: { name: themeDetails?.name || '', diff --git a/scripts/download-wp-server-files.ts b/scripts/download-wp-server-files.ts index 48973a317c..7b33b7657a 100644 --- a/scripts/download-wp-server-files.ts +++ b/scripts/download-wp-server-files.ts @@ -4,6 +4,11 @@ import fs from 'fs-extra'; import { extractZip } from '../tools/common/lib/extract-zip'; import { getLatestSQLiteCommandRelease } from '../apps/studio/src/lib/sqlite-command-release'; import { SQLITE_DATABASE_INTEGRATION_RELEASE_URL } from '../apps/studio/src/constants'; +import { + PHPMYADMIN_DOWNLOAD_URL, + PHPMYADMIN_VERSION, + getPhpMyAdminInstallSteps, +} from '@wp-playground/tools'; const WP_SERVER_FILES_PATH = path.join( __dirname, '..', 'wp-files' ); @@ -42,6 +47,12 @@ const FILES_TO_DOWNLOAD: FileToDownload[] = [ }, destinationPath: path.join( WP_SERVER_FILES_PATH, 'sqlite-command' ), }, + { + name: 'phpmyadmin', + description: 'phpMyAdmin', + getUrl: () => PHPMYADMIN_DOWNLOAD_URL, + destinationPath: path.join( WP_SERVER_FILES_PATH, 'phpmyadmin' ), + }, ]; async function downloadFile( file: FileToDownload ): Promise< void > { @@ -90,6 +101,39 @@ async function downloadFile( file: FileToDownload ): Promise< void > { } fs.renameSync( sourcePath, targetPath ); } + } else if ( name === 'phpmyadmin' ) { + /** + * phpMyAdmin is extracted into a folder like phpMyAdmin-5.2.3-english. + * We extract to a temp dir, rename to the destination, then inject the + * Playground-specific config and SQLite adapter from @wp-playground/tools. + */ + console.log( `[${ name }] Extracting files from zip ...` ); + const tmpExtractPath = path.join( os.tmpdir(), 'phpmyadmin-extract' ); + if ( fs.existsSync( tmpExtractPath ) ) { + fs.rmSync( tmpExtractPath, { recursive: true, force: true } ); + } + await extractZip( zipPath, tmpExtractPath ); + + const innerFolder = `phpMyAdmin-${ PHPMYADMIN_VERSION }-english`; + const sourcePath = path.join( tmpExtractPath, innerFolder ); + if ( fs.existsSync( extractedPath ) ) { + fs.rmSync( extractedPath, { recursive: true, force: true } ); + } + fs.renameSync( sourcePath, extractedPath ); + fs.rmSync( tmpExtractPath, { recursive: true, force: true } ); + + // Inject Playground-specific config and SQLite adapter + console.log( `[${ name }] Injecting Playground-specific files ...` ); + const installSteps = await getPhpMyAdminInstallSteps(); + for ( const step of installSteps ) { + if ( step.step === 'writeFile' && typeof step.data === 'string' ) { + // step.path is like /tools/phpmyadmin/config.inc.php — strip the /tools/phpmyadmin prefix + const relativePath = step.path.replace( '/tools/phpmyadmin/', '' ); + const destFile = path.join( extractedPath, relativePath ); + await fs.ensureDir( path.dirname( destFile ) ); + await fs.writeFile( destFile, step.data ); + } + } } else { console.log( `[${ name }] Extracting files from zip ...` ); await extractZip( zipPath, extractedPath ); diff --git a/tools/common/constants.ts b/tools/common/constants.ts index 515e708740..fa3693fc1f 100644 --- a/tools/common/constants.ts +++ b/tools/common/constants.ts @@ -32,3 +32,6 @@ export const MINIMUM_WORDPRESS_VERSION = '6.2.1' as const; // https://wordpress. export const DEFAULT_WORDPRESS_VERSION = 'latest' as const; export const DEFAULT_PHP_VERSION: typeof RecommendedPHPVersion = RecommendedPHPVersion; export const SQLITE_FILENAME = 'sqlite-database-integration' as const; + +// phpMyAdmin - set to true to enable by default for all new sites +export const DEFAULT_ENABLE_PHPMYADMIN = false; diff --git a/tools/common/lib/site-events.ts b/tools/common/lib/site-events.ts index ec8f25ca02..390d484bd6 100644 --- a/tools/common/lib/site-events.ts +++ b/tools/common/lib/site-events.ts @@ -26,6 +26,7 @@ export const siteDetailsSchema = z.object( { enableXdebug: z.boolean().optional(), enableDebugLog: z.boolean().optional(), enableDebugDisplay: z.boolean().optional(), + enablePhpMyAdmin: z.boolean().optional(), } ); export type SiteDetails = z.infer< typeof siteDetailsSchema >; diff --git a/tools/common/lib/site-needs-restart.ts b/tools/common/lib/site-needs-restart.ts index 870ae81a7d..99219b54cc 100644 --- a/tools/common/lib/site-needs-restart.ts +++ b/tools/common/lib/site-needs-restart.ts @@ -7,6 +7,7 @@ export interface SiteSettingChanges { credentialsChanged?: boolean; debugLogChanged?: boolean; debugDisplayChanged?: boolean; + phpmyadminChanged?: boolean; } export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { @@ -19,6 +20,7 @@ export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { credentialsChanged, debugLogChanged, debugDisplayChanged, + phpmyadminChanged, } = changes; return !! ( @@ -29,6 +31,7 @@ export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { xdebugChanged || credentialsChanged || debugLogChanged || - debugDisplayChanged + debugDisplayChanged || + phpmyadminChanged ); }