Skip to content

Commit

Permalink
Detect actual, loaded WP version (#1503)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
brandonpayton authored Jun 21, 2024
1 parent 1ed4c01 commit d6a90af
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 13 deletions.
13 changes: 10 additions & 3 deletions packages/playground/remote/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -230,7 +237,7 @@ function emptyHtml() {
}

type WPModuleDetails = {
staticAssetsDirectory: string;
staticAssetsDirectory?: string;
};

const scopeToWpModule: Record<string, WPModuleDetails> = {};
Expand Down
49 changes: 39 additions & 10 deletions packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -102,30 +107,37 @@ 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;
}

/**
* @returns WordPress module details, including the static assets directory and default theme.
*/
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,
};
}

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/playground/wordpress/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
97 changes: 97 additions & 0 deletions packages/playground/wordpress/src/test/version-detect.spec.ts
Original file line number Diff line number Diff line change
@@ -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`,
'<?php $wp_version = "";'
);

const detectionResult = await getLoadedWordPressVersion(handler).then(
() => '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);
});
}
});
50 changes: 50 additions & 0 deletions packages/playground/wordpress/src/version-detect.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
const php = await requestHandler.getPrimaryPhp();
const result = await php.run({
code: `<?php
require '${requestHandler.documentRoot}/wp-includes/version.php';
echo $wp_version;
`,
});

const versionString = result.text;
if (!versionString) {
throw new Error('Unable to read loaded WordPress version.');
}

return versionStringToLoadedWordPressVersion(versionString);
}

export function versionStringToLoadedWordPressVersion(
wpVersionString: string
): string {
const nightlyPattern = /-(alpha|beta|RC)\d*-\d+$/;
if (nightlyPattern.test(wpVersionString)) {
return 'nightly';
}

// TODO: Tighten this to detect specific old beta version, like 6.2-beta.
const betaPattern = /-(beta|RC)\d*$/;
if (betaPattern.test(wpVersionString)) {
return 'beta';
}

const majorMinorMatch = wpVersionString.match(/^(\d+\.\d+)(?:\.\d+)?$/);
if (majorMinorMatch !== null) {
return majorMinorMatch[1];
}

// Return original version string if we could not parse it.
// This is important to allow so folks can bring their own WP builds.
return wpVersionString;
}

export function isSupportedWordPressVersion(wpVersion: string) {
const supportedVersionKeys = Object.keys(SupportedWordPressVersions);
return supportedVersionKeys.includes(wpVersion);
}

0 comments on commit d6a90af

Please sign in to comment.