Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
28407da
Add phpMyAdmin data model and CLI server support
wojtekn Mar 17, 2026
5464e71
Add phpMyAdmin support to site set command and desktop app bridge
wojtekn Mar 17, 2026
82a027a
Add phpMyAdmin toggle in Debugging tab and button on Overview tab
wojtekn Mar 17, 2026
b3c5a01
Move phpMyAdmin button into Open in… grid; always show, disable when off
wojtekn Mar 17, 2026
17e8534
Fix phpMyAdmin checkbox layout to match other debug options
wojtekn Mar 17, 2026
51ef389
Add phpMyAdmin row to Debugging section in Settings tab
wojtekn Mar 17, 2026
2b0193e
Patch @wp-playground/cli to allow --phpmyadmin with --skip-sqlite-setup
wojtekn Mar 17, 2026
78f37ad
Fix phpMyAdmin not preserved during site restart via blueprint path
wojtekn Mar 17, 2026
5e7c6ab
Fix enablePhpMyAdmin not persisted to disk in Studio appdata
wojtekn Mar 17, 2026
6fb6b87
Update set.test.ts expected error message to include --phpmyadmin
wojtekn Mar 17, 2026
5aa9b35
Update content-tab-settings test to expect 5 Disabled rows
wojtekn Mar 17, 2026
5587906
Patch @wp-playground/tools DbiMysqli to use Studio's SQLite path
wojtekn Mar 17, 2026
f6e42bb
Bundle phpMyAdmin at build time for offline support
wojtekn Mar 18, 2026
c6fce03
Enable phpMyAdmin by default for new sites
wojtekn Mar 18, 2026
9684620
Remove temporary offline mode debug logging
wojtekn Mar 18, 2026
425fae2
Fix phpMyAdmin SQLite path in @wp-playground/tools CJS build
wojtekn Mar 18, 2026
64af33b
Fix wp-files path resolution in dev mode
wojtekn Mar 18, 2026
11cd965
Merge branch 'trunk' into add-phpmyadmin-support
wojtekn Mar 18, 2026
c5654b8
Apply DEFAULT_ENABLE_PHPMYADMIN fallback to existing sites
wojtekn Mar 18, 2026
8f3bde6
Fix content-tab-settings test: set enablePhpMyAdmin: false in fixture
wojtekn Mar 18, 2026
b836d9b
Add e2e tests for phpMyAdmin button visibility and accessibility
wojtekn Mar 18, 2026
bb12204
Fix e2e phpMyAdmin tests: share session lifecycle across all describe…
wojtekn Mar 18, 2026
c6d8591
Merge branch 'trunk' into add-phpmyadmin-support
wojtekn Mar 19, 2026
94bfcee
Fix path missed during solving conflict
wojtekn Mar 19, 2026
d9130d7
Remove phpMyAdmin controls making it always enabled for all sites
wojtekn Mar 19, 2026
c984719
More cleanup
wojtekn Mar 19, 2026
f35f597
Clean unnecessary change from CLI
wojtekn Mar 19, 2026
93cd9d6
Update way of patching phpMyAdmin patch sqlite location
wojtekn Mar 20, 2026
dd6a99e
Merge branch 'trunk' into add-phpmyadmin-support
wojtekn Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/cli/lib/server-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ export function getLanguagePacksPath(): string {
export function getAiInstructionsPath(): string {
return path.join( getServerFilesPath(), 'skills' );
}

export function getPhpMyAdminPath(): string {
return path.join( getServerFilesPath(), 'phpmyadmin' );
}
29 changes: 29 additions & 0 deletions apps/cli/patches/@wp-playground+cli+3.1.12.patch

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions apps/cli/patches/@wp-playground+tools+3.1.12.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
diff --git a/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js b/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js
index f17cbe9..7319b2c 100644
--- a/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js
+++ b/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js
@@ -22,8 +22,18 @@ use Throwable;
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';
+// Load the SQLite driver from the first available location.
+// The path varies depending on the environment (Playground, Studio, etc.).
+$sqlite_driver_locations = array(
+ '/internal/shared/sqlite-database-integration/wp-pdo-mysql-on-sqlite.php',
+ defined( 'WP_SQLITE_LOCATION' ) ? WP_SQLITE_LOCATION . '/wp-pdo-mysql-on-sqlite.php' : false,
+);
+foreach ($sqlite_driver_locations as $sqlite_driver_path) {
+ if ($sqlite_driver_path && file_exists($sqlite_driver_path)) {
+ require_once $sqlite_driver_path;
+ break;
+ }
+}

// Supress the following phpMyAdmin warning:
// "The mysqlnd extension is missing. Please check your PHP configuration."
diff --git a/node_modules/@wp-playground/tools/DbiMysqli-IfZRkV2V.cjs b/node_modules/@wp-playground/tools/DbiMysqli-IfZRkV2V.cjs
index 02572b2..c958d5d 100644
--- a/node_modules/@wp-playground/tools/DbiMysqli-IfZRkV2V.cjs
+++ b/node_modules/@wp-playground/tools/DbiMysqli-IfZRkV2V.cjs
@@ -22,8 +22,18 @@ use Throwable;
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';
+// Load the SQLite driver from the first available location.
+// The path varies depending on the environment (Playground, Studio, etc.).
+$sqlite_driver_locations = array(
+ '/internal/shared/sqlite-database-integration/wp-pdo-mysql-on-sqlite.php',
+ defined( 'WP_SQLITE_LOCATION' ) ? WP_SQLITE_LOCATION . '/wp-pdo-mysql-on-sqlite.php' : false,
+);
+foreach ($sqlite_driver_locations as $sqlite_driver_path) {
+ if ($sqlite_driver_path && file_exists($sqlite_driver_path)) {
+ require_once $sqlite_driver_path;
+ break;
+ }
+}

// Supress the following phpMyAdmin warning:
// "The mysqlnd extension is missing. Please check your PHP configuration."
18 changes: 16 additions & 2 deletions apps/cli/wordpress-server-child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -179,8 +180,9 @@ async function getBaseRunCLIArgs(
const enableDebugLog = config.enableDebugLog ?? false;
const enableDebugDisplay = config.enableDebugDisplay ?? false;

const defaultConstants: Record< string, boolean > = {
const defaultConstants: Record< string, boolean | string > = {
WP_SQLITE_AST_DRIVER: true,
WP_SQLITE_LOCATION: '/wordpress/wp-content/mu-plugins/sqlite-database-integration',
WP_DEBUG: enableDebugLog || enableDebugDisplay,
WP_DEBUG_LOG: enableDebugLog,
WP_DEBUG_DISPLAY: enableDebugDisplay,
Expand Down Expand Up @@ -259,6 +261,18 @@ async function getBaseRunCLIArgs(
args.xdebug = true;
}

const phpMyAdminHostPath = getPhpMyAdminPath();
if ( await fs.pathExists( phpMyAdminHostPath ) ) {
mounts.push( {
hostPath: phpMyAdminHostPath,
vfsPath: '/tools/phpmyadmin',
} );
logToConsole( 'Mounting bundled phpMyAdmin' );
} else {
logToConsole( 'Bundled phpMyAdmin not found, falling back to Playground download' );
}
args.phpmyadmin = true;

lastCliArgs = sanitizeRunCLIArgs( args );
return args;
}
Expand Down
54 changes: 36 additions & 18 deletions apps/studio/e2e/overview-customize-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,29 +69,30 @@ test.describe( 'Overview customize links', () => {
}
return decoded;
};
test.describe( 'Block theme customize shortcut links', () => {
test.beforeAll( async () => {
await session.launch();

const onboarding = new Onboarding( session.mainWindow );
await onboarding.completeOnboarding( { customSiteName: siteName } );
await onboarding.closeWhatsNew();
test.beforeAll( async () => {
await session.launch();

const siteContent = new SiteContent( session.mainWindow, siteName );
await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } );
} );
const onboarding = new Onboarding( session.mainWindow );
await onboarding.completeOnboarding( { customSiteName: siteName } );
await onboarding.closeWhatsNew();

test.afterEach( async ( { page: _page }, testInfo ) => {
await session.reportMainProcessLogsOnFailure( testInfo );
} );
const siteContent = new SiteContent( session.mainWindow, siteName );
await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } );
} );

test.afterAll( async () => {
if ( session.electronApp ) {
await restoreOpenExternal();
}
await session.cleanup();
} );
test.afterEach( async ( { page: _page }, testInfo ) => {
await session.reportMainProcessLogsOnFailure( testInfo );
} );

test.afterAll( async () => {
if ( session.electronApp ) {
await restoreOpenExternal();
}
await session.cleanup();
} );

test.describe( 'Block theme customize shortcut links', () => {
test( 'shows overview shortcuts for a new site', async () => {
const siteContent = new SiteContent( session.mainWindow, siteName );
await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } );
Expand Down Expand Up @@ -185,4 +186,21 @@ test.describe( 'Overview customize links', () => {
await expect( headingLocator ).toBeVisible( { timeout: 120_000 } );
} );
} );

test.describe( 'phpMyAdmin shortcut link', () => {
test( 'phpMyAdmin button is visible and enabled for a new site', async () => {
const siteContent = new SiteContent( session.mainWindow, siteName );
const phpMyAdminButton = siteContent.locator.getByRole( 'button', { name: 'phpMyAdmin' } );
await expect( phpMyAdminButton ).toBeVisible( { timeout: 120_000 } );
await expect( phpMyAdminButton ).toBeEnabled();
} );

test( 'opens phpMyAdmin shortcut', async ( { page } ) => {
const redirectUrl = await openShortcut( page, 'phpMyAdmin' );
expect( redirectUrl ).toContain( '/phpmyadmin/' );

// phpMyAdmin renders a table of database tables
await expect( page.locator( '#pma_navigation' ) ).toBeVisible( { timeout: 120_000 } );
} );
} );
} );
20 changes: 20 additions & 0 deletions apps/studio/src/components/content-tab-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
archive,
code,
desktop,
grid,
pencil,
layout,
navigation,
Expand Down Expand Up @@ -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' ] = [
{
Expand Down Expand Up @@ -176,6 +179,23 @@ function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'sel
}
},
} );

