Closed
Conversation
bda9d53 to
90815ba
Compare
18d442f to
9c61190
Compare
79c3c39 to
373d90a
Compare
Introduce LegacyPHPVersions and AllPHPVersions types alongside the existing SupportedPHPVersions. PHP 5.6 is classified as a "legacy" version — available but not in the standard supported list. Update Blueprint types to accept legacy PHP versions and regenerate the Blueprint JSON schema.
Add PHP 5.6.40 compilation support for both asyncify and JSPI build modes. Key changes to the build pipeline: - Add OpenSSL 1.1 compatibility patch (1100 lines) since PHP 5.6 directly accesses opaque OpenSSL structs removed in later versions - Replace PHP 5.6's bundled SQLite 3.8.10.2 with SQLite 3.51.0 (the v2.2.22 SQLite driver requires >= 3.37.0) - Add EMULATE_FUNCTION_POINTER_CASTS for JSPI build to fix a sort crash in PHP 5.6's zend_qsort - Patch C source files for compatibility with older PHP internal APIs - Add node-builds/5-6 and web-builds/5-6 packages with pre-compiled WASM binaries - Add PHP 5.6 loader entries in node and web runtime packages
Add functions that transform PHP 7+ code to PHP 5.6 compatible code for the SQLite integration plugin: - stripPhp7TypeDeclarations: removes parameter and return type declarations, including from interface/abstract methods (`;` terminators). Handles `mixed`, `never`, and nullable types. - replaceNullCoalescing: transforms ?? operators to isset() ternaries, with special handling for class constants and method calls to avoid double evaluation. Includes a safety limit to prevent infinite loops. - replacePhp7ErrorClasses: replaces Throwable with Exception, removes PHP 8 attributes and declare(strict_types), transforms Closure::call(), replaces callable property invocations with call_user_func(), and handles isset() on class constant arrays. These are used both at runtime (when extracting the SQLite plugin ZIP) and offline (in the pre-patching script).
Add an offline patcher script that transforms the SQLite Database Integration v2.2.22 plugin into a PHP 5.6-compatible version: - Removes all PHP 7+ syntax using the transpiler functions - Renames 'throw' method (reserved word in PHP 5.6) - Adds polyfills: placeholder_escape(), reinitialize_sqlite(), init_charset(), get_caller(), log_query(), and wpdb properties - Guards WordPress function calls that don't exist in old versions - Makes schema parse failures non-fatal for old WP schemas - Skips SQLite's wp_install() override for WP < 3.0 The pre-patched ZIP is committed as a build artifact. Add version entries for v2.2.22 and v2.2.22-php56 in the SQLite driver module details. Regenerate with: node --experimental-strip-types scripts/patch-sqlite-for-php56.mjs
60f9b99 to
e9870f7
Compare
Make WordPress 1.2 through 4.9 bootable on PHP 5.6 with SQLite by
patching WP source files at boot time. All legacy-specific code lives
in dedicated modules (legacy-wp-fixes.ts, mysql-shims.ts) to keep
boot.ts and index.ts focused on the core boot flow.
WP source file patching (legacy-wp-fixes.ts):
- wp-settings.php: disable extension_loaded('mysql') check, suppress
E_DEPRECATED/E_STRICT, remove set_magic_quotes_runtime, guard
get_magic_quotes_gpc, fix =& new syntax, replace $HTTP_SERVER_VARS
- wp-db.php: guard $wpdb creation, patch mysql_connect to call
db_connect(), inject wpdb method polyfills (set_prefix, timer_start,
timer_stop, init_charset, bail, check_connection)
- install.php: fix relative paths, replace $HTTP_GET_VARS/$HTTP_POST_VARS,
combine WP 1.x multi-step installer into single request
- functions.php: fix $all_options stdClass, eval db.php for parser bug
- schema.php: add wp_get_db_schema() polyfill for WP < 3.3
- Create wp-load.php shim for WP < 2.0, version.php stub for WP < 1.5
- Pre-create WP 1.x tables via PDO, seed admin user and default content
- Strip doc comments and skip large includes for WP 2.8 (parser bug)
SQLite integration (index.ts, legacy-wp-fixes.ts):
- Patch SQLite plugin for PHP 5.6 (strip type declarations, null
coalescing, rename reserved words, replace __DIR__ and require_once)
- Write db.php with MySQL/MySQLi stubs and PHP polyfills
- Lazy $wpdb loader calls reinitialize_sqlite() for old WP
- Functional mysql_* stubs (mysql-shims.ts) delegate to $wpdb
Boot flow adjustments (boot.ts):
- Skip ensureWpConfig for legacy PHP, copy wp-config-sample.php instead
- Non-fatal install errors and database validation for legacy PHP
- Skip isWordPressInstalled() for legacy PHP (crashes WASM on old WP)
Also add JavaScript unzip fallback for PHP builds without ZipArchive.
Web worker (playground-worker-endpoint-blueprints-v1.ts): - Select SQLite driver version based on PHP and WP version: PHP 5.6 uses pre-patched v2.2.22-php56, WP < 6.x on modern PHP uses v2.1.16, modern WP uses trunk - Pass phpVersion to bootWordPress for legacy detection - Disable WP_DEBUG for legacy PHP CLI (blueprints-v1-handler.ts, download.ts, worker-thread-v1.ts): - Add getPrebuiltWordPressPath() to use bundled WP ZIPs - Pass SQLite version parameter to fetchSqliteIntegration() - Pass phpVersion through to boot process Mu-plugins (0-playground.php, wp_http_fetch.php, wp_http_dummy.php): - Guard wp_doing_ajax() and wp_doing_cron() with function_exists() - Guard get_current_user_id() for WP < 3.0 compatibility - Guard Requests class and Requests_Transport interface checks - Replace [] syntax with array() for PHP 5.6 compatibility
Test that WordPress versions 1.2 through 4.9 boot on PHP 5.6 with SQLite and display "Hello world!". Starts the dev server, then runs each version through the browser via Playwright. WP 1.0.2 is excluded — the SQLite AST driver can't parse its enum() column types, causing WHERE clause failures.
When a non-minified WP version like "4.9" or "1.5" is passed via ?wp= parameter, construct a wordpress.org download URL and route it through the CORS proxy instead of silently falling back to the latest minified WP build. Versions >= 2.0 work as wordpress-<major>.<minor>.zip (wordpress.org redirects to latest patch). Versions < 2.0 need normalization since wordpress.org only hosts explicit patch versions (1.0.2, 1.2.2, 1.5.2). Also fix SQLite version selection to use the actual requested WP version instead of the minified fallback, ensuring old WP on modern PHP correctly gets the v2.1.16 SQLite driver.
Three issues prevented wp-admin from working on PHP 5.6: 1. template.php and media.php were replaced with empty stubs as a workaround for the WASM parser size limit. The real fix is to strip doc comments (which stripDocCommentsFromWpIncludes already does). Remove the stub replacement. Also preserve ?> tags during comment stripping — PHP's ?> closes PHP mode even inside // comments. 2. Old WordPress (< 3.7) uses relative paths in wp-admin scripts. Patch these to use dirname(__FILE__) for absolute resolution. Covers index.php, index-extra.php, and admin.php across all old WP versions. 3. Auth cookies don't persist correctly for old WordPress admin sessions. Fix by re-generating auth cookies on every admin request: mu-plugin for WP 2.8+, direct admin.php patch for WP < 2.8 (no mu-plugin support). Handles three auth eras: - WP 2.5+: wp_generate_auth_cookie + $_COOKIE population - WP 2.0-2.4: USER_COOKIE/PASS_COOKIE with md5(md5(password)) - WP 1.5: COOKIEHASH-based hardcoded cookie names Also skip prefetchUpdateChecks for WP < 5.0 (the functions it calls don't exist in old WP), skip 0-playground.php mu-plugin for WP < 3.0 (closures in hooks cause fatal errors), and set up wp_user_roles + usermeta when missing after install.
1Password's inline tooltip enters a tight inject/remove loop on Playground's sandboxed iframes, flickering the UI and generating thousands of cached chrome-extension inline-tooltip.css requests per second. Add data-1p-ignore to the username and password inputs to tell the extension to skip these fields entirely.
Replace the "throw if already acquired" assertion with a 1-concurrency Semaphore so parallel requests queue instead of failing. This makes maxPhpInstances=1 viable for contexts where concurrent HTTP requests hit the same PHP instance.
PHP 5.6's zend_compile_file uses mmap() to read source files into the lexer buffer, trusting fstat for the buffer size. When reading through PROXYFS on a secondary instance, fstat can return a stale size (e.g. a file rewritten from 570 to 538 bytes between MEMFS init and PROXYFS mount), causing the mmap buffer to extend past the real EOF. The parser reads garbage and reports "syntax error". PHP 7+ removed the mmap path from zend_stream_fixup entirely, falling back to a read-based path that reallocates on size mismatch — which is why only PHP 5.6 is affected. Add a withMmap option to proxyFileSystem() and set it to false for legacy PHP. Without stream_ops.mmap, Emscripten returns ENOSYS from mmap() and PHP uses the safe read-based fallback. The mmap patch was originally added for ICU (intl extension), which legacy PHP does not use.
The patchWpAdminRelativePaths function only patched require/require_once
statements with relative paths (./file.php, ../file.php) but not
include/include_once. Old WordPress versions (3.0-3.6) use include()
with relative paths in wp-admin files — e.g. include('./edit-post-rows.php')
and include('./admin-footer.php') in edit.php. Since PHP resolves these
relative to CWD (the document root in Playground, not wp-admin/), the
includes silently fail.
Extend all regex patterns to match include and include_once alongside
require and require_once, preserving the original keyword in the
replacement to maintain correct PHP semantics (include vs require).
The legacy-PHP db.php guard in 0-sqlite.php reads wp-content/db.php into a variable called $content to check for the @playground-managed marker. Because this preload runs via auto_prepend_file in global scope before every request, the $content global persists into WordPress code. WP 1.0 and 1.5's post.php initializes variables from $HTTP_POST_VARS/$HTTP_GET_VARS only when they are NOT already set (`if (!isset($$wpvar))`). Since $content was already set to the entire db.php source code by the preload, it was never overwritten with an empty string, causing the "Write Post" textarea to display hundreds of lines of PHP code. Rename the variable to $_pg_db_php and unset it after use.
runPostInstallLegacyFixups() was reading requestHandler.siteUrl to derive the Playground scope URL for updating wp_options.siteurl and wp_options.home. But PHPRequestHandler exposes the URL as absoluteUrl, not siteUrl. The result was an empty string, so the PLAYGROUND_SITE_URL env var was never set and the database retained the placeholder 'http://localhost'. WP 1.5+ was unaffected because it respects the WP_HOME/WP_SITEURL constants defined at boot time. WP 1.0 reads siteurl directly from the database, so it served all URLs (CSS, links) pointing at http://localhost instead of the scoped Playground URL.
…ermalinks
The placeholder_escape() polyfill for WordPress < 4.8.3 was missing the
add_filter('query', 'remove_placeholder_escape') call that modern WordPress
includes. Without it, the _real_escape() -> add_placeholder_escape() chain
replaced % characters with hash placeholders, but nothing restored them
before the query was executed. This caused permalink structure tokens like
%year%, %monthnum%, %day%, %postname% to be stored in the database as
{hash}year{hash}, {hash}monthnum{hash}, etc.
The fix adds the filter registration to match wpdb::placeholder_escape()
behavior in WordPress >= 4.8.3. The 'query' filter fires in
WP_SQLite_DB::query() via apply_filters('query', $query), restoring %
characters before the SQL reaches SQLite.
Affects WP 3.0–4.5 running on PHP 5.6 via the SQLite integration driver.
The regex patterns in patchWpAdminRelativePaths() only matched
single-quoted paths ('file.php'), missing double-quoted variants
("file.php") used in several WP 1.5-2.3 admin files such as
link-manager.php's include_once ("./admin-header.php").
Use ['"] with a backreference (\2) to match both quote styles.
Also remove two redundant "space before parens" patterns that
were subsets of the \s* patterns above them.
Two problems prevented auto-login on legacy WordPress: 1. The internal mu-plugins (including 1-auto-login.php) loaded via the muplugins_loaded hook, which only exists in WP 2.8+. On older WP, the hook never fires and the mu-plugins never load. 2. The auto-login code called wp_set_current_user(), wp_set_auth_cookie(), is_user_logged_in(), and get_user_by() — functions that don't exist in WP < 2.5. Fix #1: Register playground_load_mu_plugins on the init hook as a fallback (priority -1000 to run before other init callbacks). A static $loaded flag prevents double-loading when both hooks fire on WP 2.8+. Fix #2: For legacy PHP, the auto-login body now detects which auth API is available and uses the appropriate method: - WP 2.5+: standard wp_set_current_user() + wp_set_auth_cookie() - WP 1.5-2.4: USER_COOKIE/PASS_COOKIE with double-md5 password hash - WP 1.0-1.2: wordpressuser_/wordpresspass_ cookies + global user vars
The legacy admin auth mu-plugin (0-legacy-admin-auth.php) was calling wp_set_auth_cookie() on every admin request, which creates a new session token each time. WordPress 4.0+ embeds the session token in nonces, so when the token changes between form render and form submit, nonce verification fails with "Are you sure you want to do this?" The fix: - Check is_user_logged_in() before re-authenticating. If the user already has valid cookies from a prior request, do nothing. - Only call wp_set_auth_cookie() once — when the user is not yet logged in — so the session token remains stable across requests. - For WP < 4.0 (where wp_set_auth_cookie doesn't update $_COOKIE in-process), still populate $_COOKIE with wp_generate_auth_cookie() but only when $_COOKIE is empty, to satisfy auth_redirect(). Also adds the same is_user_logged_in() guard to the inline admin.php auth patch used for WP 2.0–2.7, avoiding unnecessary cookie overrides when the auto-login mu-plugin has already authenticated the user.
Setting permalink_structure via update_option() stores the desired URL pattern but doesn't generate the actual rewrite rules that WordPress needs to resolve pretty URLs. Without flushing, the rewrite_rules option stays empty and WordPress can't map URLs like /2026/04/10/hello-world/ back to the correct post query. Modern WordPress (6.x) auto-regenerates rules when the option is missing, but WP 3.0-4.5 requires an explicit flush_rewrite_rules() call. Add it to both the legacy and modern code paths for correctness.
Legacy WordPress (2.0–3.5) assigns properties on uninitialized variables ($obj->prop = value), which was valid in PHP 4 but triggers E_WARNING in PHP 5.6. These warnings appear on rendered pages across WP 2.0, 2.5, 3.0, and 3.5 in files like wp-includes/functions.php, wp-admin/includes/theme.php, and wp-admin/includes/post.php. The fix adds a targeted suppression in the Playground error handler for this specific warning message, matching the existing pattern used for other benign warnings. This avoids modifying WP core (downloaded as unmodified zip releases) and is more precise than lowering the error_reporting level broadly.
WP 2.5-2.7 admin-ajax.php loads wp-config.php directly (bypassing admin.php) and immediately checks is_user_logged_in(), dying with -1 if the user isn't authenticated. Since WP < 2.8 has no mu-plugin support, neither the Playground auth mu-plugin nor the admin.php auth patch apply to AJAX requests. The preload auto-login fires at init but skips re-authentication once the playground_auto_login_already_happened cookie is set. Auth cookies from the HttpCookieStore don't validate through WP 2.5's wp_validate_auth_cookie() because they were generated during the initial auto-login redirect, leaving AJAX requests without a valid session. Inject auth code (wp_set_current_user + $_COOKIE population) before the is_user_logged_in() gate in admin-ajax.php, matching the existing patchAdminAuthRedirect approach for admin.php. This fixes: - Post save (AJAX autosave returning "You do not have permission") - Plugin activation (nonce verification failures after redirect)
The boot-time flush_rewrite_rules() call ran in a standalone php.run() script that loaded wp-load.php but never fired WordPress init hooks. Post types and taxonomies weren't registered, so the generated rewrite rules were incomplete or empty. This caused pretty permalink URLs like /2026/04/10/hello-world/ to 404 on WP 2.5, 3.0, and 3.5. For WP < 3.0, flush_rewrite_rules() doesn't exist at all, so rewrite rules were never flushed. Move the flush to a mu-plugin that runs on 'init' at priority 99999, where WordPress is fully bootstrapped and all post types/taxonomies are registered. The mu-plugin: - Checks if permalink_structure is set but rewrite_rules is empty - Uses $wp_rewrite->flush_rules() which works on WP 1.5+ (unlike flush_rewrite_rules() which requires WP 3.0+) - Writes a flag file to avoid repeated flushes on subsequent requests The first real WordPress request triggers the flush before WP::parse_request() runs, so permalink resolution works immediately.
On WP < 4.8.3, wpdb::prepare() passes values through vsprintf() without escaping '%' characters first — the placeholder_escape mechanism was only added in WP 4.8.3. The permalink pattern '/%year%/%monthnum%/%day%/%postname%/' contains sequences like %y, %m, %d, %p that vsprintf() interprets as format specifiers, mangling the stored value to an empty or garbled string. Bypass wpdb entirely by writing permalink_structure directly to the SQLite database via PDO with parameterized queries. This fixes pretty permalinks on WP 2.5, 3.0, and 3.5.
Two issues prevented /?p=1 and pretty permalink URLs from resolving posts on legacy WordPress (2.5, 3.0, 3.5): 1. SQLite date function type mismatch: WordPress < 4.0 generates date queries with string comparisons like YEAR(post_date)='2026'. The SQLite driver's user-defined YEAR/MONTH/DAYOFMONTH functions return PHP integers. SQLite's strict type system treats integer != text (4 != '4'), so these WHERE clauses silently matched zero rows. Fix: a query filter in the preload strips quotes from numeric values in date function comparisons. 2. Preload hooks destroyed by wp-settings.php: WordPress 2.5–3.x explicitly unsets $wp_filter at boot to prevent register_globals interference. This destroyed the playground_load_mu_plugins hook set by the auto_prepend_file preload, so Playground's mu-plugins never loaded (including the rewrite-rules flusher and other essential boot-time fixes). Fix: patch wp-settings.php to remove $wp_filter from the unset() call. Also adds a guard to skip mu-plugins on WP < 2.8, where the hook system cannot handle closures and functions like site_url() don't exist yet.
Add three new test phases to the legacy WordPress boot test: - View single post: clicks "Hello world!" on the front page and verifies the post content loads (tests pretty permalinks) - New post page: navigates to post-new.php and verifies the editor loads without nonce errors - Plugin activation: clicks Activate on the first available plugin and verifies it activates without permission errors Extended tests run for WP 2.5+ (where the plugin/post UI exists). WP 1.0-2.3 still test front page, post viewing, and admin only.
WP 2.8-2.9's call_user_func_array() in plugin.php treats callbacks as arrays, which fatally crashes on Closure objects. Convert all anonymous functions registered via add_action()/add_filter() to named functions for compatibility: - playground_save_wp_env_info (wp_loaded) - playground_allowed_redirect_hosts (allowed_redirect_hosts) - playground_disable_admin_email_check (admin_email_check_interval)
Post view: use exact name matching for the "Hello world!" link to avoid clicking the "Edit Hello world!" admin bar link that appears first in DOM order on Twenty Seventeen themes. newPost: also check innerHTML for PHP errors to catch fatal errors hidden inside collapsed divs (WP 3.3's WP_Screen crash).
The auto-login mu-plugin (1-auto-login.php) was never loaded for WordPress versions before 2.8 because playground_load_mu_plugins() skipped all mu-plugins on these old versions. The blanket skip was intended to avoid crashes from closures and missing APIs, but the auto-login mu-plugin is specifically written for legacy WP and uses only named functions. Changes: - Allow 1-auto-login.php to load on WP < 2.8 by exempting it from the blanket mu-plugin skip in playground_load_mu_plugins() - Call playground_auto_login() and playground_auto_login_redirect_target() directly after loading the mu-plugin, because PHP 5.6's foreach iterates over a copy of the array, so add_action() calls inside the mu-plugin won't fire for the current init invocation - Add PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT flag to prevent the auto-login from sending Set-Cookie headers and redirects when called from the preload context. In Playground's service worker, Set-Cookie + redirect causes a loop because the cookie isn't applied before the redirected request fires. The legacy auto-login already populates $_COOKIE in-process, making the redirect unnecessary. - Widen the $wp_filter unset detection in patchWpSettingsPhp() to match more formatting variants (e.g. without space after open paren) - Add no-parentheses require/include path fixes for wp-admin files (e.g. require_once 'admin.php') used by WP 2.0
Three regressions introduced by 57d2491: 1. WP 2.6-2.7 fatal error: patchAdminAuthRedirect() called get_current_user_id() which doesn't exist before WP 2.8. Replace with wp_get_current_user()->ID (available since WP 2.0). 2. WP 2.5 admin login page: the PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT flag prevented wp_set_auth_cookie() from running, but the WP 2.5+ auto-login branch didn't populate $_COOKIE either. auth_redirect() in admin.php found no cookies and redirected to wp-login.php. Fix: when skip-redirect is active, generate auth cookies in-process via wp_generate_auth_cookie() and set them in $_COOKIE directly. 3. WP 2.1 front page 404: the home/siteurl DB options didn't include the Playground scope path. WP < 2.6 doesn't support WP_HOME/ WP_SITEURL constants, so parse_request() couldn't strip the scope prefix from REQUEST_URI. The remaining path matched no rewrite rule, triggering a 404. Fix: always update siteurl/home in postInstallLegacyFixups when the DB value doesn't match PLAYGROUND_SITE_URL, not just when it equals 'http://localhost'.
WordPress 2.0 and earlier don't natively override get_option('siteurl')
with the WP_SITEURL constant — that check was added in WP 2.2. Without
it, get_settings('siteurl') returns the raw DB value, which can differ
from the Playground's scoped URL. This causes all admin navigation links
to point to the wrong URL (missing the /scope:xxx/ prefix), breaking
navigation in WP 2.0's admin dashboard.
Add option_siteurl and option_home filters in the legacy PHP preload
that return the WP_SITEURL / WP_HOME constant values when defined. This
replicates the behavior that modern WordPress provides natively.
WP 1.0's apply_filters() expects $wp_filter[$tag] to be a flat array
of function name strings. WP 1.2 expects $wp_filter[$tag][$priority]
to hold plain function name strings. WP 1.5+ expects the associative
array format with 'function' and 'accepted_args' keys.
Our playground_add_filter() was using the WP 1.5+ format
unconditionally, which crashed WP 1.0 ("Function name must be a
string") and WP 1.2 ("Array callback has to contain indices 0 and 1")
when the SQLite integration's wpdb called apply_filters('query', ...).
Detect the WordPress version from wp-includes/version.php at runtime
and store hooks in the format the target version expects.
Two fixes for legacy WordPress admin in the Playground environment: 1. Bypass check_admin_referer() on WP < 2.5. The function checks $_SERVER['HTTP_REFERER'] against the siteurl, but the Referer header is missing or incorrect in Playground's service worker. This caused plugin activation (and other admin actions) to fail with "you need to enable sending referrers". WP 2.5+ uses nonce-based verification and is unaffected. 2. Fix empty posts listing on the WP 1.5 dashboard. The recent posts query includes "AND post_date_gmt < '$today'" which fails in SQLite, hiding the Posts section entirely. The condition is redundant since post_status = 'publish' already excludes scheduled posts on WP 1.x.
The regex in patchCheckAdminReferer() used [^}]* which cannot match
past the closing brace of an inner if-block. WP 1.2 wraps its die()
call in `if (...) { die(...); }` (braced), while WP 1.5 uses a
braceless `if (...) die(...)`. The old regex stopped at the if-block's
closing brace, leaving the function's closing brace as orphaned syntax
and producing a parse error on line 377.
Replace [^}]* with (?:[^{}]|\{[^}]*\})* to allow one level of brace
nesting, correctly matching both WP 1.2 and WP 1.5 function bodies.
Remove the EXTENDED_TEST_VERSIONS gate that skipped WP < 2.5. All versions from 1.0 to 4.9 now run all 5 test phases. Use post.php for WP < 2.5 (post-new.php didn't exist yet).
WP 1.5's Magpie RSS library calls error() as a standalone function from fetch_rss() and _response_to_rss(), but error() is only defined as a method on the RSSCache class. In Playground, outbound HTTP always fails, so every fetch_rss() call hits the error path and causes a fatal "Call to undefined function error()" that @ cannot suppress. This killed the dashboard rendering mid-page — everything after the Blog Stats section (which precedes the first fetch_rss() call) was missing, including the Dashboard heading and admin footer. Patch rss-functions.php to define the missing global error() function as a no-op stub (matching the RSSCache::error() behavior when MAGPIE_DEBUG is off).
Member
Author
|
Closing in favor of #3490. |
adamziel
pushed a commit
that referenced
this pull request
Apr 29, 2026
> **Stacked on #3501** (Add PHP 5.2 WebAssembly builds and runtime support). Review that PR first. ## Summary Boot legacy WordPress (1.0 – 6.2) in the browser, with SQLite as the database. <img width="743" height="586" alt="Screenshot 2026-04-15 at 13 30 53" src="https://github.com/user-attachments/assets/1c42ebab-cfb8-4968-88e8-b8a628a42515" /> 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: - **Offline PHP 7+ → 5.2 downgrader** (`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. - **SQLite plugin patcher** (`scripts/patch-sqlite-for-php52.mjs`) — produces a PHP 5.2-compatible build of the upstream SQLite integration plugin. - **Legacy WordPress boot flow** (`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.php` SQLite compatibility, and more. - **WP 5.0–6.2 on PHP 7.4** — extends the legacy boot to cover the mid-modern range where PHP 8.x is unreliable. - **Site settings UI** — "Include older versions" checkbox + grouped dropdown + automatic PHP locking: WP 1.0 – 4.9 → PHP 5.2, WP 5.0 – 6.2 → PHP 7.4, WP 6.3+ → default. - **Release code names** — WP version picker shows jazz-musician shorthand (e.g. "6.9 (Gene)", "4.9 (Tipton)"). - **TinyMCE content_css fix** — inlines editor CSS via `content_style` to work around TinyMCE's `document.open()` creating a service-worker-uncontrolled iframe. ## Test plan ### Automated (Playwright, all 32 legacy versions) ```bash npm run dev # Wait for http://localhost:5400/website-server/ to be ready node packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs ``` Covers 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 1. `npm run dev` and open http://localhost:5400/. 2. Open the settings gear → tick **Include older versions**. 3. Verify the WP dropdown shows two groups and every release from 1.0 to 6.9. 4. Pick **WP 4.9** → PHP locks to 5.2 (disabled). 5. Pick **WP 5.5** → PHP locks to 7.4 (disabled). 6. Pick **WP 4.8**, go to Posts → Add New, verify TinyMCE loads without CSS errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Run old WordPress versions (1.5 through 4.9 tested, any existing release should work) in the browser using PHP 5.6 WASM with SQLite as the database backend.
??,Throwable, attributes,Closure::call()) to PHP 5.6, used to patch the SQLite Database Integration plugin. Guards nullsafe operators (?->) from corruption.reinitialize_sqlite(),placeholder_escape(), etc.) and guards for old WP functionswp-load.phpshim for WP < 2.0,extension_loaded('mysql')bypass, lazy$wpdbloader withreinitialize_sqlite(), MySQL/MySQLi function stubs, WP 1.5 seed data insertion (SQLite NOT NULL constraint fix)?wp=4.9resolves to a wordpress.org download automatically (versions < 2.0 are normalized: 1.0→1.0.2, 1.2→1.2.2, 1.5→1.5.2)Tested with WP 1.2 through 4.9 (31 versions).
Test plan
Automated (Playwright)
Requires Chrome/Chromium with JSPI support (Chrome 131+).
Manual (browser)
npm run devUnit tests
npx nx test playground-wordpress --testFile=legacy-php-compat.spec.tsRuns 28 tests covering the PHP 5.6 transpiler functions (
stripPhp7TypeDeclarations,replaceNullCoalescing,replacePhp7ErrorClasses).Known issues
enum()column typespreg_replace /e modifier(WP 1.5 code predates PHP 5.4/5.5)prefetchUpdateChecksfails on all legacy WP versions (non-fatal)assertValidDatabaseConnectionalways fails for legacy PHP (treated as non-fatal)vite.config.tsneeds updating to copy version-specific ZIPsv2.1.16SQLite selection for WP < 6.x on modern PHP