Add legacy WordPress support on PHP 5.2#3490
Conversation
d2b37ed to
a0865df
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds first-class legacy WordPress (1.0–4.9) support by introducing a PHP 5.2 WASM runtime, a PHP 7+→5.2 downgrader pipeline for the SQLite integration plugin, and end-to-end boot/install fixes plus UI + CI coverage to select and validate older WordPress versions.
Changes:
- Added PHP 5.2 runtimes (node + web), wiring them into loaders and version typing (
AllPHPVersion). - Implemented an AST-based PHP 7+→5.2 downgrader (many visitors + pretty printer) and bundled a prepatched SQLite plugin zip for PHP 5.2.
- Updated Playground boot flow, worker downloads, and website settings UI to support “Include older versions” + automatic PHP version locking; added Playwright coverage and CI job.
Reviewed changes
Copilot reviewed 82 out of 99 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.base.json | Adds TS path aliases for new PHP 5.2 node/web build packages. |
| scripts/php52-downgrader/src/Visitor/VariadicAndSplatVisitor.php | Downgrades variadics and splat calls to PHP 5.2-compatible forms. |
| scripts/php52-downgrader/src/Visitor/StripTypeDeclarationsVisitor.php | Removes PHP 7+ type declarations for PHP 5.2 compatibility. |
| scripts/php52-downgrader/src/Visitor/ShortTernaryVisitor.php | Lowers ?: short ternary to full ternary with temp vars when needed. |
| scripts/php52-downgrader/src/Visitor/SelfParentStaticRewriter.php | Rewrites self/parent/static references when hoisting code out of class scope. |
| scripts/php52-downgrader/src/Visitor/ReservedMethodRenameVisitor.php | Renames reserved/invalid method identifiers to keep output parseable. |
| scripts/php52-downgrader/src/Visitor/PromoteForHoistedClosuresVisitor.php | Promotes member visibility to support hoisted-closure $this access on PHP 5.2. |
| scripts/php52-downgrader/src/Visitor/Php7ErrorClassesVisitor.php | Maps PHP 7 error classes/Throwable to Exception. |
| scripts/php52-downgrader/src/Visitor/NullsafeVisitor.php | Lowers PHP 8 nullsafe operator to ternary + temps. |
| scripts/php52-downgrader/src/Visitor/NullCoalescingVisitor.php | Lowers ?? and ??= into PHP 5.2-compatible forms. |
| scripts/php52-downgrader/src/Visitor/NamespaceStripVisitor.php | Strips namespaces and leading \ in names for PHP 5.2 parsing. |
| scripts/php52-downgrader/src/Visitor/LateStaticBindingVisitor.php | Rewrites static:: to self:: for PHP 5.2 (drops LSB). |
| scripts/php52-downgrader/src/Visitor/InstanceCallOnNewVisitor.php | Rewrites (new Foo())->method/prop access into helper calls. |
| scripts/php52-downgrader/src/Visitor/FinallyVisitor.php | Lowers finally blocks by duplicating finally body into try/catches. |
| scripts/php52-downgrader/src/Visitor/ExponentVisitor.php | Rewrites ** and **= to pow(...). |
| scripts/php52-downgrader/src/Visitor/DirConstantVisitor.php | Forces __DIR__ into dirname(__FILE__) before later rewrites. |
| scripts/php52-downgrader/src/Visitor/ClosureHoistingVisitor.php | Hoists closures/arrow functions into named functions with capture handling. |
| scripts/php52-downgrader/src/Visitor/ClassKeywordVisitor.php | Rewrites ::class magic constant into compatible expressions. |
| scripts/php52-downgrader/src/Visitor/CallableExprVisitor.php | Rewrites complex callable expressions into call_user_func(...). |
| scripts/php52-downgrader/src/Visitor/AttributeAndDeclareStripVisitor.php | Removes PHP 8 attributes and declare(strict_types=1) for PHP 5.2. |
| scripts/php52-downgrader/src/Visitor/ArrayDerefOnCallVisitor.php | Rewrites fn()[0] patterns to helper _pg52_at(...). |
| scripts/php52-downgrader/src/Visitor/ArrayClassConstantVisitor.php | Hoists non-const class constant initializers into static properties + runtime init. |
| scripts/php52-downgrader/src/PrettyPrinter.php | Forces PHP 5.2-friendly printing (long arrays, __DIR__ safety net). |
| scripts/php52-downgrader/src/Downgrader.php | Orchestrates the visitor pipeline + cross-file constant hoist discovery. |
| scripts/php52-downgrader/composer.json | Adds composer package for the downgrader toolchain. |
| scripts/php52-downgrader/.gitignore | Ignores Composer vendor directory for the new tool. |
| packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs | Adds Playwright test covering boot flows across WP 1.0–4.9. |
| packages/playground/wordpress/src/mysql-shims.ts | Introduces mysql_* stubs for very old WP versions using SQLite. |
| packages/playground/wordpress/src/boot.ts | Adds legacy boot/install flow, network disabling, and safe DB checks for PHP < 7. |
| packages/playground/wordpress/project.json | Adds an Nx target to run the legacy boot test. |
| packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts | Adds module details for the prepatched v2.2.22-php52 SQLite driver asset. |
| packages/playground/website/src/lib/state/redux/site-management-api-middleware.ts | Switches PHP version typing to AllPHPVersion to include legacy versions. |
| packages/playground/website/src/lib/state/opfs/opfs-site-storage.ts | Updates persisted site metadata typing to allow legacy PHP versions. |
| packages/playground/website/src/components/site-manager/site-settings-form/unconnected-site-settings-form.tsx | Adds “Include older versions” toggle + grouped WP dropdown + PHP version locking. |
| packages/playground/website/src/components/site-manager/site-settings-form/older-wordpress-versions.ts | Adds older WP version list and a mapping to forced PHP versions. |
| packages/playground/remote/src/lib/playground-worker-endpoint.ts | Forces single-instance mode for legacy PHP; uses PHP 5.2 MU plugin stub. |
| packages/playground/remote/src/lib/playground-worker-endpoint-blueprints-v1.ts | Enables wordpress.org zip downloads for non-minified versions and selects v2.2.22-php52 for legacy PHP. |
| packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php | Replaces null coalescing with PHP 5.2-safe isset ternary. |
| packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php | Adds guards for older PHP/WP and avoids PHP 5.4+ array shorthand. |
| packages/playground/remote/src/lib/playground-mu-plugin/0-playground-php52.php | Adds a minimal PHP 5.2-compatible MU plugin stub (dummy HTTP transport + cron disable). |
| packages/playground/client/src/blueprints-v1-handler.ts | Skips update-check prefetch on legacy WordPress versions. |
| packages/playground/cli/tests/run-cli.spec.ts | Adjusts CLI test versions to avoid legacy WP on modern PHP. |
| packages/playground/cli/src/run-cli.ts | Allows selecting legacy PHP via AllPHPVersions and gates extensions/debug defaults for legacy PHP. |
| packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts | Passes phpVersion through to bootWordPress in the CLI worker. |
| packages/playground/cli/src/blueprints-v1/download.ts | Adds ability to pick specific sqlite driver zip variants (incl -php52). |
| packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts | Chooses prepatched sqlite driver version for legacy PHP. |
| packages/playground/blueprints/src/lib/v1/types.ts | Updates blueprint PHP version typing to include legacy versions. |
| packages/playground/blueprints/src/lib/v1/compile.ts | Compiles blueprint php version using AllPHPVersions. |
| packages/playground/blueprints/src/lib/types.ts | Updates runtime config PHP typing to AllPHPVersion. |
| packages/playground/blueprints/public/blueprint-schema.json | Adds AllPHPVersion/LegacyPHPVersion to schema to expose PHP 5.2. |
| packages/php-wasm/web/src/lib/load-runtime.ts | Allows loading legacy PHP in web, writes pre-boot php.ini via preRun, and blocks intl for legacy. |
| packages/php-wasm/web/src/lib/get-php-loader-module.ts | Adds loader module resolution for PHP 5.2 web build. |
| packages/php-wasm/web-builds/5-2/tsconfig.lib.json | Adds TS build configuration for PHP 5.2 web build package. |
| packages/php-wasm/web-builds/5-2/tsconfig.json | Adds TS project config for PHP 5.2 web build package. |
| packages/php-wasm/web-builds/5-2/src/index.ts | Adds 5.2 loader that selects JSPI vs Asyncify at runtime. |
| packages/php-wasm/web-builds/5-2/project.json | Adds Nx project/targets for building/publishing PHP 5.2 web binaries. |
| packages/php-wasm/web-builds/5-2/package.json | Adds npm package metadata for @php-wasm/web-5-2. |
| packages/php-wasm/web-builds/5-2/build.js | Adds esbuild-based bundle step for web PHP 5.2 package. |
| packages/php-wasm/universal/src/lib/supported-php-versions.ts | Introduces LegacyPHPVersions, AllPHPVersions, and isLegacyPHPVersion. |
| packages/php-wasm/universal/src/lib/proxy-file-system.ts | Skips PROXYFS mmap patching for legacy PHP to avoid parser corruption. |
| packages/php-wasm/universal/src/lib/legacy-php-ini.ts | Adds pre-boot php.ini content + preRun writer for legacy PHP builds. |
| packages/php-wasm/universal/src/lib/index.ts | Exports new legacy version helpers and legacy php.ini utility. |
| packages/php-wasm/node/src/lib/load-runtime.ts | Enables legacy PHP loading on node; blocks extensions on legacy; adds preRun ini creation. |
| packages/php-wasm/node/src/lib/get-php-loader-module.ts | Adds loader module resolution for PHP 5.2 node build. |
| packages/php-wasm/node/src/lib/extensions/memcached/get-memcached-extension-module.ts | Formatting-only changes in extension module resolution. |
| packages/php-wasm/node-builds/5-2/tsconfig.lib.json | Adds TS build configuration for PHP 5.2 node build package. |
| packages/php-wasm/node-builds/5-2/tsconfig.json | Adds TS project config for PHP 5.2 node build package. |
| packages/php-wasm/node-builds/5-2/src/index.ts | Adds 5.2 loader that selects JSPI vs Asyncify at runtime. |
| packages/php-wasm/node-builds/5-2/project.json | Adds Nx project/targets for building/publishing PHP 5.2 node binaries. |
| packages/php-wasm/node-builds/5-2/package.json | Adds npm package metadata for @php-wasm/node-5-2. |
| packages/php-wasm/node-builds/5-2/build.js | Adds esbuild-based bundle step for node PHP 5.2 package. |
| packages/php-wasm/compile/php/proc_open.h | Adds guards and PHP 5/7-compatible pipe typing for proc_open integration. |
| packages/php-wasm/compile/php/proc_open.c | Adds PHP 5.x stub implementation and PHP 5/7-compatible includes. |
| packages/php-wasm/compile/php/php_wasm.c | Adds PHP 5/7 API compatibility shims for string APIs and SAPI function signatures. |
| packages/php-wasm/compile/php/apply-mysqlnd-patch.sh | Makes mysqlnd patch application conditional by PHP version/file availability. |
| packages/php-wasm/compile/php/Dockerfile | Updates build logic to support PHP 5.x differences (zip/sqlite/curl/imagick/fiber/asm guards, warning flags). |
| packages/php-wasm/compile/php-wasm-memory-storage/wasm_memory_storage.c | Disables custom memory storage implementation for PHP < 7 with no-op init/shutdown. |
| packages/php-wasm/compile/php-wasm-dns-polyfill/dns_polyfill.c | Adds PHP 5/7 signature/parameter parsing compatibility. |
| packages/php-wasm/compile/php-post-message-to-js/post_message_to_js.c | Adds PHP 5/7 parameter parsing and return-string API compatibility. |
| .github/workflows/ci.yml | Adds CI job to boot-test legacy WP versions using Playwright. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private function isPhp52ConstantExpr(Node\Expr $expr): bool | ||
| { | ||
| if ($expr instanceof Node\Scalar\Int_) { | ||
| return true; | ||
| } | ||
| if ($expr instanceof Node\Scalar\Float_) { | ||
| return true; | ||
| } | ||
| if ($expr instanceof Node\Scalar\String_) { | ||
| return true; | ||
| } | ||
| if ($expr instanceof Expr\ConstFetch) { | ||
| $name = $expr->name->toString(); | ||
| return in_array( | ||
| $name, | ||
| ['true', 'false', 'null', 'TRUE', 'FALSE', 'NULL'], | ||
| true | ||
| ); | ||
| } | ||
| if ($expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus) { | ||
| return $this->isPhp52ConstantExpr($expr->expr); | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
The docblock says PHP 5.2 class constants may reference other class constants, but isPhp52ConstantExpr() currently treats all Expr\\ClassConstFetch as non-constant and will hoist them into runtime-initialized static properties. That changes semantics and can break valid constant usages (e.g., switch cases, array keys) that require a real constant. Consider extending isPhp52ConstantExpr() to treat Expr\\ClassConstFetch (at least for non-self/static/parent or other safe cases) as a valid PHP 5.2 constant expression so those constants remain compile-time constants.
| /** Returns the callable expression (first arg to call_user_func_array). */ | ||
| private function buildCallable(Node $node): ?Expr | ||
| { | ||
| if ($node instanceof Expr\FuncCall) { | ||
| if ($node->name instanceof Node\Name) { | ||
| return new Node\Scalar\String_($node->name->toString()); | ||
| } | ||
| if ($node->name instanceof Expr) { | ||
| return $node->name; | ||
| } | ||
| return null; | ||
| } | ||
| if ($node instanceof Expr\MethodCall) { | ||
| $method = $node->name instanceof Node\Identifier | ||
| ? $node->name->toString() | ||
| : null; | ||
| if ($method === null) { | ||
| return null; | ||
| } | ||
| return new Expr\Array_([ | ||
| new Node\ArrayItem($node->var), | ||
| new Node\ArrayItem(new Node\Scalar\String_($method)), | ||
| ]); | ||
| } | ||
| if ($node instanceof Expr\StaticCall) { | ||
| $method = $node->name instanceof Node\Identifier | ||
| ? $node->name->toString() | ||
| : null; | ||
| if ($method === null) { | ||
| return null; | ||
| } | ||
| $cls = $node->class instanceof Node\Name | ||
| ? $node->class->toString() | ||
| : null; | ||
| if ($cls === null) { | ||
| return null; | ||
| } | ||
| return new Expr\Array_([ | ||
| new Node\ArrayItem(new Node\Scalar\String_($cls)), | ||
| new Node\ArrayItem(new Node\Scalar\String_($method)), | ||
| ]); | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
For method/static calls with dynamic names (e.g. $obj->{$m}(...$args) or Foo::{$m}(...$args)), buildCallable() currently returns null, which causes rewriteSplatCall() to bail out and leave the unpack (...) syntax in the output—resulting in PHP 5.2 parse errors. Instead of returning null for dynamic identifiers, consider building a callable array with the method expression as-is, and for StaticCall also allow $node->class forms beyond Node\\Name (e.g. variables) when they can be passed through to call_user_func_array.
a0865df to
efa1168
Compare
f4e6cbd to
91853e3
Compare
91853e3 to
5b6a80c
Compare
5b6a80c to
a91d005
Compare
a91d005 to
106086f
Compare
106086f to
c1e7651
Compare
Everything a legacy WordPress / legacy PHP setup needs now lives
under packages/playground/wordpress/src/legacy-wp/:
* legacy-boot.ts — bootLegacyWordPress(), the self-contained
entry point mirroring bootWordPress's step ordering for PHP
5.2 + WP 1.0–4.9 on SQLite. Also hosts applyLegacyPhpIniOverrides
(called from bootRequestHandler).
* legacy-fixes.ts — WordPress source-file patches
(patchWordPressSourceFiles, backportWpPreV62MysqlCheck),
generateDbPhpContent, runPostInstallLegacyFixups, and the
LEGACY_WP_ERROR_REPORTING_* constants.
* mysql-shims.ts — PHP shims for mysql_* / mysqli_* APIs that
old WP expects.
boot.ts stays modern-first: a single
`if (isLegacyPHPVersion(options.phpVersion)) return bootLegacyWordPress(...)`
dispatches at the top of bootWordPress, and the body has zero
legacy conditionals. assertValidDatabaseConnection no longer takes
a phpVersion param — only the modern path reaches it. The
installWordPressSafe + assertValidDatabaseConnectionSafe wrappers
are gone; each boot path owns its own error handling.
Also:
* Gate backportWpPreV62MysqlCheck explicitly to WP 5.0–6.1 (renamed
from patchLegacyMysqlCheckForModernWp); skip the placeholder
db.php write on WP 6.2+ so modern trunk behaviour is preserved.
* Replace parseInt(phpVersion, 10) < 7 checks with
isLegacyPHPVersion() so non-numeric values like 'latest' never
slip into the legacy branch.
…lder
Extract every legacy mu-plugin and SQLite preload setup from
index.ts into dedicated files under legacy-wp/:
* legacy-mu-plugins.ts — setupLegacyPlatformLevelMuPlugins,
LEGACY_AUTO_LOGIN_BODY, legacy preload env.php, PHP 5.2
error handler, legacy auto_prepend_file.
* legacy-sqlite-preload.ts — preloadLegacySqliteIntegration,
buildLegacySqlitePreload, mysql*/mysqli* stubs, str_*
polyfills, WP < 3.1 add_action() guard.
Shared helpers live in neutral files at src/ root so legacy-wp/
doesn't import from the parent index.ts:
* platform-mu-plugins.ts — writeCommonPlatformMuPlugins
(0-playground.php, sitemap-redirect.php,
inline-tinymce-content-css.php), called from both paths.
* sqlite-preload-loader.ts — SQLITE_PRELOAD_LOADER_CLASS,
referenced by both modern and legacy preload builders.
setupPlatformLevelMuPlugins and preloadSqliteIntegration now
dispatch to the legacy helpers at the top and their bodies carry
no phpMajor / isPhp52 branches. index.ts goes from 1521 lines to
691.
Also drop the WP 5.0–6.1 db.php placeholder.
backportWpPreV62MysqlCheck used to write an empty
wp-content/db.php so install.php step=2's \$mysql_compat check
would fall through its file_exists(WP_CONTENT_DIR . '/db.php')
escape hatch. The placeholder was never needed —
WP_SQLite_DB::db_version() returns '8.0' (per the SQLite plugin's
docstring: "it returns mysql version number, but it means nothing
for SQLite. So it returns the newest mysql version"), which
satisfies install.php's version_compare check directly via the
lazy-\$wpdb loader. With this change no file is ever written to
wp-content on the modern boot path.
Reviewed every patch in legacy-wp/legacy-fixes.ts (and supporting files)
for need, gating tightness, and comment clarity. Net: 4828 → 3637 lines.
Verified WP 1.0–4.9 + PHP 5.2 still boot end-to-end.
Notable changes:
- Drop ensureLegacyAdminAuth — duplicated the auto-login flow already
provided by legacy-mu-plugins.ts.
- Drop the wp_check_mysql_version no-op — mysql-shims.ts already
returns '8.0.0' from mysql_get_server_info.
- Drop the dead overrides array in patchWpAdminRelativePaths — the
generic regex pass already covers every entry.
- Drop duplicate mysql_/mysqli_/str_* stubs from generateDbPhpContent —
the 0-sqlite.php preload runs first and already defines them.
- Promote `mail` to LEGACY_PHP_DISABLED_NETWORK_FUNCTIONS so it's
blocked across the whole boot lifecycle, and drop the wp_mail no-op
in patchWpInstallMailCrash (PHPMailer's SMTP fallback also dies on
the disabled fsockopen, so it now fails safely with a WP_Error).
- Drop the dead collapseWp1xInstallerSteps helper — install.php is
never reached during legacy boot, and runPostInstallLegacyFixups
seeds the admin user via PDO so a manual visit short-circuits to
"Already Installed".
- Drop the dead `if (!\$_pg_skip_redirect)` redirect branches from all
three eras of LEGACY_AUTO_LOGIN_BODY — playground_load_mu_plugins
unconditionally defines PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT before
invoking the body in legacy contexts.
- Restrict playground_legacy_set_auth_cookies_early to WP < 2.5; the
wordpressuser_* cookies it sets are inert on later versions.
- Share legacyAuthCookieBlock(usernameExpr) between
patchAdminAuthRedirect and patchAdminAjaxAuth; replace their brittle
wp-settings.php text scans with readOnDiskWpVersion gates.
- Move every WP-version-specific gate out of the patch functions and
into patchWordPressSourceFiles — the orchestrator now reads the WP
version once and dispatches each patch only when its range matches.
The previously needle-only patches (e.g. patchWp10*, patchWp21*,
patchCheckAdminReferer) get explicit ranges too, so they short-circuit
at the front door instead of reading files they can't apply to.
- Replace the fragile one-level-nesting regex in patchCheckAdminReferer
with a reusable balanced-brace helper (replacePhpFunctionBody), reused
by the "not installed" die() and the do_action('init') rewrite
extracted out of patchWpSettingsPhp.
- Fix patchWpSettingsPhp bug where settingsChanged was set
unconditionally inside the error_reporting block, rewriting
wp-settings on every boot.
- Split the PHP 5.2 vs 5.3+ error-handler boilerplate in
legacy-mu-plugins.ts into named helpers instead of inline ternaries.
- Trim multi-paragraph "## The bug / ## The fix" docblocks to a single
why-sentence each, dropping embedded WP source samples and
indentation diagrams.
Spell out in the top-of-file doc block that the legacy patches run as plain, idempotent string replacements without parsing PHP, why this relies on legacy WP releases being frozen, and point at the boot-smoke suite as the safety net for changes here.
Guards against Playground writing its own db.php, object-cache.php, advanced-cache.php, or sunrise.php into a user-mounted wp-content. Studio and similar consumers mount real wp-content directories into Playground, and a stray Playground-managed drop-in there would silently take over the user's external site.
Two targeted changes that remove ~2/3 of the per-version wall time: - Drop waitForWPFrame's 3 s poll granularity down to 500 ms, so the loop reflects how fast Playground actually responds instead of always burning whole 3 s ticks. - Replace the two unconditional waitForTimeout(8000) waits after post-click (Phase 2) and plugin-activate click (Phase 5) with an event-based wait: waitForWPFrame now accepts `excludeUrl` (skip the scoped frame while its URL still matches the pre-click value) and `contentPredicate` (require specific outcome text before returning, so we don't race the intermediate admin shell between POST and post-redirect render). Phase 5 also explicitly waits for any Activate link to become visible before selecting one — navigateViaUrlBar returns as soon as plugins.php has any body text, which on slow CI boots can be just the admin shell before the plugin list is populated. Local run on 7 representative versions (6.2, 6.1, 6.0, 5.9, 5.8, 4.9, 3.2) goes from ~33 s/version to ~10 s/version, all 5 phases pass including the ones that flaked under the prior code.
The SQLite integration plugin ships with MySQL 8 strict defaults (NO_ZERO_DATE + STRICT_TRANS_TABLES + ONLY_FULL_GROUP_BY), but legacy WordPress (1.0–4.9, contemporary with MySQL 4.1–5.6) was written against empty or near-empty sql_mode defaults. Three `patchWp*ZeroDate*` helpers were previously patching every wp_insert_post() variant that emits '0000-00-00 00:00:00' for drafts, just to make those INSERTs pass. Reset the driver's default `active_sql_modes` to `array()` once during SQLite preload for legacy PHP boots. One regex on the driver source, two source variants (standard multi-line and the php52-downgraded single-line form) covered by the same needle. WP 3.9+ already achieves the same effect at runtime via wpdb::set_sql_mode(), so the relaxed driver default is a no-op there and strictly an improvement for WP < 3.9. Net: -213 lines in legacy-fixes.ts (three patch functions + their needles), +39 in legacy-sqlite-preload.ts. Smoke test across WP 1.5 / 2.1 / 2.5 / 2.8 / 3.2 / 3.5 / 4.0 / 4.1 all pass on PHP 5.2.
Phase 3 (wp-admin dashboard load via URL bar) has been flaking on shared CI runners for one random modern-WP version per run — 5.5, 5.9, 6.0 across recent runs, always with the same empty-detail `admin [TIMEOUT]` after the full 120 s navigateViaUrlBar budget. The iframe gets stuck partway into the first /wp-admin/ load and never progresses; a fresh URL-bar fill+Enter almost always unsticks it on the second try. Retry once on timeout inline. Cheap and keeps real admin failures visible — the retry only kicks in when the first attempt returned null, so a genuine admin-page error still reports as the specific failure mode (ERROR / UNKNOWN) rather than getting retried.
Three provably-dead snippets removed from the legacy boot path: - PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT: defined in env.php but never read anywhere. LEGACY_AUTO_LOGIN_BODY has no redirect branch that would consult it. Delete the define() and collapse the misleading docblock to describe what the code actually does. - waitForAdminFrame(): 47-line helper in the legacy boot smoke test, never called. Phase 3 uses navigateViaUrlBar + waitForWPFrame. - str_contains/str_starts_with/str_ends_with polyfills in the legacy SQLite preload: the downgraded v2.2.22-php52 plugin ships identical function_exists-guarded polyfills in its own php-polyfills.php, and no code between the preload and the lazy-wpdb first touch calls str_*. The preload copies were redundant. playground-wordpress build + lint + 80/80 unit tests stay green.
env.php's playground_legacy_set_auth_cookies_early runs via auto_prepend_file before every PHP script (including the source- patched wp-admin/admin.php and wp-admin/admin-ajax.php). For WP < 2.5 it already populates wordpressuser_/wordpresspass_ cookies, which are identical to the USER_COOKIE/PASS_COOKIE constants WP 1.5–2.4 reads (both resolve to wordpressuser_\$cookiehash / wordpresspass_\$cookiehash). So the USER_COOKIE and COOKIEHASH elseif branches of legacyAuthCookieBlock were re-setting cookies that were already there. Keep the HMAC branch (WP 2.5+) and the \$_pg_user=null default; drop both elseif branches. Smoke test across WP 1.2 / 2.1 / 2.5 / 4.9 / 6.2 unchanged.
The legacy error handler had two builders — buildPhp52ErrorHandler (named function + \$GLOBALS) and buildModernErrorHandler (closure wrapped in call_user_func) — chosen by isPhp52. The named-function form works on every PHP version from 5.2 through 8.x, so the split is unnecessary. Drop buildModernErrorHandler and the isPhp52 flag; emit the named-function form unconditionally. setupLegacyPlatformLevelMuPlugins no longer needs its phpVersion option.
Neither has anything to do with SQLite; both were sitting at the tail of buildLegacySqlitePreload for historical reasons. * SERVER_PROTOCOL default moves to the legacy auto_prepend_file (next to the other \$_SERVER / register_globals polyfills), so it's set before preloads run and on every entry point — HTTP requests, php.run() fixups, CLI scripts. * date.timezone moves into applyLegacyPhpIniOverrides as an ini entry, guarded so a caller-supplied value still wins. No runtime behaviour change on any tested WP version; smoke test across WP 1.2 / 2.1 / 2.5 / 4.9 / 6.2 still green.
…IMS_PHP The four connection stubs that legacy WordPress (<3.0) needs for wpdb::__construct to not bail() on a falsy return were inlined at the top of buildLegacySqlitePreload, while the rest of the mysql_* family lived in MYSQL_SHIMS_PHP. Split by historical accident, not by design. Move the four connection stubs into MYSQL_SHIMS_PHP at the top (connection stubs first, then query/result stubs). The preload emits the same PHP output as before, but there's now one source of truth in mysql-shims.ts for every mysql_*/mysqli_* shim.
The newly-tagged v3.0.0-rc.3 release ships AUTO_INCREMENT value
management (sqlite-database-integration#367):
* SHOW TABLE STATUS LIKE returns a real Auto_increment computed
from sqlite_sequence (was NULL).
* INSERT … VALUES('0', …) into AUTO_INCREMENT columns is rewritten
to NULLIF(CAST(… AS INTEGER), 0), so MySQL's "0 advances the
sequence" semantics work on SQLite.
* AUTO_INCREMENT = N table option support.
These improvements obviate several legacy-WP source patches we
maintained against v2.2.22's behaviour. Bring them into the legacy
PHP 5.2 boot path by retargeting the offline downgrader at the new
upstream zip and renaming the produced artefact accordingly.
Mechanical: SRC_ZIP/OUT_ZIP in patch-sqlite-for-php52.mjs, the
'v2.2.22-php52' literal in the worker endpoint and CLI handler,
the version union in download.ts, and the resolver entry in
get-sqlite-driver-module-details.ts.
Follow-up: drop the now-redundant patches in legacy-fixes.ts.
WP 1.0's wp-admin/post.php emits `INSERT … VALUES ('0', …)` into the
AUTO_INCREMENT `ID` column. v2.2.22 stored the literal 0; we rewrote
the source to NULL so SQLite would pick the next rowid.
v3.0.0-rc.3's INSERT translator now wraps every AUTO_INCREMENT value
in `NULLIF(CAST(… AS INTEGER), 0)` (unless NO_AUTO_VALUE_ON_ZERO is
active). The 0-literal advances the sequence on its own, so the
source patch is a no-op.
Verified: WP 1.0 boot smoke test still PASS on front / post /
admin / new post phases.
Same story as the WP 1.0 patch one commit back: WP 1.2's wp-admin/post.php inserts a literal '0' into the AUTO_INCREMENT ID column, and v3.0.0-rc.3 now NULLIFs it transparently. Verified: WP 1.2 boot smoke test PASS on all five phases (front, post, admin, new post, plugin activation).
WP 2.0-2.2 install.php / upgrade-functions.php / admin-db.php
inserts the seed category as `INSERT INTO wp_categories (…, cat_ID,
…) VALUES ('0', …)`. v2.2.22 stored the literal 0 and the resulting
cat_ID=0 / category_parent=0 self-loop hung get_nested_categories()
in an infinite recursion.
v3.0.0-rc.3 NULLIFs zero-valued AUTO_INCREMENT inserts at the SQL
translator layer, so cat_ID becomes the next sequence value as on
MySQL. The rewriting source patch is no longer required.
Verified: WP 2.0 / 2.1 / 2.2 boot smoke tests all PASS on the five
phases (front, post, admin, new post, plugin activation), so
neither the dashboard nor the categories admin recurses any more.
The patch did two things, both now redundant with v3.0.0-rc.3: 1. SHOW TABLE STATUS LIKE 'wp_posts' previously returned Auto_increment = NULL on SQLite, so we backfilled with a MAX(ID)+1 fallback. v3.0.0-rc.3 reads the value from sqlite_sequence and returns the real next-value, so $id_result->Auto_increment is populated already. 2. WP 1.5's $postquery INSERT omits `pinged` and `post_content_filtered`, which are NOT NULL in the SQLite-built schema. v3.0.0-rc.3's INSERT translator fills omitted NOT NULL columns with their IMPLICIT DEFAULT under non-strict mode (we already clear active_sql_modes via relaxSqliteDriverSqlModes in the legacy preload), so the handwritten `''` columns are no longer required. Verified: WP 1.5 boot smoke test PASS on all five phases.
452dbd7 to
7085c52
Compare
Summary
Boot legacy WordPress (1.0 – 6.2) in the browser, with SQLite as the database.
The Playground settings panel gains an "Include older versions" checkbox that exposes every WordPress release from 1.0 through 6.9 (with jazz release code names) and auto-locks the PHP version to whatever runs it.
Replaces and closes #3469 (the PHP 5.6 version of the same feature).
What's in this PR
Builds on the PHP 5.2 WASM runtime from #3501 and adds:
scripts/php52-downgrader/) — a nikic/PhpParser pipeline of ~20 visitors that strips modern syntax so arbitrary PHP 7-era files parse on PHP 5.2.scripts/patch-sqlite-for-php52.mjs) — produces a PHP 5.2-compatible build of the upstream SQLite integration plugin.wordpress/src/boot.ts,legacy-wp-fixes.ts,mysql-shims.ts) — legacy-aware installer path, PDO table fallback for WP < 3.5, ~3400 lines of targeted source patches for WP 1.0 – 2.8 parser/SQL quirks, auth/nonce compat,wp-db.phpSQLite compatibility, and more.content_styleto work around TinyMCE'sdocument.open()creating a service-worker-uncontrolled iframe.Test plan
Automated (Playwright, all 32 legacy versions)
npm run dev # Wait for http://localhost:5400/website-server/ to be ready node packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjsCovers front page, single post, admin dashboard, new post page, and plugin activation across every WP minor release from 1.0 to 6.2.
Manual UI walk-through
npm run devand open http://localhost:5400/.🤖 Generated with Claude Code