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
|
|
||
| /** | ||
| * Network I/O functions that must be disabled on legacy PHP builds | ||
| * (< 7) to avoid "null function or function signature mismatch" |
There was a problem hiding this comment.
Isn't this the problem that EMULATE_FUNCTION_POINTER_CASTS was solving? This isn't a blocker here, but a functional plugin directory in WP Admin would be very cool.
There was a problem hiding this comment.
Claude says:
Short answer: yes and no — two layers at play.
-
The WASM signature-mismatch traps are the same class
EMULATE_FUNCTION_POINTER_CASTSused to paper over. The project dropped that flag a while back in favor of specific C-level patches (proc_open, dns_polyfill, post_message_to_js — there's a note inphp-part-1.spec.tsby the proc_open test). PHP 5.2 gets those extensions compiled in too, but its fsockopen/cURL paths still trap — hence thedisable_functionsstopgap here. -
Even without those traps, HTTP on PHP 5.2 wouldn't work:
0-playground-php52.phpregistersWp_Http_Dummy(returnsfalsefor every request) in place of the Fetch transport, becausewp_http_fetch.phpuses PHP 5.3+ namespace syntax and won't parse on 5.2.
So a functional plugin directory on 5.2 would need a 5.2-compatible wp_http_fetch.php (the PHP 5.2 downgrader could probably produce one) wired up in 0-playground-php52.php, not just a compile flag. Agreed it's a cool follow-up — not blocking here.
There was a problem hiding this comment.
I mean, what stops us from having a custom transport? Also, fsockopen wouldnt go through that transport anyway
| } | ||
| if (phpMajor < 7) { |
There was a problem hiding this comment.
| } | |
| if (phpMajor < 7) { |
There was a problem hiding this comment.
It seems like we don't need a separate if here.
There was a problem hiding this comment.
Addressed in a6d2e9a — the legacy branch is now a separate bootLegacyWordPress() in legacy-wp/legacy-boot.ts, so the modern bootWordPress no longer carries any legacy-specific if sandwich.
| ] as const; | ||
|
|
||
| /** | ||
| * Minimal wp-content/db.php drop-in for modern WordPress. Its job |
There was a problem hiding this comment.
We don't have that today, right? Why is this needed after this PR? Would this create an unwanted db.php file for Studio users? Let's add a test for Playground CLI to ensure that booting a site with a custom wp-content mounted doesn't create any new files in that wp-content to make sure we never regress on this.
There was a problem hiding this comment.
Addressed in 537d860 (dropped the modern-WP db.php placeholder — the SQLite preload doesn't need a drop-in at all) and 4f967a9 (new CLI regression test mounts a host wp-content and asserts Playground doesn't drop db.php, object-cache.php, advanced-cache.php, sunrise.php, or any other file at its root).
| { phpVersion: options.phpVersion } | ||
| ); | ||
|
|
||
| // Write wp-content/db.php. Two distinct WordPress code paths |
There was a problem hiding this comment.
AFAIR Playground doesn't create that file today. Let's avoid creating it except in legacy WP versions.
There was a problem hiding this comment.
Addressed in 537d860 — the modern boot path no longer writes any db.php. A drop-in is still written, but only by the legacy boot path (legacy-wp/legacy-boot.ts).
| ); | ||
| } | ||
|
|
||
| // WordPress 5.0–6.1's `wp_check_php_mysql_versions()` runs |
There was a problem hiding this comment.
Let's avoid this on WP 6.2+
There was a problem hiding this comment.
Addressed in a6d2e9a — backportWpPreV62MysqlCheck reads the on-disk $wp_version and short-circuits when it's ≥ 6.2 (also < 5.0, since those go through the legacy boot path instead).
| * include definitions for some of the necessary constants. | ||
| */ | ||
| await ensureWpConfig(php, requestHandler.documentRoot); | ||
| const phpMajor = Number.isFinite(parseInt(options.phpVersion ?? '', 10)) |
There was a problem hiding this comment.
The legacy logic clutters this function a lot, and it's such a niche use-case. Is there any way we could retain a mostly uncluttered boot logic for modern WP versions? As in move that legacy-specific code and large doc blocks out of this function and reduce that to a few small calls. Or even have a separate boot function for legacy WordPresses.
There was a problem hiding this comment.
Addressed in a6d2e9a — the legacy branches in bootWordPress moved to a dedicated bootLegacyWordPress under legacy-wp/legacy-boot.ts. The modern entry point is now a single if (isLegacyPHPVersion(...)) return bootLegacyWordPress(...) dispatch followed by the unchanged modern flow.
| ); | ||
| if (!hasCustomDatabasePath) { | ||
| await assertValidDatabaseConnection(requestHandler); | ||
| await assertValidDatabaseConnectionSafe( |
There was a problem hiding this comment.
Do we need assertValidDatabaseConnectionSafe and assertValidDatabaseConnection? Could we keep using assertValidDatabaseConnection as the only function identifier?
There was a problem hiding this comment.
Addressed in a6d2e9a — the *Safe variant is gone; bootWordPress uses the original assertValidDatabaseConnection, and the legacy-specific relaxation lives inside bootLegacyWordPress.
| @@ -0,0 +1,145 @@ | |||
| /** | |||
| * PHP code that provides functional mysql_* function stubs. | |||
There was a problem hiding this comment.
This is cool. Later on this would make sense as a part of @php-wasm/universal as a general mysql shim. Or maybe as a part of the composer packages we'll release for the SQLite plugin? Not sure. Nothing blocking here.
| @@ -0,0 +1,3396 @@ | |||
| /** | |||
| * Legacy WordPress version fixes. | |||
There was a problem hiding this comment.
Wat. This actually works for a large range of WordPress versions? amazing :D
There was a problem hiding this comment.
I would suggest version-specific .patch files or, at least, version checks before the replacements here. But this seems to be actually working as is. Scary! This just runs blind replacements without worrying about the WP version – how do we know we're not patching the wrong WordPress version? We probably don't. Does it matter? If everything works, probably not. Those WP versions won't change, and patching this might break the delicate equilibrum we have here. This is terrible. I love it. Let's document that in this doc block.
There was a problem hiding this comment.
Addressed in 76de744 — added a top-of-file doc block in legacy-wp/legacy-fixes.ts spelling out the blind find-and-replace approach, why it's load-bearing on frozen legacy WP releases, and pointing at the boot-smoke suite as the safety net before touching patches there.
| * @param php | ||
| */ | ||
| export async function setupPlatformLevelMuPlugins(php: UniversalPHP) { | ||
| export async function setupPlatformLevelMuPlugins( |
There was a problem hiding this comment.
Legacy handling adds a bunch of clutter. How much can we decouple these things? Could we contain most of these things in like a legacy/ subdirectory or even a single legacy.ts file and then just call them from other places? I'd love the main code paths to remain very focused on the modern WP versions and only have tactical mentions of the legacy flows that can be inspected if needed but that would mostly stay out of our way when we work with this code.
There was a problem hiding this comment.
Addressed in a6d2e9a — all legacy-specific code now lives under packages/playground/wordpress/src/legacy-wp/ (legacy-boot.ts, legacy-fixes.ts, legacy-mu-plugins.ts, legacy-sqlite-preload.ts, mysql-shims.ts). The main boot path only references it through the bootLegacyWordPress dispatch.
adamziel
left a comment
There was a problem hiding this comment.
The ideas are great. Even the terrible ones are impressive. Let's just cleanup the main data flows. Human attention is limited and the legacy code blocks are really large. Let's make sure we don't have to always sieve through them when we work with Playground's main data flows – we won't need them most of the time.
The SQLite database integration plugin uses PHP 7+ features that PHP 5.2 can't parse (type declarations, closures, `??`, short arrays, traits, class constant expressions, namespaces, etc.). Rather than maintaining a frozen, hand-patched binary blob that has to be regenerated by eye on every upstream release, this commit adds the tooling to rebuild it deterministically offline. Components: * `scripts/patch-sqlite-for-php52.mjs` — the offline patcher. Unzips the upstream v2.2.22 plugin to a temp dir, runs the AST downgrader, applies a short list of per-file surgical fixes for shapes that can't live in a generic visitor, and re-zips to the final output. Requires a host PHP 7.4+ and `composer` on PATH; auto-runs `composer install` in the downgrader directory the first time. * `scripts/php52-downgrader/` — standalone PHP tool on nikic/php-parser v5. `composer.json` + `composer.lock` pin the dependency; vendor files are gitignored. 21 `NodeVisitor`s cover every 7+ → 5.2 syntactic transform: type declarations, `??`, `?->`, `?:`, `**`, `[...]`, variadics, `X::class`, `static::`, `__DIR__`, `fn()[n]`, `(new X())->y`, PHP 7 error classes, `#[Attributes]`, `declare(strict_types)`, fully-qualified names, namespace stripping, reserved method renames, `finally`, array class constants. * `ClosureHoistingVisitor` lifts every `Closure` / `ArrowFunction` to a named top-level function. `use(...)` captures become extra parameters or `$GLOBALS`-backed captures; bodies that reference `$this` are rewritten to a renamed `$__pg_this` parameter; the host class is opted into public member visibility by `PromoteForHoistedClosuresVisitor` so the hoisted function can reach private/protected members. * Every `self::X` / `parent::X` reference inside a hoisted expression or an extracted class-constant array is rewritten to the literal class name (or the parent class name from the `extends` clause). * Surgical fallbacks in `patch-sqlite-for-php52.mjs` handle trait inlining in `class-wp-pdo-proxy-statement`, `ReflectionProperty` try/catch wraps, `WP_SQLite_DB` polyfill injection, `array_column` polyfill, `Closure::call` proxies in `WP_SQLite_Driver`, and a handful of WP function-existence guards. * `bin/scan-out-of-class-self.mjs` is a deterministic post-pass that walks the pretty-printed output and fails the build if any `self::`/`parent::`/`static::` token appears outside a class body. Catches scope-leak regressions that runtime-fatal on PHP 5.2 but slip past a plain parse check. Committed artifacts: * `sqlite-database-integration-v2.2.22.zip` — unpatched upstream. * `sqlite-database-integration-v2.2.22-php52.zip` — downgraded output so downstream consumers (website, CLI) don't need PHP or composer at consumption time. * `get-sqlite-driver-module-details.ts` — updated to expose the new `v2.2.22-php52` plugin variant. Verified with `playground-wordpress` unit tests (80/80 green) and `test-legacy-wp-version-boot.mjs`: WP 4.9 → 1.0 on PHP 5.2, all five phases (front / post / admin / new post / plugin activation) pass on every version that supports each phase.
Port complete legacy WordPress (1.0–4.9) boot support from the legacy-wordpress-support-v2 branch, adapted for PHP 5.2 only. boot.ts: Legacy PHP boot flow with source patching, safe install and validation wrappers, post-install fixups for WP versions that lack standard install infrastructure, and a `runDbDeltaOnly` path that gates the PDO fallback to `wp_version < 3.5` so WP 3.5+ boots cleanly through the AST SQLite driver without getting WP 1.x-shaped tables written behind the driver's back. index.ts: Legacy-aware mu-plugin setup with PHP 4 superglobal polyfills, eval()-based preload for parser-incompatible files, WordPress version-conditional auth bypass, and an error handler adapted for PHP 5.2 syntax. Auto-login always takes the in-process cookie path (PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT unconditionally defined) because redirect+Set-Cookie is unreliable on PHP 5.2 WASM — wp_set_auth_cookie()'s headers don't consistently reach the Playground service worker's cookie store across all WP versions. legacy-wp-fixes.ts: ~3000 lines of WP source patches covering: - wp-settings.php deprecated function removal - wp-db.php SQLite compatibility - install.php schema fixes for PDO/SQLite - WP 1.0–2.8 specific parser and SQL fixes - Auth cookie and nonce compatibility for WP < 4.5 - Pretty permalink support for legacy WP - comment_count backfill on wp_posts via $alter_cols (WP 1.5+ reads wp_posts.comment_count directly in get_comments_number()) mysql-shims.ts: MySQL function stubs delegating to $wpdb for WP < 3.0. Add CI job and test script for legacy WP version boot testing.
Three wordpress.org-downloaded WP bootstrap issues were silently
breaking modern WP 5.0–6.1 on the SQLite integration:
1. wp_check_php_mysql_versions() on WP 5.0–6.1 hard-checks
extension_loaded('mysqli'), which a userland stub cannot
satisfy. Patch wp-includes/load.php to use
function_exists('mysqli_connect') instead — matching the fix
WordPress itself shipped in 6.2. No-op on 6.2+.
2. install.php step=2 has its own mysql version check that falls
through on file_exists(WP_CONTENT_DIR . '/db.php'). Write a
minimal @playground-managed db.php drop-in for modern WP so
that check passes; teach the preload guard to recognise our
own marker and not self-skip on its own file.
3. The SQLite plugin declares private \$allow_unsafe_unquoted_parameters
on WP_SQLite_DB and then calls \$this->__get() from prepare(),
expecting wpdb::__get() to return the parent's property. That
works on WP 6.2+ (wpdb declares the same property upstream)
but triggers a silent fatal Error on older WordPress, where
wpdb's __get runs \`return \$this->\$name;\` from the parent
class context and PHP refuses to read a child's private
member. Patch the declaration to protected so both class
contexts can reach it.
Additionally, WP 5.0 (Gutenberg 1.0) crashes the runtime with
exit code 255 inside prefetchUpdateChecks when using the modern
SQLite driver, so extend the prefetch skip range from < 5 to
< 5.1.
Test script:
- Convert flat WP_VERSIONS list to a {wp, php} matrix so the
same script can cover both the legacy (WP 1.0–4.9 + PHP 5.2)
and modern (WP 5.0–6.2 + PHP 7.4) ranges.
- Add WP_ONLY env filter for targeted local runs.
- Editor-markers check now also recognises Gutenberg
(id="editor", edit-post-layout, block-editor-writing-flow)
so WP 5.0+ post-new.php passes the newPost phase.
- Plugin phase targets Hello Dolly by href instead of the first
Activate link, which on modern WP is Akismet and lands on a
setup page without the expected confirmation strings.
All 13 modern versions (5.0–6.2) now pass all 5 phases locally,
and the legacy range (4.9, 3.5, 2.0, 1.2 spot-checked) is
unchanged.
Replace the "Need an older version?" link in the Playground settings panel with a proper "Include older versions" checkbox. When checked, the WordPress version dropdown lists every minor release from 1.0 through the current minified set, grouped into "Current versions" (the minified 6.3 – 6.9 + trunk/beta bucket) and "Older versions" (non-minified 6.2 back to 1.0) with disabled separator rows. When the user picks an older version, the PHP version dropdown is automatically locked to whatever runs that WordPress: * WP 1.0 – 4.9 → PHP 5.2 (the only pre-7 WASM build we ship). * WP 5.0 – 6.2 → PHP 7.4 (safest single choice across the whole bucket; PHP 8.x is unreliable on WP 5.0 – 5.5). * WP 6.3+ → no lock, default stays RecommendedPHPVersion. Implementation notes: * Widen SiteFormData.phpVersion (and the redux SiteSettings / opfs-site-storage schema) from SupportedPHPVersion to AllPHPVersion so 5.2 is representable end-to-end. * The PHP Controller is disabled via SelectControl's own `disabled` prop, not react-hook-form's Controller.disabled — the latter nulls out the field value and breaks the unlock transition. * When the user unlocks from a legacy-locked state (e.g. 4.9 → 6.5), the effect resets phpVersion to RecommendedPHPVersion so the dropdown doesn't render a stale 5.2 that isn't in its option list. * The checkbox auto-initializes to `true` when the form loads with an already-older wpVersion, so bookmarking `?wp=4.9&php=5.2` shows the right state without the user toggling anything. * older-wordpress-versions.ts holds the hardcoded WP release list plus getForcedPhpVersionForWordPress() / isOlderWordPressVersion() helpers.
Show each WordPress release's jazz-musician shorthand next to its number in the Site Settings version picker (e.g. "6.9 (Gene)", "4.9 (Tipton)"). Labels without a matching release — trunk, beta, and pre-release majors — stay unchanged. The mapping uses the single-word shorthand each release's announcement URL on wordpress.org/news carries (first name, last name, or nickname, depending on the release).
TinyMCE loads its content_css (editor styles, dashicons, theme CSS) via <link> tags inside an about:blank iframe it creates for the editing area. The Playground service worker cannot intercept sub-resource requests from about:blank documents, so those CSS files return 404 — visible as "Failed to load content css" in TinyMCE's notification bar. This affects all WP versions that use TinyMCE (3.9–4.9, and modern WP with Classic Editor) on both localhost and 127.0.0.1 with a fresh browser context. Add a mu-plugin that hooks tiny_mce_before_init, reads each content_css file from disk, and passes the combined CSS inline via TinyMCE's content_style setting. This bypasses the network request entirely. Written in PHP 5.2-compatible syntax so it works across all supported PHP versions.
991109b to
df5c21e
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.
30d1c77 to
c6241c1
Compare
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.
981c11d to
4f967a9
Compare
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.
490d1fb to
d7c2cd4
Compare
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.
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