diff --git a/packages/playground/cli/src/setup-php.ts b/packages/playground/cli/src/setup-php.ts index b4adac3997..ca64824e18 100644 --- a/packages/playground/cli/src/setup-php.ts +++ b/packages/playground/cli/src/setup-php.ts @@ -7,7 +7,11 @@ import { } from '@php-wasm/universal'; import { rootCertificates } from 'tls'; import { dirname } from '@php-wasm/util'; -import { envPHP_to_loadMuPlugins } from '@wp-playground/wordpress'; +import { + preloadPhpInfoRoute, + enablePlatformMuPlugins, + preloadRequiredMuPlugin, +} from '@wp-playground/wordpress'; export async function createPhp( requestHandler: PHPRequestHandler, @@ -44,20 +48,9 @@ export async function createPhp( '/internal/shared/ca-bundle.crt', rootCertificates.join('\n') ); - php.writeFile( - '/internal/shared/preload/env.php', - envPHP_to_loadMuPlugins - ); - php.writeFile( - '/internal/shared/preload/phpinfo.php', - ` { - const php = await createPhp(requestHandler); - php.defineConstant('SCOPED_SITE_PATH', new URL(scopedSiteUrl).pathname); - if (isPrimary) { - php.writeFile( - '/internal/shared/preload/env.php', - envPHP_to_loadMuPlugins - ); - php.writeFile( - '/internal/shared/preload/phpinfo.php', - ` + await createPhp(requestHandler, scopedSiteUrl, isPrimary), documentRoot: DOCROOT, absoluteUrl: scopedSiteUrl, rewriteRules: wordPressRewriteRules, -}); +}) as PHPRequestHandler; const apiEndpoint = new PlaygroundWorkerEndpoint( requestHandler, downloadMonitor, @@ -241,18 +202,6 @@ try { }); } - // Always install the playground mu-plugin, even if WordPress is loaded - // from the OPFS. This ensures: - // * The mu-plugin is always there, even when a custom WordPress directory - // is mounted. - // * The mu-plugin is always up to date. - await writeFiles(primaryPhp, joinPaths('/internal/shared/mu-plugins'), { - '0-playground.php': playgroundMuPlugin, - '1-playground-web.php': playgroundWebMuPlugin, - 'playground-includes/wp_http_dummy.php': transportDummy, - 'playground-includes/wp_http_fetch.php': transportFetch, - }); - if (virtualOpfsDir) { await bindOpfs({ php: primaryPhp, diff --git a/packages/playground/remote/src/lib/worker-utils.ts b/packages/playground/remote/src/lib/worker-utils.ts index 4f58598be4..f5cd64addd 100644 --- a/packages/playground/remote/src/lib/worker-utils.ts +++ b/packages/playground/remote/src/lib/worker-utils.ts @@ -10,11 +10,24 @@ import { SupportedPHPVersionsList, rotatePHPRuntime, PHPRequestHandler, + proxyFileSystem, + writeFiles, } from '@php-wasm/universal'; import { EmscriptenDownloadMonitor } from '@php-wasm/progress'; -import { createSpawnHandler, phpVar } from '@php-wasm/util'; +import { createSpawnHandler, joinPaths, phpVar } from '@php-wasm/util'; import { createMemoizedFetch } from './create-memoized-fetch'; import { logger } from '@php-wasm/logger'; +/** @ts-ignore */ +import transportFetch from './playground-mu-plugin/playground-includes/wp_http_fetch.php?raw'; +/** @ts-ignore */ +import transportDummy from './playground-mu-plugin/playground-includes/wp_http_dummy.php?raw'; +/** @ts-ignore */ +import playgroundWebMuPlugin from './playground-mu-plugin/0-playground.php?raw'; +import { + enablePlatformMuPlugins, + preloadPhpInfoRoute, + preloadRequiredMuPlugin, +} from '@wp-playground/wordpress'; export type ReceivedStartupOptions = { wpVersion?: string; @@ -59,15 +72,42 @@ export const startupOptions = { phpExtensions: receivedParams.phpExtensions || [], } as ParsedStartupOptions; -export async function createPhp(requestHandler: PHPRequestHandler) { +export async function createPhp( + requestHandler: PHPRequestHandler, + siteUrl: string, + isPrimary: boolean +) { const php = new WebPHP(); php.requestHandler = requestHandler as any; + php.initializeRuntime(await createPhpRuntime()); - php.setPhpIniEntry('memory_limit', '256M'); if (startupOptions.sapiName) { await php.setSapiName(startupOptions.sapiName); } + php.setPhpIniEntry('memory_limit', '256M'); php.setSpawnHandler(spawnHandlerFactory(requestHandler.processManager)); + + if (isPrimary) { + const scopedSitePath = new URL(siteUrl).pathname; + await preloadPhpInfoRoute( + php, + joinPaths(scopedSitePath, 'phpinfo.php') + ); + await enablePlatformMuPlugins(php); + await preloadRequiredMuPlugin(php); + await writeFiles(php, joinPaths('/internal/shared/mu-plugins'), { + '1-playground-web.php': playgroundWebMuPlugin, + 'playground-includes/wp_http_dummy.php': transportDummy, + 'playground-includes/wp_http_fetch.php': transportFetch, + }); + } else { + proxyFileSystem(await requestHandler.getPrimaryPhp(), php, [ + '/tmp', + requestHandler.documentRoot, + '/internal/shared', + ]); + } + // Rotate the PHP runtime periodically to avoid memory leak-related crashes. // @see https://github.com/WordPress/wordpress-playground/pull/990 for more context rotatePHPRuntime({ diff --git a/packages/playground/wordpress/src/index.ts b/packages/playground/wordpress/src/index.ts index ce1a4fee92..0889a7a590 100644 --- a/packages/playground/wordpress/src/index.ts +++ b/packages/playground/wordpress/src/index.ts @@ -4,111 +4,147 @@ import { unzip } from '@wp-playground/blueprints'; export * from './rewrite-rules'; -export const RecommendedPHPVersion = '8.0'; +/** + * Preloads the platform mu-plugins from /internal/shared/mu-plugins. + * This avoids polluting the WordPress installation with mu-plugins + * that are only needed in the Playground environment. + * + * @param php + */ +export async function enablePlatformMuPlugins(php: UniversalPHP) { + await php.mkdir('/internal/shared/mu-plugins'); + await php.writeFile( + '/internal/shared/preload/env.php', + ` $function_to_add, 'accepted_args' => $accepted_args); + } + function playground_add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { + playground_add_filter( $tag, $function_to_add, $priority, $accepted_args ); + } + + // Load our mu-plugins after customer mu-plugins + // NOTE: this means our mu-plugins can't use the muplugins_loaded action! + playground_add_action( 'muplugins_loaded', 'playground_load_mu_plugins', 0 ); + function playground_load_mu_plugins() { + // Load all PHP files from /internal/shared/mu-plugins, sorted by filename + $mu_plugins_dir = '/internal/shared/mu-plugins'; + if(!is_dir($mu_plugins_dir)){ + return; + } + $mu_plugins = glob( $mu_plugins_dir . '/*.php' ); + sort( $mu_plugins ); + foreach ( $mu_plugins as $mu_plugin ) { + require_once $mu_plugin; + } + } + ` + ); +} -export const envPHP_to_loadMuPlugins = ``, + allowRedirectHosts: ``, + supportPermalinksWithoutIndexPhp: ``, + createFontsDirectory: ``, + silenceUnfixableErrors: ` + * Warning: strtotime(): Epoch doesn't fit in a PHP integer in + */ + if (strpos($message, "fit in a PHP integer") !== false) { + return; + } + /** + * Don't complain about network errors when not connected to the network. + */ + if ( + ( + ! defined('USE_FETCH_FOR_REQUESTS') || + ! USE_FETCH_FOR_REQUESTS + ) && + strpos($message, "WordPress could not establish a secure connection to WordPress.org") !== false) + { + return; + } + return false; + }); + ?>`, + configureErrorLogging: ``, + }; -// Allow adding filters/actions prior to loading WordPress. -// $function_to_add MUST be a string. -function playground_add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { - global $wp_filter; - $wp_filter[$tag][$priority][$function_to_add] = array('function' => $function_to_add, 'accepted_args' => $accepted_args); -} -function playground_add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { - playground_add_filter( $tag, $function_to_add, $priority, $accepted_args ); + const playgroundMuPlugin = Object.values(specificMuPlugins) + .map((p) => p.trim()) + .join('\n'); + await php.writeFile( + '/internal/shared/mu-plugins/0-playground.php', + playgroundMuPlugin + ); } -// Load our mu-plugins after customer mu-plugins -// NOTE: this means our mu-plugins can't use the muplugins_loaded action! -playground_add_action( 'muplugins_loaded', 'playground_load_mu_plugins', 0 ); -function playground_load_mu_plugins() { - // Load all PHP files from /internal/shared/mu-plugins, sorted by filename - $mu_plugins_dir = '/internal/shared/mu-plugins'; - if(!is_dir($mu_plugins_dir)){ - return; - } - $mu_plugins = glob( $mu_plugins_dir . '/*.php' ); - sort( $mu_plugins ); - foreach ( $mu_plugins as $mu_plugin ) { - require_once $mu_plugin; +/** + * Runs phpinfo() when the requested path is /phpinfo.php. + */ +export async function preloadPhpInfoRoute( + php: UniversalPHP, + requestPath = '/phpinfo.php' +) { + await php.writeFile( + '/internal/shared/preload/phpinfo.php', + ``, - allowRedirectHosts: ``, - supportPermalinksWithoutIndexPhp: ``, - createFontsDirectory: ``, - silenceUnfixableErrors: ` - * Warning: strtotime(): Epoch doesn't fit in a PHP integer in - */ - if (strpos($message, "fit in a PHP integer") !== false) { - return; - } - /** - * Don't complain about network errors when not connected to the network. - */ - if ( - ( - ! defined('USE_FETCH_FOR_REQUESTS') || - ! USE_FETCH_FOR_REQUESTS - ) && - strpos($message, "WordPress could not establish a secure connection to WordPress.org") !== false) - { - return; - } - return false; - }); - ?>`, - configureErrorLogging: ``, -}; - -export const playgroundMuPlugin = Object.values(specificMuPlugins) - .map((p) => p.trim()) - .join('\n'); export async function preloadSqliteIntegration( php: UniversalPHP, @@ -147,59 +183,59 @@ export async function preloadSqliteIntegration( await php.writeFile( `/internal/shared/preload/0-sqlite.php`, `load_sqlite_integration(); - if($GLOBALS['wpdb'] === $this) { - throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); - } - return call_user_func_array( - array($GLOBALS['wpdb'], $name), - $arguments - ); - } - public function __get($name) { - $this->load_sqlite_integration(); - if($GLOBALS['wpdb'] === $this) { - throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); - } - return $GLOBALS['wpdb']->$name; - } - public function __set($name, $value) { - $this->load_sqlite_integration(); - if($GLOBALS['wpdb'] === $this) { - throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); - } - $GLOBALS['wpdb']->$name = $value; - } - protected function load_sqlite_integration() { - require_once ${phpVar(SQLITE_MUPLUGIN_PATH)}; - } +/** + * Loads the SQLite integration plugin before WordPress is loaded + * and without creating a drop-in "db.php" file. + * + * Technically, it creates a global $wpdb object whose only two + * purposes are to: + * + * * Exist – because the require_wp_db() WordPress function won't + * connect to MySQL if $wpdb is already set. + * * Load the SQLite integration plugin the first time it's used + * and replace the global $wpdb reference with the SQLite one. + * + * This lets Playground keep the WordPress installation clean and + * solves dillemas like: + * + * * Should we include db.php in Playground exports? + * * Should we remove db.php from Playground imports? + * * How should we treat stale db.php from long-lived OPFS sites? + * + * @see https://github.com/WordPress/wordpress-playground/discussions/1379 for + * more context. + */ +class Playground_SQLite_Integration_Loader { + public function __call($name, $arguments) { + $this->load_sqlite_integration(); + if($GLOBALS['wpdb'] === $this) { + throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); + } + return call_user_func_array( + array($GLOBALS['wpdb'], $name), + $arguments + ); + } + public function __get($name) { + $this->load_sqlite_integration(); + if($GLOBALS['wpdb'] === $this) { + throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); + } + return $GLOBALS['wpdb']->$name; + } + public function __set($name, $value) { + $this->load_sqlite_integration(); + if($GLOBALS['wpdb'] === $this) { + throw new Exception('Infinite loop detected in $wpdb – SQLite integration plugin could not be loaded'); + } + $GLOBALS['wpdb']->$name = $value; + } + protected function load_sqlite_integration() { + require_once ${phpVar(SQLITE_MUPLUGIN_PATH)}; } - $wpdb = $GLOBALS['wpdb'] = new Playground_SQLite_Integration_Loader(); - ` +} +$wpdb = $GLOBALS['wpdb'] = new Playground_SQLite_Integration_Loader(); + ` ); /** * Ensure the SQLite integration is loaded and clearly communicate @@ -209,11 +245,11 @@ export async function preloadSqliteIntegration( await php.writeFile( `/internal/shared/mu-plugins/sqlite-test.php`, `