Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,33 @@ jobs:
node-version: 20
- run: packages/php-wasm/cli/tests/smoke-test.sh

test-legacy-wp-version-boot:
if: github.repository == 'WordPress/wordpress-playground' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/actions/prepare-playground
- name: Install Playwright Browser
run: npx playwright install chromium --with-deps
- name: Start dev server
run: |
npm run dev > /tmp/playground-dev.log 2>&1 &
timeout=120; elapsed=0
until curl -s -o /dev/null http://127.0.0.1:5400/website-server/ 2>/dev/null; do
sleep 3
elapsed=$((elapsed + 3))
if [ $elapsed -ge $timeout ]; then
echo "Dev server failed to start within ${timeout}s"
cat /tmp/playground-dev.log | tail -50
exit 1
fi
done
- name: Test legacy WordPress version boot
run: node packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs

# Redis extension tests - verifies the php-redis extension loads
# and provides the expected API, and can connect to a real Redis server.
# Redis requires JSPI because asyncify cannot properly handle exceptions
Expand Down
75 changes: 75 additions & 0 deletions packages/playground/cli/tests/run-cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,81 @@ describe.each(blueprintVersions)(
expect(response.text).toContain('My WordPress Website');
});

// Regression test: Playground must not write its own drop-ins
// (db.php, object-cache.php, advanced-cache.php, sunrise.php)
// into a user-mounted wp-content. Studio and other consumers
// mount real wp-content directories into Playground, and any
// Playground-managed file written at the wp-content root would
// silently take over the user's external site.
test('should not drop any new files at the wp-content root when wp-content is mounted', async () => {
const hostWpContent = await mkdtemp(
path.join(tmpdir(), 'playground-test-mount-wpcontent-')
);
// Minimal wp-content skeleton. `plugins/` and `themes/`
// stay empty so WP's unzip step fills them in; any file
// added at the root of hostWpContent after boot must come
// from Playground itself.
mkdirSync(path.join(hostWpContent, 'plugins'));
mkdirSync(path.join(hostWpContent, 'themes'));
writeFileSync(
path.join(hostWpContent, 'index.php'),
'<?php // Silence is golden.\n'
);
const filesBefore = new Set(readdirSync(hostWpContent));

try {
await using cliServer = await runCLI({
...suiteCliArgs,
command: 'server',
'mount-before-install': [
{
hostPath: hostWpContent,
vfsPath: '/wordpress/wp-content',
},
],
});

// Confirm the site booted so we're actually exercising
// the full boot → install flow, not a no-op path.
const homeResponse = await fetch(
new URL('/', cliServer.serverUrl)
);
expect(homeResponse.status).toBe(200);

// No Playground-managed drop-in should appear at the
// wp-content root. These four names cover the WP
// drop-ins that a rogue Playground write could abuse.
for (const dropIn of [
'db.php',
'object-cache.php',
'advanced-cache.php',
'sunrise.php',
]) {
expect(existsSync(path.join(hostWpContent, dropIn))).toBe(
false
);
}

// Belt-and-suspenders: any net-new *file* at the
// wp-content root is a Playground drop-in regression.
// Directories added by WordPress itself during install
// (e.g. `database/` for the SQLite DB, `fonts/` for
// the Fonts API, `upgrade/`) are legitimate and ignored.
const filesAfter = new Set(readdirSync(hostWpContent));
const unexpectedNewFiles = [...filesAfter]
.filter((f) => !filesBefore.has(f))
.filter(
(f) =>
!lstatSync(
path.join(hostWpContent, f)
).isDirectory()
);
expect(unexpectedNewFiles).toEqual([]);
} finally {
rmSync(hostWpContent, { recursive: true, force: true });
}
}, 120000);