buttonsArray.push( {
label: __( 'phpMyAdmin' ),
className: 'text-nowrap',
icon: grid,
disabled: isServerLoading,
onClick: async () => {
if ( ! selectedSite.running ) {
await startServer( selectedSite );
}
getIpcApi().openSiteURL(
selectedSite.id,
'/phpmyadmin/index.php?route=/database/structure&db=wordpress'
);
},
} );

return <ButtonsSection buttonsArray={ buttonsArray } title={ __( 'Open in…' ) } />;
}

Expand Down
1 change: 0 additions & 1 deletion apps/studio/src/components/content-tab-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
<SettingsRow label={ __( 'Debug display' ) }>
<span>{ selectedSite.enableDebugDisplay ? __( 'Enabled' ) : __( 'Disabled' ) }</span>
</SettingsRow>

<tr>
<th colSpan={ 2 } className="pb-4 ltr:text-left rtl:text-right">
<h3 className="text-frame-text text-sm font-semibold mt-4">{ __( 'WP Admin' ) }</h3>
Expand Down
7 changes: 7 additions & 0 deletions apps/studio/src/lib/server-files-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,10 @@ export function getLanguagePacksPath(): string {
export function getAiInstructionsPath(): string {
return path.join( getBasePath(), 'skills' );
}

/**
* The path where bundled phpMyAdmin files are stored.
*/
export function getPhpMyAdminPath(): string {
return path.join( getBasePath(), 'phpmyadmin' );
}
11 changes: 11 additions & 0 deletions apps/studio/src/setup-wp-server-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { updateLatestWPCliVersion } from 'src/lib/download-utils';
import {
getAiInstructionsPath,
getLanguagePacksPath,
getPhpMyAdminPath,
getWordPressVersionPath,
getSqlitePath,
getWpCliPath,
Expand Down Expand Up @@ -121,6 +122,15 @@ async function copyBundledAiInstructions() {
await recursiveCopyDirectory( bundledAiInstructionsPath, getAiInstructionsPath() );
}

async function copyBundledPhpMyAdmin() {
const bundledPath = path.join( getWpFilesPath(), '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( getWpFilesPath(), 'latest', 'languages' );
if ( ! ( await fs.pathExists( bundledLanguagePacksPath ) ) ) {
Expand All @@ -140,6 +150,7 @@ export async function setupWPServerFiles() {
[ 'translations', copyBundledTranslations ],
[ 'language packs', copyBundledLanguagePacks ],
[ 'AI instructions', copyBundledAiInstructions ],
[ 'phpMyAdmin', copyBundledPhpMyAdmin ],
];

for ( const [ name, step ] of steps ) {
Expand Down
44 changes: 44 additions & 0 deletions scripts/download-wp-server-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' );

Expand Down Expand Up @@ -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 > {
Expand Down Expand Up @@ -90,6 +101,39 @@ async function downloadFile( file: FileToDownload ): Promise< void > {
}
fs.renameSync( sourcePath, targetPath );
}
} else if ( name === 'phpmyadmin' ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* 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 );
Expand Down
Loading