From d6a90afd9567bd7a5eaa6010193dc471788911cc Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Thu, 20 Jun 2024 20:40:03 -0400 Subject: [PATCH] Detect actual, loaded WP version (#1503) ## Motivation for the change, related issues When loading from browser storage, Playground can incorrectly assume the loaded WP version is the default version when it is actually something else. This can cause issues where remote assets are requested from the wrong version of WP. In addition, this work is needed to support a follow-up PR where we retrieve the `wordpress-remote-asset-paths` listing from the website when it is not already present within browser storage. ## Implementation details This PR adds two functions, one for detecting the version using `wp-includes/version.php`, and another for determining whether the detected version is one of the WP versions supported through the Playground web app. These functions are used when initializing worker threads to detect the version of WP that was booted. If detection fails, the WordPress version is left set to the default version. ## Testing Instructions (or ideally a Blueprint) - CI, including new unit tests --- packages/playground/remote/service-worker.ts | 13 ++- .../remote/src/lib/worker-thread.ts | 49 ++++++++-- packages/playground/wordpress/src/index.ts | 4 + .../wordpress/src/test/version-detect.spec.ts | 97 +++++++++++++++++++ .../wordpress/src/version-detect.ts | 50 ++++++++++ 5 files changed, 200 insertions(+), 13 deletions(-) create mode 100644 packages/playground/wordpress/src/test/version-detect.spec.ts create mode 100644 packages/playground/wordpress/src/version-detect.ts diff --git a/packages/playground/remote/service-worker.ts b/packages/playground/remote/service-worker.ts index 54547408a2..cf2cad94ba 100644 --- a/packages/playground/remote/service-worker.ts +++ b/packages/playground/remote/service-worker.ts @@ -49,13 +49,20 @@ initializeServiceWorker({ return emptyHtml(); } - const { staticAssetsDirectory } = await getScopedWpDetails(scope!); - const workerResponse = await convertFetchEventToPHPRequest(event); if ( workerResponse.status === 404 && workerResponse.headers.get('x-file-type') === 'static' ) { + const { staticAssetsDirectory } = await getScopedWpDetails( + scope! + ); + if (!staticAssetsDirectory) { + const plain404Response = workerResponse.clone(); + plain404Response.headers.delete('x-file-type'); + return plain404Response; + } + // If we get a 404 for a static file, try to fetch it from // the from the static assets directory at the remote server. const requestedUrl = new URL(event.request.url); @@ -230,7 +237,7 @@ function emptyHtml() { } type WPModuleDetails = { - staticAssetsDirectory: string; + staticAssetsDirectory?: string; }; const scopeToWpModule: Record = {}; diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index 007df12cf2..4ec50aae74 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -35,7 +35,12 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d /** @ts-ignore */ import playgroundWebMuPlugin from './playground-mu-plugin/0-playground.php?raw'; import { PHPWorker } from '@php-wasm/universal'; -import { bootWordPress } from '@wp-playground/wordpress'; +import { + bootWordPress, + getLoadedWordPressVersion, + isSupportedWordPressVersion, +} from '@wp-playground/wordpress'; +import { logger } from '@php-wasm/logger'; const scope = Math.random().toFixed(16); @@ -102,18 +107,23 @@ export class PlaygroundWorkerEndpoint extends PHPWorker { scope: string; /** - * A string representing the version of WordPress being used. + * A string representing the requested version of WordPress. + */ + requestedWordPressVersion: string; + + /** + * A string representing the version of WordPress that was loaded. */ - wordPressVersion: string; + loadedWordPressVersion: string | undefined; constructor( monitor: EmscriptenDownloadMonitor, scope: string, - wordPressVersion: string + requestedWordPressVersion: string ) { super(undefined, monitor); this.scope = scope; - this.wordPressVersion = wordPressVersion; + this.requestedWordPressVersion = requestedWordPressVersion; } /** @@ -121,11 +131,13 @@ export class PlaygroundWorkerEndpoint extends PHPWorker { */ async getWordPressModuleDetails() { return { - majorVersion: this.wordPressVersion, - staticAssetsDirectory: `wp-${this.wordPressVersion.replace( - '_', - '.' - )}`, + majorVersion: + this.loadedWordPressVersion || this.requestedWordPressVersion, + staticAssetsDirectory: + this.loadedWordPressVersion && + isSupportedWordPressVersion(this.loadedWordPressVersion) + ? `wp-${this.loadedWordPressVersion}` + : undefined, }; } @@ -246,6 +258,23 @@ try { const primaryPhp = await requestHandler.getPrimaryPhp(); await apiEndpoint.setPrimaryPHP(primaryPhp); + // NOTE: We need to derive the loaded WP version or we might assume WP loaded + // from browser storage is the default version when it is actually something else. + // Incorrectly assuming WP version can break things like remote asset retrieval + // for minified WP builds. + apiEndpoint.loadedWordPressVersion = await getLoadedWordPressVersion( + requestHandler + ); + if ( + apiEndpoint.requestedWordPressVersion !== + apiEndpoint.loadedWordPressVersion + ) { + logger.warn( + `Loaded WordPress version (${apiEndpoint.loadedWordPressVersion}) differs ` + + `from requested version (${apiEndpoint.requestedWordPressVersion}).` + ); + } + setApiReady(); } catch (e) { setAPIError(e as Error); diff --git a/packages/playground/wordpress/src/index.ts b/packages/playground/wordpress/src/index.ts index 9948b50b4d..2fbdb2c0a9 100644 --- a/packages/playground/wordpress/src/index.ts +++ b/packages/playground/wordpress/src/index.ts @@ -2,6 +2,10 @@ import { PHP, UniversalPHP } from '@php-wasm/universal'; import { joinPaths, phpVar } from '@php-wasm/util'; import { unzipFile } from '@wp-playground/common'; export { bootWordPress } from './boot'; +export { + getLoadedWordPressVersion, + isSupportedWordPressVersion, +} from './version-detect'; export * from './rewrite-rules'; diff --git a/packages/playground/wordpress/src/test/version-detect.spec.ts b/packages/playground/wordpress/src/test/version-detect.spec.ts new file mode 100644 index 0000000000..6a7a64b1c5 --- /dev/null +++ b/packages/playground/wordpress/src/test/version-detect.spec.ts @@ -0,0 +1,97 @@ +import { + SupportedWordPressVersions, + getSqliteDatabaseModule, + getWordPressModule, +} from '@wp-playground/wordpress-builds'; +import { RecommendedPHPVersion } from '@wp-playground/common'; +import { loadNodeRuntime } from '@php-wasm/node'; +import { bootWordPress } from '../boot'; +import { + getLoadedWordPressVersion, + versionStringToLoadedWordPressVersion, +} from '../version-detect'; + +describe('Test WP version detection', async () => { + for (const expectedWordPressVersion of Object.keys( + SupportedWordPressVersions + )) { + it(`detects WP ${expectedWordPressVersion} at runtime`, async () => { + const handler = await bootWordPress({ + createPhpRuntime: async () => + await loadNodeRuntime(RecommendedPHPVersion), + siteUrl: 'http://playground-domain/', + wordPressZip: await getWordPressModule( + expectedWordPressVersion + ), + sqliteIntegrationPluginZip: await getSqliteDatabaseModule(), + }); + const loadedWordPressVersion = await getLoadedWordPressVersion( + handler + ); + expect(loadedWordPressVersion).to.equal(expectedWordPressVersion); + }); + } + + it('errors when unable to read version at runtime', async () => { + const handler = await bootWordPress({ + createPhpRuntime: async () => + await loadNodeRuntime(RecommendedPHPVersion), + siteUrl: 'http://playground-domain/', + wordPressZip: await getWordPressModule(), + sqliteIntegrationPluginZip: await getSqliteDatabaseModule(), + }); + const php = await handler.getPrimaryPhp(); + + php.unlink(`${handler.documentRoot}/wp-includes/version.php`); + const detectionResult = await getLoadedWordPressVersion(handler).then( + () => 'no-error', + () => 'error' + ); + expect(detectionResult).to.equal('error'); + }); + + it('errors on reading empty version at runtime', async () => { + const handler = await bootWordPress({ + createPhpRuntime: async () => + await loadNodeRuntime(RecommendedPHPVersion), + siteUrl: 'http://playground-domain/', + wordPressZip: await getWordPressModule(), + sqliteIntegrationPluginZip: await getSqliteDatabaseModule(), + }); + const php = await handler.getPrimaryPhp(); + + php.writeFile( + `${handler.documentRoot}/wp-includes/version.php`, + ' 'no-error', + () => 'error' + ); + expect(detectionResult).to.equal('error'); + }); + + const versionMap = { + '6.3': '6.3', + '6.4.2': '6.4', + '6.5': '6.5', + '6.5.4': '6.5', + '6.6-alpha-57783': 'nightly', + '6.6-beta-57783': 'nightly', + '6.6-RC-54321': 'nightly', + '6.6-RC2-12345': 'nightly', + '6.6-beta': 'beta', + '6.6-beta2': 'beta', + '6.6-RC': 'beta', + '6.6-RC2': 'beta', + 'custom-version': 'custom-version', + }; + + for (const [input, expected] of Object.entries(versionMap)) { + it(`maps '${input}' to '${expected}'`, () => { + const result = versionStringToLoadedWordPressVersion(input); + expect(result).to.equal(expected); + }); + } +}); diff --git a/packages/playground/wordpress/src/version-detect.ts b/packages/playground/wordpress/src/version-detect.ts new file mode 100644 index 0000000000..2c1762f688 --- /dev/null +++ b/packages/playground/wordpress/src/version-detect.ts @@ -0,0 +1,50 @@ +import type { PHPRequestHandler } from '@php-wasm/universal'; +import { SupportedWordPressVersions } from '@wp-playground/wordpress-builds'; + +export async function getLoadedWordPressVersion( + requestHandler: PHPRequestHandler +): Promise { + const php = await requestHandler.getPrimaryPhp(); + const result = await php.run({ + code: `