// Regression test: mounting files under /tmp (which is already
// NODEFS-mounted to a shared host directory) used to race
// across 6 workers and intermittently fail with ErrnoError 20
Expand Down
7 changes: 5 additions & 2 deletions packages/playground/client/src/blueprints-v1-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ export class BlueprintsV1Handler {
* Pre-fetch WordPress update checks to speed up the initial wp-admin load.
* Skip for old WordPress versions — the functions called by prefetch
* (wp_check_php_version, wp_update_plugins, etc.) don't exist or crash
* on legacy WP, and the resulting PHP errors create noise.
* on legacy WP, and the resulting PHP errors create noise. WP 5.0
* (Gutenberg 1.0) also crashes the runtime with exit code 255 inside
* prefetchUpdateChecks when using the modern SQLite driver, so extend
* the skip range up to (but not including) WP 5.1.
*
* parseFloat extracts the major version from strings like "6.8",
* "4.9.26", etc. Non-numeric values like "nightly" or "trunk"
Expand All @@ -99,7 +102,7 @@ export class BlueprintsV1Handler {
* @see https://github.com/WordPress/wordpress-playground/pull/2295
*/
const wpMajor = parseFloat(runtimeConfiguration.wpVersion);
const isLegacyWpVersion = Number.isFinite(wpMajor) && wpMajor < 5;
const isLegacyWpVersion = Number.isFinite(wpMajor) && wpMajor < 5.1;
if (runtimeConfiguration.networking && !isLegacyWpVersion) {
await playground.prefetchUpdateChecks();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { AllPHPVersion } from '@php-wasm/universal';

/**
* WordPress versions that ship as non-minified downloads from
* wordpress.org (or via the Playground CORS proxy). The web worker
* handles these via its `!isMinifiedVersion` branch in
* playground-worker-endpoint-blueprints-v1.ts.
*
* Ordered newest-first so the UI dropdown shows the most recent
* older versions at the top of the "older versions" group.
*/
export const OlderWordPressVersions = [
// WP 6.0 – 6.2 work on PHP 7.4+ but run best on PHP 8.x. Still not
// minified today, so they're fetched from wordpress.org like the
// legacy bucket.
'6.2',
'6.1',
'6.0',
// WP 5.x — PHP 5.6.20+ required; PHP 7.4 is the safest choice.
'5.9',
'5.8',
'5.7',
'5.6',
'5.5',
'5.4',
'5.3',
'5.2',
'5.1',
'5.0',
// WP 4.x — PHP 5.2.4+ required; our only 5.x WASM build is 5.2.
'4.9',
'4.8',
'4.7',
'4.6',
'4.5',
'4.4',
'4.3',
'4.2',
'4.1',
'4.0',
// WP 3.x
'3.9',
'3.8',
'3.7',
'3.6',
'3.5',
'3.4',
'3.3',
'3.2',
'3.1',
'3.0',
// WP 2.x (2.4 was never released)
'2.9',
'2.8',
'2.7',
'2.6',
'2.5',
'2.3',
'2.2',
'2.1',
'2.0',
// WP 1.x (1.1, 1.3, 1.4 were never released)
'1.5',
'1.2',
'1.0',
] as const;

export type OlderWordPressVersion = (typeof OlderWordPressVersions)[number];

/**
* Returns the PHP version a given WordPress release must run on
* inside Playground, or `null` if any supported modern PHP version
* will do.
*
* - WP < 5.0 (the legacy bucket): only our PHP 5.2 WASM build works.
* WP 4.x officially requires PHP 5.2.4+, but Playground's 5.6+
* builds have been retired so 5.2 is the only option available
* here.
* - WP 5.0 – 6.2 (the older-but-not-legacy bucket): PHP 7.4 is the
* safest single choice — old enough for WP 5.0's PHP 5.2.4 era
* code (which runs fine on 7.4) yet new enough that nothing
* depends on PHP 5 quirks. PHP 8.x would work for WP 5.6+ but not
* reliably for WP 5.0 – 5.5, so we force 7.4 across the whole
* bucket.
* - WP 6.3+ (the minified bucket): returns `null`. The UI lets the
* user pick any supported PHP version and we default to the
* recommended one.
*/
export function getForcedPhpVersionForWordPress(
wpVersion: string | undefined
): AllPHPVersion | null {
if (!wpVersion) {
return null;
}
const major = parseFloat(wpVersion);
if (!Number.isFinite(major)) {
return null;
}
if (major < 5) {
return '5.2';
}
if (major < 6.3) {
return '7.4';
}
return null;
}

/** True for WP versions that live in the non-minified "older" bucket. */
export function isOlderWordPressVersion(
wpVersion: string | undefined
): boolean {
if (!wpVersion) {
return false;
}
return (OlderWordPressVersions as readonly string[]).includes(wpVersion);
}
Loading
Loading