From a5fff0216786ce87196bcfe8200be3a600e8a890 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Wed, 8 Nov 2023 12:49:55 +0100 Subject: [PATCH] Add network support via fetch() (#724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request introduces network support to the WordPress Playground via the JavaScript fetch() API. By adding this support, the playground will have the ability to execute HTTP requests, paving the way for enhanced interactions with various APIs and services. The implementation involves creating a custom transport that delegates HTTP requests to the fetch() API. This custom transport handles network requests, thus enabling communication outside the local environment. Local testing will be a bit challenging until I deploy the updated plugin-proxy.php to playground.wordpress.net CleanShot 2023-10-26 at 00 29 07@2x ## How does it work? A new Requests transport for WordPress asks JavaScript to run fetch() via `post_message_to_js()`. JavaScript runs the request and returns the response ľ see https://github.com/WordPress/wordpress-playground/pull/732 for more details of that mechanism ## Testing Instructions: Testing this feature locally might be challenging until the updated plugin-proxy.php is deployed to the playground environment. Here are some basic steps to test the changes: 1. Open local Playground 2. Set up an mu-plugin that calls `wp_safe_remote_get()` to domain not on this list: ['api.wordpress.org', 'w.org', 's.w.org'] 3. Ensure that the requests are handled correctly and the expected responses are received. 4. Test various types of requests (GET, POST, etc.) and verify the correct response. 5. Check the functionality with different APIs (e.g., REST API, external APIs). cc @akirk @dmsnell @ellatrix --- .../docs/site/docs/08-query-api/01-index.md | 3 +- .../docs/09-blueprints-api/03-data-format.md | 9 + .../universal/src/lib/universal-php.ts | 2 +- .../blueprints/public/blueprint-schema.json | 2525 ++++++++--------- .../blueprints/src/lib/blueprint.ts | 4 + .../playground/blueprints/src/lib/compile.ts | 8 + .../steps/apply-wordpress-patches/index.ts | 50 +- .../mu-plugins/add_requests_transport.php | 19 +- .../includes/requests_transport_fetch.php | 60 +- packages/playground/client/src/index.ts | 1 + .../remote/src/lib/boot-playground-remote.ts | 6 + .../src/lib/setup-fetch-network-transport.ts | 93 + .../setup-fetch-network-transport.spec.ts | 52 + .../website/public/plugin-proxy.php | 187 +- .../playground-configuration-group/index.tsx | 1 + .../website/src/lib/make-blueprint.tsx | 2 + packages/playground/website/src/main.tsx | 7 + packages/playground/website/vite.config.ts | 16 +- 18 files changed, 1589 insertions(+), 1456 deletions(-) create mode 100644 packages/playground/remote/src/lib/setup-fetch-network-transport.ts create mode 100644 packages/playground/remote/src/test/setup-fetch-network-transport.spec.ts diff --git a/packages/docs/site/docs/08-query-api/01-index.md b/packages/docs/site/docs/08-query-api/01-index.md index 53daab14d0..91a9b4cf6c 100644 --- a/packages/docs/site/docs/08-query-api/01-index.md +++ b/packages/docs/site/docs/08-query-api/01-index.md @@ -24,8 +24,9 @@ You can go ahead and try it out. The Playground will automatically install the t | Option | Default Value | Description | | ---------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `php` | `8.0` | Loads the specified PHP version. Supported values: `5.6`, `7.0`, `7.1`, `7.2`, `7.3`, `7.4`, `8.0`, `8.1`, `8.2`, `latest` | -| `wp` | `latest` | Loads the specified WordPress version. Supported values: `5.9`, `6.0`, `6.1`, `6.2`, `6.3`, `latest`, `nightly`, `beta` | +| `wp` | `latest` | Loads the specified WordPress version. Supported values: `5.9`, `6.0`, `6.1`, `6.2`, `6.3`, `latest`, `nightly`, `beta` | | `php-extension-bundle` | | Loads a bundle of PHP extensions. Supported bundles: `kitchen-sink` (for gd, mbstring, iconv, libxml, xml, dom, simplexml, xmlreader, xmlwriter) | +| `networking` | `yes` or `no` | Enables or disables the networking support for Playground. Defaults to `yes` | | `plugin` | | Installs the specified plugin. Use the plugin name from the plugins directory URL, e.g. for a URL like `https://wordpress.org/plugins/wp-lazy-loading/`, the plugin name would be `wp-lazy-loading`. You can pre-install multiple plugins by saying `plugin=coblocks&plugin=wp-lazy-loading&…`. Installing a plugin automatically logs the user in as an admin | | `theme` | | Installs the specified theme. Use the theme name from the themes directory URL, e.g. for a URL like `https://wordpress.org/themes/disco/`, the theme name would be `disco`. Installing a theme automatically logs the user in as an admin | | `url` | `/wp-admin/` | Load the specified initial page displaying WordPress | diff --git a/packages/docs/site/docs/09-blueprints-api/03-data-format.md b/packages/docs/site/docs/09-blueprints-api/03-data-format.md index a90a814dd6..eb8e22509a 100644 --- a/packages/docs/site/docs/09-blueprints-api/03-data-format.md +++ b/packages/docs/site/docs/09-blueprints-api/03-data-format.md @@ -22,6 +22,9 @@ import BlueprintExample from '@site/src/components/Blueprints/BlueprintExample.m "wp": "5.9" }, "phpExtensionBundles": ["kitchen-sink"], + "features": { + "networking": true + }, "steps": [ { "step": "login", @@ -55,3 +58,9 @@ The `preferredVersions` property, unsurprisingly, declares the preferred of PHP The `phpExtensionBundles` property is an array of PHP extension bundles to load. The following bundles are supported: - `kitchen-sink`: Installs `gd`, `mbstring`, `iconv`, `libxml`, `xml`, `dom`, `simplexml`, `xmlreader`, `xmlwriter` + +## Features + +The `features` property is used to enable or disable certain features of the Playground. It can contain the following properties: + +- `networking`: Defaults to `true`. Enables or disables the networking support for Playground. If enabled, `wp_safe_remote_get` and similar WordPress functions will actually use `fetch()` to make HTTP requests. If disabled, they will immediately fail instead. diff --git a/packages/php-wasm/universal/src/lib/universal-php.ts b/packages/php-wasm/universal/src/lib/universal-php.ts index 2ffa629900..42348d51e5 100644 --- a/packages/php-wasm/universal/src/lib/universal-php.ts +++ b/packages/php-wasm/universal/src/lib/universal-php.ts @@ -416,7 +416,7 @@ export interface IsomorphicLocalPHP extends RequestHandler { export type MessageListener = ( data: string -) => Promise | Promise | Promise | string | void; +) => Promise | string | void; interface EventEmitter { on(event: string, listener: (...args: any[]) => void): this; emit(event: string, ...args: any[]): boolean; diff --git a/packages/playground/blueprints/public/blueprint-schema.json b/packages/playground/blueprints/public/blueprint-schema.json index cb0433b122..562d8fbdf5 100644 --- a/packages/playground/blueprints/public/blueprint-schema.json +++ b/packages/playground/blueprints/public/blueprint-schema.json @@ -1,1322 +1,1205 @@ { - "$schema": "http://json-schema.org/schema", - "$ref": "#/definitions/Blueprint", - "definitions": { - "Blueprint": { - "type": "object", - "properties": { - "landingPage": { - "type": "string", - "description": "The URL to navigate to after the blueprint has been run." - }, - "preferredVersions": { - "type": "object", - "properties": { - "php": { - "anyOf": [ - { - "$ref": "#/definitions/SupportedPHPVersion" - }, - { - "type": "string", - "const": "latest" - } - ], - "description": "The preferred PHP version to use. If not specified, the latest supported version will be used" - }, - "wp": { - "type": "string", - "description": "The preferred WordPress version to use. If not specified, the latest supported version will be used" - } - }, - "required": [ - "php", - "wp" - ], - "additionalProperties": false, - "description": "The preferred PHP and WordPress versions to use." - }, - "phpExtensionBundles": { - "type": "array", - "items": { - "$ref": "#/definitions/SupportedPHPExtensionBundle" - }, - "description": "The PHP extensions to use." - }, - "steps": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/StepDefinition" - }, - { - "type": "string" - }, - { - "not": {} - }, - { - "type": "boolean", - "const": false - }, - { - "type": "null" - } - ] - }, - "description": "The steps to run." - }, - "$schema": { - "type": "string" - } - }, - "additionalProperties": false - }, - "SupportedPHPVersion": { - "type": "string", - "enum": [ - "8.2", - "8.1", - "8.0", - "7.4", - "7.3", - "7.2", - "7.1", - "7.0" - ] - }, - "SupportedPHPExtensionBundle": { - "type": "string", - "const": "kitchen-sink" - }, - "StepDefinition": { - "type": "object", - "discriminator": { - "propertyName": "step" - }, - "required": [ - "step" - ], - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "activatePlugin" - }, - "pluginPath": { - "type": "string", - "description": "Path to the plugin directory as absolute path (/wordpress/wp-content/plugins/plugin-name); or the plugin entry file relative to the plugins directory (plugin-name/plugin-name.php)." - }, - "pluginName": { - "type": "string", - "description": "Optional. Plugin name to display in the progress bar." - } - }, - "required": [ - "pluginPath", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "activateTheme" - }, - "themeFolderName": { - "type": "string", - "description": "The name of the theme folder inside wp-content/themes/" - } - }, - "required": [ - "step", - "themeFolderName" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "applyWordPressPatches" - }, - "siteUrl": { - "type": "string" - }, - "wordpressPath": { - "type": "string" - }, - "addPhpInfo": { - "type": "boolean" - }, - "patchSecrets": { - "type": "boolean" - }, - "disableSiteHealth": { - "type": "boolean" - }, - "disableWpNewBlogNotification": { - "type": "boolean" - }, - "makeEditorFrameControlled": { - "type": "boolean" - }, - "prepareForRunningInsideWebBrowser": { - "type": "boolean" - } - }, - "required": [ - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "cp" - }, - "fromPath": { - "type": "string", - "description": "Source path" - }, - "toPath": { - "type": "string", - "description": "Target path" - } - }, - "required": [ - "fromPath", - "step", - "toPath" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "defineWpConfigConsts" - }, - "consts": { - "type": "object", - "additionalProperties": {}, - "description": "The constants to define" - }, - "virtualize": { - "type": "boolean", - "description": "Enables the virtualization of wp-config.php and playground-consts.json files, leaving the local system files untouched. The variables defined in the /vfs-blueprints/playground-consts.json file are loaded via the auto_prepend_file directive in the php.ini file.", - "default": false - } - }, - "required": [ - "consts", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "defineSiteUrl" - }, - "siteUrl": { - "type": "string", - "description": "The URL" - } - }, - "required": [ - "siteUrl", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "importFile" - }, - "file": { - "$ref": "#/definitions/FileReference", - "description": "The file to import" - } - }, - "required": [ - "file", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "installPlugin", - "description": "The step identifier." - }, - "pluginZipFile": { - "$ref": "#/definitions/FileReference", - "description": "The plugin zip file to install." - }, - "options": { - "$ref": "#/definitions/InstallPluginOptions", - "description": "Optional installation options." - } - }, - "required": [ - "pluginZipFile", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "installTheme", - "description": "The step identifier." - }, - "themeZipFile": { - "$ref": "#/definitions/FileReference", - "description": "The theme zip file to install." - }, - "options": { - "type": "object", - "properties": { - "activate": { - "type": "boolean", - "description": "Whether to activate the theme after installing it." - } - }, - "additionalProperties": false, - "description": "Optional installation options." - } - }, - "required": [ - "step", - "themeZipFile" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "login" - }, - "username": { - "type": "string", - "description": "The user to log in as. Defaults to 'admin'." - }, - "password": { - "type": "string", - "description": "The password to log in with. Defaults to 'password'." - } - }, - "required": [ - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "mkdir" - }, - "path": { - "type": "string", - "description": "The path of the directory you want to create" - } - }, - "required": [ - "path", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "mv" - }, - "fromPath": { - "type": "string", - "description": "Source path" - }, - "toPath": { - "type": "string", - "description": "Target path" - } - }, - "required": [ - "fromPath", - "step", - "toPath" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "request" - }, - "request": { - "$ref": "#/definitions/PHPRequest", - "description": "Request details (See /wordpress-playground/api/universal/interface/PHPRequest)" - } - }, - "required": [ - "request", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "replaceSite" - }, - "fullSiteZip": { - "$ref": "#/definitions/FileReference", - "description": "The zip file containing the new WordPress site" - } - }, - "required": [ - "fullSiteZip", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "rm" - }, - "path": { - "type": "string", - "description": "The path to remove" - } - }, - "required": [ - "path", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "rmdir" - }, - "path": { - "type": "string", - "description": "The path to remove" - } - }, - "required": [ - "path", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "runPHP", - "description": "The step identifier." - }, - "code": { - "type": "string", - "description": "The PHP code to run." - } - }, - "required": [ - "code", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "runPHPWithOptions" - }, - "options": { - "$ref": "#/definitions/PHPRunOptions", - "description": "Run options (See /wordpress-playground/api/universal/interface/PHPRunOptions)" - } - }, - "required": [ - "options", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "runWpInstallationWizard" - }, - "options": { - "$ref": "#/definitions/WordPressInstallationOptions" - } - }, - "required": [ - "options", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "setPhpIniEntry" - }, - "key": { - "type": "string", - "description": "Entry name e.g. \"display_errors\"" - }, - "value": { - "type": "string", - "description": "Entry value as a string e.g. \"1\"" - } - }, - "required": [ - "key", - "step", - "value" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "setSiteOptions", - "description": "The name of the step. Must be \"setSiteOptions\"." - }, - "options": { - "type": "object", - "additionalProperties": {}, - "description": "The options to set on the site." - } - }, - "required": [ - "options", - "step" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "unzip" - }, - "zipPath": { - "type": "string", - "description": "The zip file to extract" - }, - "extractToPath": { - "type": "string", - "description": "The path to extract the zip file to" - } - }, - "required": [ - "extractToPath", - "step", - "zipPath" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "updateUserMeta" - }, - "meta": { - "type": "object", - "additionalProperties": {}, - "description": "An object of user meta values to set, e.g. { \"first_name\": \"John\" }" - }, - "userId": { - "type": "number", - "description": "User ID" - } - }, - "required": [ - "meta", - "step", - "userId" - ] - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "progress": { - "type": "object", - "properties": { - "weight": { - "type": "number" - }, - "caption": { - "type": "string" - } - }, - "additionalProperties": false - }, - "step": { - "type": "string", - "const": "writeFile" - }, - "path": { - "type": "string", - "description": "The path of the file to write to" - }, - "data": { - "anyOf": [ - { - "$ref": "#/definitions/FileReference" - }, - { - "type": "string" - }, - { - "type": "object", - "properties": { - "BYTES_PER_ELEMENT": { - "type": "number" - }, - "buffer": { - "type": "object", - "properties": { - "byteLength": { - "type": "number" - } - }, - "required": [ - "byteLength" - ], - "additionalProperties": false - }, - "byteLength": { - "type": "number" - }, - "byteOffset": { - "type": "number" - }, - "length": { - "type": "number" - } - }, - "required": [ - "BYTES_PER_ELEMENT", - "buffer", - "byteLength", - "byteOffset", - "length" - ], - "additionalProperties": { - "type": "number" - } - } - ], - "description": "The data to write" - } - }, - "required": [ - "data", - "path", - "step" - ] - } - ] - }, - "FileReference": { - "anyOf": [ - { - "$ref": "#/definitions/VFSReference" - }, - { - "$ref": "#/definitions/LiteralReference" - }, - { - "$ref": "#/definitions/CoreThemeReference" - }, - { - "$ref": "#/definitions/CorePluginReference" - }, - { - "$ref": "#/definitions/UrlReference" - } - ] - }, - "VFSReference": { - "type": "object", - "properties": { - "resource": { - "type": "string", - "const": "vfs", - "description": "Identifies the file resource as Virtual File System (VFS)" - }, - "path": { - "type": "string", - "description": "The path to the file in the VFS" - } - }, - "required": [ - "resource", - "path" - ], - "additionalProperties": false - }, - "LiteralReference": { - "type": "object", - "properties": { - "resource": { - "type": "string", - "const": "literal", - "description": "Identifies the file resource as a literal file" - }, - "name": { - "type": "string", - "description": "The name of the file" - }, - "contents": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "BYTES_PER_ELEMENT": { - "type": "number" - }, - "buffer": { - "type": "object", - "properties": { - "byteLength": { - "type": "number" - } - }, - "required": [ - "byteLength" - ], - "additionalProperties": false - }, - "byteLength": { - "type": "number" - }, - "byteOffset": { - "type": "number" - }, - "length": { - "type": "number" - } - }, - "required": [ - "BYTES_PER_ELEMENT", - "buffer", - "byteLength", - "byteOffset", - "length" - ], - "additionalProperties": { - "type": "number" - } - } - ], - "description": "The contents of the file" - } - }, - "required": [ - "resource", - "name", - "contents" - ], - "additionalProperties": false - }, - "CoreThemeReference": { - "type": "object", - "properties": { - "resource": { - "type": "string", - "const": "wordpress.org/themes", - "description": "Identifies the file resource as a WordPress Core theme" - }, - "slug": { - "type": "string", - "description": "The slug of the WordPress Core theme" - } - }, - "required": [ - "resource", - "slug" - ], - "additionalProperties": false - }, - "CorePluginReference": { - "type": "object", - "properties": { - "resource": { - "type": "string", - "const": "wordpress.org/plugins", - "description": "Identifies the file resource as a WordPress Core plugin" - }, - "slug": { - "type": "string", - "description": "The slug of the WordPress Core plugin" - } - }, - "required": [ - "resource", - "slug" - ], - "additionalProperties": false - }, - "UrlReference": { - "type": "object", - "properties": { - "resource": { - "type": "string", - "const": "url", - "description": "Identifies the file resource as a URL" - }, - "url": { - "type": "string", - "description": "The URL of the file" - }, - "caption": { - "type": "string", - "description": "Optional caption for displaying a progress message" - } - }, - "required": [ - "resource", - "url" - ], - "additionalProperties": false - }, - "InstallPluginOptions": { - "type": "object", - "properties": { - "activate": { - "type": "boolean", - "description": "Whether to activate the plugin after installing it." - } - }, - "additionalProperties": false - }, - "PHPRequest": { - "type": "object", - "properties": { - "method": { - "$ref": "#/definitions/HTTPMethod", - "description": "Request method. Default: `GET`." - }, - "url": { - "type": "string", - "description": "Request path or absolute URL." - }, - "headers": { - "$ref": "#/definitions/PHPRequestHeaders", - "description": "Request headers." - }, - "files": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "size": { - "type": "number" - }, - "type": { - "type": "string" - }, - "lastModified": { - "type": "number" - }, - "name": { - "type": "string" - }, - "webkitRelativePath": { - "type": "string" - } - }, - "required": [ - "lastModified", - "name", - "size", - "type", - "webkitRelativePath" - ], - "additionalProperties": false - }, - "description": "Uploaded files" - }, - "body": { - "type": "string", - "description": "Request body without the files." - }, - "formData": { - "type": "object", - "additionalProperties": {}, - "description": "Form data. If set, the request body will be ignored and the content-type header will be set to `application/x-www-form-urlencoded`." - } - }, - "required": [ - "url" - ], - "additionalProperties": false - }, - "HTTPMethod": { - "type": "string", - "enum": [ - "GET", - "POST", - "HEAD", - "OPTIONS", - "PATCH", - "PUT", - "DELETE" - ] - }, - "PHPRequestHeaders": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "PHPRunOptions": { - "type": "object", - "properties": { - "relativeUri": { - "type": "string", - "description": "Request path following the domain:port part." - }, - "scriptPath": { - "type": "string", - "description": "Path of the .php file to execute." - }, - "protocol": { - "type": "string", - "description": "Request protocol." - }, - "method": { - "$ref": "#/definitions/HTTPMethod", - "description": "Request method. Default: `GET`." - }, - "headers": { - "$ref": "#/definitions/PHPRequestHeaders", - "description": "Request headers." - }, - "body": { - "type": "string", - "description": "Request body without the files." - }, - "fileInfos": { - "type": "array", - "items": { - "$ref": "#/definitions/FileInfo" - }, - "description": "Uploaded files." - }, - "code": { - "type": "string", - "description": "The code snippet to eval instead of a php file." - } - }, - "additionalProperties": false - }, - "FileInfo": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "BYTES_PER_ELEMENT": { - "type": "number" - }, - "buffer": { - "type": "object", - "properties": { - "byteLength": { - "type": "number" - } - }, - "required": [ - "byteLength" - ], - "additionalProperties": false - }, - "byteLength": { - "type": "number" - }, - "byteOffset": { - "type": "number" - }, - "length": { - "type": "number" - } - }, - "required": [ - "BYTES_PER_ELEMENT", - "buffer", - "byteLength", - "byteOffset", - "length" - ], - "additionalProperties": { - "type": "number" - } - } - }, - "required": [ - "key", - "name", - "type", - "data" - ], - "additionalProperties": false - }, - "WordPressInstallationOptions": { - "type": "object", - "properties": { - "adminUsername": { - "type": "string" - }, - "adminPassword": { - "type": "string" - } - }, - "additionalProperties": false - } - } -} \ No newline at end of file + "$schema": "http://json-schema.org/schema", + "$ref": "#/definitions/Blueprint", + "definitions": { + "Blueprint": { + "type": "object", + "properties": { + "landingPage": { + "type": "string", + "description": "The URL to navigate to after the blueprint has been run." + }, + "preferredVersions": { + "type": "object", + "properties": { + "php": { + "anyOf": [ + { + "$ref": "#/definitions/SupportedPHPVersion" + }, + { + "type": "string", + "const": "latest" + } + ], + "description": "The preferred PHP version to use. If not specified, the latest supported version will be used" + }, + "wp": { + "type": "string", + "description": "The preferred WordPress version to use. If not specified, the latest supported version will be used" + } + }, + "required": ["php", "wp"], + "additionalProperties": false, + "description": "The preferred PHP and WordPress versions to use." + }, + "features": { + "type": "object", + "properties": { + "networking": { + "type": "boolean", + "description": "Should boot with support for network request via wp_safe_remote_get?" + } + }, + "additionalProperties": false + }, + "phpExtensionBundles": { + "type": "array", + "items": { + "$ref": "#/definitions/SupportedPHPExtensionBundle" + }, + "description": "The PHP extensions to use." + }, + "steps": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/StepDefinition" + }, + { + "type": "string" + }, + { + "not": {} + }, + { + "type": "boolean", + "const": false + }, + { + "type": "null" + } + ] + }, + "description": "The steps to run." + }, + "$schema": { + "type": "string" + } + }, + "additionalProperties": false + }, + "SupportedPHPVersion": { + "type": "string", + "enum": ["8.2", "8.1", "8.0", "7.4", "7.3", "7.2", "7.1", "7.0"] + }, + "SupportedPHPExtensionBundle": { + "type": "string", + "const": "kitchen-sink" + }, + "StepDefinition": { + "type": "object", + "discriminator": { + "propertyName": "step" + }, + "required": ["step"], + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "activatePlugin" + }, + "pluginPath": { + "type": "string", + "description": "Path to the plugin directory as absolute path (/wordpress/wp-content/plugins/plugin-name); or the plugin entry file relative to the plugins directory (plugin-name/plugin-name.php)." + }, + "pluginName": { + "type": "string", + "description": "Optional. Plugin name to display in the progress bar." + } + }, + "required": ["pluginPath", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "activateTheme" + }, + "themeFolderName": { + "type": "string", + "description": "The name of the theme folder inside wp-content/themes/" + } + }, + "required": ["step", "themeFolderName"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "applyWordPressPatches" + }, + "siteUrl": { + "type": "string" + }, + "wordpressPath": { + "type": "string" + }, + "addPhpInfo": { + "type": "boolean" + }, + "patchSecrets": { + "type": "boolean" + }, + "disableSiteHealth": { + "type": "boolean" + }, + "disableWpNewBlogNotification": { + "type": "boolean" + }, + "makeEditorFrameControlled": { + "type": "boolean" + }, + "prepareForRunningInsideWebBrowser": { + "type": "boolean" + } + }, + "required": ["step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "cp" + }, + "fromPath": { + "type": "string", + "description": "Source path" + }, + "toPath": { + "type": "string", + "description": "Target path" + } + }, + "required": ["fromPath", "step", "toPath"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "defineWpConfigConsts" + }, + "consts": { + "type": "object", + "additionalProperties": {}, + "description": "The constants to define" + }, + "virtualize": { + "type": "boolean", + "description": "Enables the virtualization of wp-config.php and playground-consts.json files, leaving the local system files untouched. The variables defined in the /vfs-blueprints/playground-consts.json file are loaded via the auto_prepend_file directive in the php.ini file.", + "default": false + } + }, + "required": ["consts", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "defineSiteUrl" + }, + "siteUrl": { + "type": "string", + "description": "The URL" + } + }, + "required": ["siteUrl", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "importFile" + }, + "file": { + "$ref": "#/definitions/FileReference", + "description": "The file to import" + } + }, + "required": ["file", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "installPlugin", + "description": "The step identifier." + }, + "pluginZipFile": { + "$ref": "#/definitions/FileReference", + "description": "The plugin zip file to install." + }, + "options": { + "$ref": "#/definitions/InstallPluginOptions", + "description": "Optional installation options." + } + }, + "required": ["pluginZipFile", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "installTheme", + "description": "The step identifier." + }, + "themeZipFile": { + "$ref": "#/definitions/FileReference", + "description": "The theme zip file to install." + }, + "options": { + "type": "object", + "properties": { + "activate": { + "type": "boolean", + "description": "Whether to activate the theme after installing it." + } + }, + "additionalProperties": false, + "description": "Optional installation options." + } + }, + "required": ["step", "themeZipFile"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "login" + }, + "username": { + "type": "string", + "description": "The user to log in as. Defaults to 'admin'." + }, + "password": { + "type": "string", + "description": "The password to log in with. Defaults to 'password'." + } + }, + "required": ["step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "mkdir" + }, + "path": { + "type": "string", + "description": "The path of the directory you want to create" + } + }, + "required": ["path", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "mv" + }, + "fromPath": { + "type": "string", + "description": "Source path" + }, + "toPath": { + "type": "string", + "description": "Target path" + } + }, + "required": ["fromPath", "step", "toPath"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "request" + }, + "request": { + "$ref": "#/definitions/PHPRequest", + "description": "Request details (See /wordpress-playground/api/universal/interface/PHPRequest)" + } + }, + "required": ["request", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "replaceSite" + }, + "fullSiteZip": { + "$ref": "#/definitions/FileReference", + "description": "The zip file containing the new WordPress site" + } + }, + "required": ["fullSiteZip", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "rm" + }, + "path": { + "type": "string", + "description": "The path to remove" + } + }, + "required": ["path", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "rmdir" + }, + "path": { + "type": "string", + "description": "The path to remove" + } + }, + "required": ["path", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "runPHP", + "description": "The step identifier." + }, + "code": { + "type": "string", + "description": "The PHP code to run." + } + }, + "required": ["code", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "runPHPWithOptions" + }, + "options": { + "$ref": "#/definitions/PHPRunOptions", + "description": "Run options (See /wordpress-playground/api/universal/interface/PHPRunOptions)" + } + }, + "required": ["options", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "runWpInstallationWizard" + }, + "options": { + "$ref": "#/definitions/WordPressInstallationOptions" + } + }, + "required": ["options", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "setPhpIniEntry" + }, + "key": { + "type": "string", + "description": "Entry name e.g. \"display_errors\"" + }, + "value": { + "type": "string", + "description": "Entry value as a string e.g. \"1\"" + } + }, + "required": ["key", "step", "value"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "setSiteOptions", + "description": "The name of the step. Must be \"setSiteOptions\"." + }, + "options": { + "type": "object", + "additionalProperties": {}, + "description": "The options to set on the site." + } + }, + "required": ["options", "step"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "unzip" + }, + "zipPath": { + "type": "string", + "description": "The zip file to extract" + }, + "extractToPath": { + "type": "string", + "description": "The path to extract the zip file to" + } + }, + "required": ["extractToPath", "step", "zipPath"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "updateUserMeta" + }, + "meta": { + "type": "object", + "additionalProperties": {}, + "description": "An object of user meta values to set, e.g. { \"first_name\": \"John\" }" + }, + "userId": { + "type": "number", + "description": "User ID" + } + }, + "required": ["meta", "step", "userId"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "progress": { + "type": "object", + "properties": { + "weight": { + "type": "number" + }, + "caption": { + "type": "string" + } + }, + "additionalProperties": false + }, + "step": { + "type": "string", + "const": "writeFile" + }, + "path": { + "type": "string", + "description": "The path of the file to write to" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/FileReference" + }, + { + "type": "string" + }, + { + "type": "object", + "properties": { + "BYTES_PER_ELEMENT": { + "type": "number" + }, + "buffer": { + "type": "object", + "properties": { + "byteLength": { + "type": "number" + } + }, + "required": ["byteLength"], + "additionalProperties": false + }, + "byteLength": { + "type": "number" + }, + "byteOffset": { + "type": "number" + }, + "length": { + "type": "number" + } + }, + "required": [ + "BYTES_PER_ELEMENT", + "buffer", + "byteLength", + "byteOffset", + "length" + ], + "additionalProperties": { + "type": "number" + } + } + ], + "description": "The data to write" + } + }, + "required": ["data", "path", "step"] + } + ] + }, + "FileReference": { + "anyOf": [ + { + "$ref": "#/definitions/VFSReference" + }, + { + "$ref": "#/definitions/LiteralReference" + }, + { + "$ref": "#/definitions/CoreThemeReference" + }, + { + "$ref": "#/definitions/CorePluginReference" + }, + { + "$ref": "#/definitions/UrlReference" + } + ] + }, + "VFSReference": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "const": "vfs", + "description": "Identifies the file resource as Virtual File System (VFS)" + }, + "path": { + "type": "string", + "description": "The path to the file in the VFS" + } + }, + "required": ["resource", "path"], + "additionalProperties": false + }, + "LiteralReference": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "const": "literal", + "description": "Identifies the file resource as a literal file" + }, + "name": { + "type": "string", + "description": "The name of the file" + }, + "contents": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "BYTES_PER_ELEMENT": { + "type": "number" + }, + "buffer": { + "type": "object", + "properties": { + "byteLength": { + "type": "number" + } + }, + "required": ["byteLength"], + "additionalProperties": false + }, + "byteLength": { + "type": "number" + }, + "byteOffset": { + "type": "number" + }, + "length": { + "type": "number" + } + }, + "required": [ + "BYTES_PER_ELEMENT", + "buffer", + "byteLength", + "byteOffset", + "length" + ], + "additionalProperties": { + "type": "number" + } + } + ], + "description": "The contents of the file" + } + }, + "required": ["resource", "name", "contents"], + "additionalProperties": false + }, + "CoreThemeReference": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "const": "wordpress.org/themes", + "description": "Identifies the file resource as a WordPress Core theme" + }, + "slug": { + "type": "string", + "description": "The slug of the WordPress Core theme" + } + }, + "required": ["resource", "slug"], + "additionalProperties": false + }, + "CorePluginReference": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "const": "wordpress.org/plugins", + "description": "Identifies the file resource as a WordPress Core plugin" + }, + "slug": { + "type": "string", + "description": "The slug of the WordPress Core plugin" + } + }, + "required": ["resource", "slug"], + "additionalProperties": false + }, + "UrlReference": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "const": "url", + "description": "Identifies the file resource as a URL" + }, + "url": { + "type": "string", + "description": "The URL of the file" + }, + "caption": { + "type": "string", + "description": "Optional caption for displaying a progress message" + } + }, + "required": ["resource", "url"], + "additionalProperties": false + }, + "InstallPluginOptions": { + "type": "object", + "properties": { + "activate": { + "type": "boolean", + "description": "Whether to activate the plugin after installing it." + } + }, + "additionalProperties": false + }, + "PHPRequest": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/HTTPMethod", + "description": "Request method. Default: `GET`." + }, + "url": { + "type": "string", + "description": "Request path or absolute URL." + }, + "headers": { + "$ref": "#/definitions/PHPRequestHeaders", + "description": "Request headers." + }, + "files": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "size": { + "type": "number" + }, + "type": { + "type": "string" + }, + "lastModified": { + "type": "number" + }, + "name": { + "type": "string" + }, + "webkitRelativePath": { + "type": "string" + } + }, + "required": [ + "lastModified", + "name", + "size", + "type", + "webkitRelativePath" + ], + "additionalProperties": false + }, + "description": "Uploaded files" + }, + "body": { + "type": "string", + "description": "Request body without the files." + }, + "formData": { + "type": "object", + "additionalProperties": {}, + "description": "Form data. If set, the request body will be ignored and the content-type header will be set to `application/x-www-form-urlencoded`." + } + }, + "required": ["url"], + "additionalProperties": false + }, + "HTTPMethod": { + "type": "string", + "enum": ["GET", "POST", "HEAD", "OPTIONS", "PATCH", "PUT", "DELETE"] + }, + "PHPRequestHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "PHPRunOptions": { + "type": "object", + "properties": { + "relativeUri": { + "type": "string", + "description": "Request path following the domain:port part." + }, + "scriptPath": { + "type": "string", + "description": "Path of the .php file to execute." + }, + "protocol": { + "type": "string", + "description": "Request protocol." + }, + "method": { + "$ref": "#/definitions/HTTPMethod", + "description": "Request method. Default: `GET`." + }, + "headers": { + "$ref": "#/definitions/PHPRequestHeaders", + "description": "Request headers." + }, + "body": { + "type": "string", + "description": "Request body without the files." + }, + "fileInfos": { + "type": "array", + "items": { + "$ref": "#/definitions/FileInfo" + }, + "description": "Uploaded files." + }, + "code": { + "type": "string", + "description": "The code snippet to eval instead of a php file." + } + }, + "additionalProperties": false + }, + "FileInfo": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "BYTES_PER_ELEMENT": { + "type": "number" + }, + "buffer": { + "type": "object", + "properties": { + "byteLength": { + "type": "number" + } + }, + "required": ["byteLength"], + "additionalProperties": false + }, + "byteLength": { + "type": "number" + }, + "byteOffset": { + "type": "number" + }, + "length": { + "type": "number" + } + }, + "required": [ + "BYTES_PER_ELEMENT", + "buffer", + "byteLength", + "byteOffset", + "length" + ], + "additionalProperties": { + "type": "number" + } + } + }, + "required": ["key", "name", "type", "data"], + "additionalProperties": false + }, + "WordPressInstallationOptions": { + "type": "object", + "properties": { + "adminUsername": { + "type": "string" + }, + "adminPassword": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/packages/playground/blueprints/src/lib/blueprint.ts b/packages/playground/blueprints/src/lib/blueprint.ts index 2479a349c5..2cb2e2eea8 100644 --- a/packages/playground/blueprints/src/lib/blueprint.ts +++ b/packages/playground/blueprints/src/lib/blueprint.ts @@ -24,6 +24,10 @@ export interface Blueprint { */ wp: string | 'latest'; }; + features?: { + /** Should boot with support for network request via wp_safe_remote_get? */ + networking?: boolean; + }; /** * The PHP extensions to use. */ diff --git a/packages/playground/blueprints/src/lib/compile.ts b/packages/playground/blueprints/src/lib/compile.ts index 29df7b9e61..253d4ce45f 100644 --- a/packages/playground/blueprints/src/lib/compile.ts +++ b/packages/playground/blueprints/src/lib/compile.ts @@ -40,6 +40,10 @@ export interface CompiledBlueprint { }; /** The requested PHP extensions to load */ phpExtensions: SupportedPHPExtension[]; + features: { + /** Should boot with support for network request via wp_safe_remote_get? */ + networking: boolean; + }; /** The compiled steps for the blueprint */ run: (playground: UniversalPHP) => Promise; } @@ -113,6 +117,10 @@ export function compileBlueprint( [], blueprint.phpExtensionBundles || [] ), + features: { + // Enable networking by default + networking: blueprint.features?.networking ?? true, + }, run: async (playground: UniversalPHP) => { try { // Start resolving resources early diff --git a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/index.ts b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/index.ts index 11e7030e37..d17104e512 100644 --- a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/index.ts +++ b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/index.ts @@ -31,6 +31,7 @@ export interface ApplyWordPressPatchesStep { disableWpNewBlogNotification?: boolean; makeEditorFrameControlled?: boolean; prepareForRunningInsideWebBrowser?: boolean; + addFetchNetworkTransport?: boolean; } export const applyWordPressPatches: StepHandler< @@ -66,6 +67,9 @@ export const applyWordPressPatches: StepHandler< if (options.prepareForRunningInsideWebBrowser === true) { await patch.prepareForRunningInsideWebBrowser(); } + if (options.addFetchNetworkTransport === true) { + await patch.addFetchNetworkTransport(); + } }; class WordPressPatcher { @@ -147,9 +151,31 @@ class WordPressPatcher { } async prepareForRunningInsideWebBrowser() { + // Various tweaks + await this.php.writeFile( + `${this.wordpressPath}/wp-content/mu-plugins/1-show-admin-credentials-on-wp-login.php`, + showAdminCredentialsOnWpLogin + ); + await this.php.writeFile( + `${this.wordpressPath}/wp-content/mu-plugins/2-nice-error-messages-for-plugins-and-themes-directories.php`, + niceErrorMessagesForPluginsAndThemesDirectories + ); + await this.php.writeFile( + `${this.wordpressPath}/wp-content/mu-plugins/3-links-targeting-top-frame-should-target-playground-iframe.php`, + linksTargetingTopFrameShouldTargetPlaygroundIframe + ); + + // Activate URL rewriting. + await this.php.writeFile( + `${this.wordpressPath}/wp-content/mu-plugins/4-enable-url-rewrite.php`, + enableUrlRewrite + ); + } + + async addFetchNetworkTransport() { await defineWpConfigConsts(this.php, { consts: { - USE_FETCH_FOR_REQUESTS: false, + USE_FETCH_FOR_REQUESTS: true, }, }); @@ -157,6 +183,8 @@ class WordPressPatcher { const transports = [ `${this.wordpressPath}/wp-includes/Requests/Transport/fsockopen.php`, `${this.wordpressPath}/wp-includes/Requests/Transport/cURL.php`, + `${this.wordpressPath}/wp-includes/Requests/src/Transport/Fsockopen.php`, + `${this.wordpressPath}/wp-includes/Requests/src/Transport/Curl.php`, ]; for (const transport of transports) { // One of the transports might not exist in the latest WordPress version. @@ -188,25 +216,7 @@ class WordPressPatcher { addRequests ); - // Various tweaks - await this.php.writeFile( - `${this.wordpressPath}/wp-content/mu-plugins/1-show-admin-credentials-on-wp-login.php`, - showAdminCredentialsOnWpLogin - ); - await this.php.writeFile( - `${this.wordpressPath}/wp-content/mu-plugins/2-nice-error-messages-for-plugins-and-themes-directories.php`, - niceErrorMessagesForPluginsAndThemesDirectories - ); - await this.php.writeFile( - `${this.wordpressPath}/wp-content/mu-plugins/3-links-targeting-top-frame-should-target-playground-iframe.php`, - linksTargetingTopFrameShouldTargetPlaygroundIframe - ); - - // Activate URL rewriting. - await this.php.writeFile( - `${this.wordpressPath}/wp-content/mu-plugins/4-enable-url-rewrite.php`, - enableUrlRewrite - ); + await this.php.mkdir(`${this.wordpressPath}/wp-content/fonts`); } } diff --git a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/add_requests_transport.php b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/add_requests_transport.php index 604f8fb203..6d43fa52b6 100644 --- a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/add_requests_transport.php +++ b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/add_requests_transport.php @@ -13,12 +13,25 @@ * the Requests class happy. */ if (defined('USE_FETCH_FOR_REQUESTS') && USE_FETCH_FOR_REQUESTS) { - require(__DIR__ . '/includes/requests_transport_fetch.php'); + require(__DIR__ . '/includes/requests_transport_fetch.php'); Requests::add_transport('Requests_Transport_Fetch'); + /** + * Disable signature verification as it doesn't seem to work with + * fetch requests: + * + * https://downloads.wordpress.org/plugin/classic-editor.zip returns no signature header. + * https://downloads.wordpress.org/plugin/classic-editor.zip.sig returns 404. + * + * @TODO Investigate why. + */ + add_filter('wp_signature_hosts', function ($hosts) { + return []; + }); + add_filter('http_request_host_is_external', function ($arg) { return true; }); } else { - require(__DIR__ . '/includes/requests_transport_dummy.php'); - Requests::add_transport('Requests_Transport_Dummy'); + require(__DIR__ . '/includes/requests_transport_dummy.php'); + Requests::add_transport('Requests_Transport_Dummy'); } diff --git a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/includes/requests_transport_fetch.php b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/includes/requests_transport_fetch.php index e4843fc7f3..5bad07eefe 100644 --- a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/includes/requests_transport_fetch.php +++ b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/includes/requests_transport_fetch.php @@ -6,11 +6,7 @@ * This file isn't actually used. It's just here for reference and development. The actual * PHP code used in WordPress is hardcoded copy residing in wordpress.mjs in the _patchWordPressCode * function. - * - * @TODO Make the build pipeline use this exact file instead of creating it - * from within the JavaScript runtime. */ - class Requests_Transport_Fetch implements Requests_Transport { public $headers = ''; @@ -43,7 +39,6 @@ public function request($url, $headers = array(), $data = array(), $options = ar return false; } - $headers = Requests::flatten($headers); if (!empty($data)) { $data_format = $options['data_format']; if ($data_format === 'query') { @@ -54,36 +49,25 @@ public function request($url, $headers = array(), $data = array(), $options = ar } } - $request = json_encode(json_encode(array( - 'headers' => $headers, - 'data' => $data, - 'url' => $url, - 'method' => $options['type'], - ))); - - $js = <<headers = vrzno_eval($js); + $request = json_encode(array( + 'type' => 'request', + 'data' => [ + 'headers' => $headers, + 'data' => $data, + 'url' => $url, + 'method' => $options['type'], + ] + )); + + $this->headers = post_message_to_js($request); + + // Store a file if the request specifies it. + // Are we sure that `$this->headers` includes the body of the response? + $before_response_body = strpos( $this->headers, "\r\n\r\n" ); + if ( isset( $options['filename'] ) && $options['filename'] && false !== $before_response_body ) { + $response_body = substr( $this->headers, $before_response_body + 4 ); + file_put_contents($options['filename'], $response_body); + } return $this->headers; } @@ -132,11 +116,7 @@ protected static function format_get($url, $data) public static function test($capabilities = array()) { - if (!function_exists('vrzno_eval')) { - return false; - } - - if (vrzno_eval("typeof XMLHttpRequest;") !== 'function') { + if (!function_exists('post_message_to_js')) { return false; } diff --git a/packages/playground/client/src/index.ts b/packages/playground/client/src/index.ts index 5c38e8d8d3..885ad5461a 100644 --- a/packages/playground/client/src/index.ts +++ b/packages/playground/client/src/index.ts @@ -83,6 +83,7 @@ export async function startPlaygroundWeb({ php: compiled.versions.php, wp: compiled.versions.wp, ['php-extension']: compiled.phpExtensions, + ['networking']: compiled.features.networking ? 'yes' : 'no', }), progressTracker ); diff --git a/packages/playground/remote/src/lib/boot-playground-remote.ts b/packages/playground/remote/src/lib/boot-playground-remote.ts index 003a08a53d..15a8ab4725 100644 --- a/packages/playground/remote/src/lib/boot-playground-remote.ts +++ b/packages/playground/remote/src/lib/boot-playground-remote.ts @@ -29,6 +29,7 @@ import serviceWorkerPath from '../../service-worker.ts?worker&url'; import { LatestSupportedWordPressVersion } from '../wordpress/get-wordpress-module'; import type { SyncProgressCallback } from './opfs/bind-opfs'; import { FilesystemOperation } from '@php-wasm/fs-journal'; +import { setupFetchNetworkTransport } from './setup-fetch-network-transport'; export const serviceWorkerUrl = new URL(serviceWorkerPath, origin); // Prevent Vite from hot-reloading this file – it would @@ -66,11 +67,13 @@ export async function bootPlaygroundRemote() { query.getAll('php-extension'), SupportedPHPExtensionsList ); + const withNetworking = query.get('networking') === 'yes'; const workerApi = consumeAPI( await spawnPHPWorkerThread(workerUrl, { wpVersion, phpVersion, ['php-extension']: phpExtensions, + networking: withNetworking ? 'yes' : 'no', storage: query.get('storage') || '', }) ); @@ -187,6 +190,9 @@ export async function bootPlaygroundRemote() { serviceWorkerUrl + '' ); setupPostMessageRelay(wpFrame, getOrigin(await playground.absoluteUrl)); + if (withNetworking) { + setupFetchNetworkTransport(workerApi); + } setAPIReady(); } catch (e) { diff --git a/packages/playground/remote/src/lib/setup-fetch-network-transport.ts b/packages/playground/remote/src/lib/setup-fetch-network-transport.ts new file mode 100644 index 0000000000..5a34c7b746 --- /dev/null +++ b/packages/playground/remote/src/lib/setup-fetch-network-transport.ts @@ -0,0 +1,93 @@ +import { UniversalPHP } from '@php-wasm/universal'; +import { applyWordPressPatches } from '@wp-playground/blueprints'; + +export interface RequestData { + url: string; + method?: string; + headers?: Record; + data?: string; +} + +export interface RequestMessage { + type: 'request'; + data: RequestData; +} + +/** + * Allow WordPress to make network requests via the fetch API. + * On the WordPress side, this is handled by Requests_Transport_Fetch + * + * @param playground the Playground instance to set up with network support. + */ +export async function setupFetchNetworkTransport(playground: UniversalPHP) { + await applyWordPressPatches(playground, { + addFetchNetworkTransport: true, + }); + + await playground.onMessage(async (message: string) => { + const envelope: RequestMessage = JSON.parse(message); + const { type, data } = envelope; + if (type !== 'request') { + return ''; + } + + return handleRequest(data); + }); +} + +export async function handleRequest(data: RequestData, fetchFn = fetch) { + const hostname = new URL(data.url).hostname; + const fetchUrl = ['api.wordpress.org', 'w.org', 's.w.org'].includes( + hostname + ) + ? `/plugin-proxy.php?url=${encodeURIComponent(data.url)}` + : data.url; + + let response; + try { + response = await fetchFn(fetchUrl, { + method: data.method || 'GET', + headers: data.headers, + body: data.data, + credentials: 'omit', + }); + } catch (e) { + // console.error(e); + return new TextEncoder().encode( + `HTTP/1.1 400 Invalid Request\r\ncontent-type: text/plain\r\n\r\nPlayground could not serve the request.` + ); + } + const responseHeaders: string[] = []; + response.headers.forEach((value, key) => { + responseHeaders.push(key + ': ' + value); + }); + + /* + * Technically we should only send ASCII here and ensure we don't send control + * characters or newlines. We ought to be very careful with HTTP headers since + * some attacks rely on assumed processing of them to let things slip in that + * would end the headers section before its done. e.g. we don't want to allow + * emoji in a header and we don't want to allow \r\n\r\n in a header. + * + * That being said, the browser takes care of it for us. + * response.headers is an instance of the Headers class, and you just can't + * construct the Headers instance if the values are malformed: + * + * > new Headers({'Content-type': 'text/html\r\n\r\nBreakout!'}) + * Failed to construct 'Headers': Invalid value + */ + const headersText = + [ + 'HTTP/1.1 ' + response.status + ' ' + response.statusText, + ...responseHeaders, + ].join('\r\n') + `\r\n\r\n`; + const headersBuffer = new TextEncoder().encode(headersText); + const bodyBuffer = new Uint8Array(await response.arrayBuffer()); + const jointBuffer = new Uint8Array( + headersBuffer.byteLength + bodyBuffer.byteLength + ); + jointBuffer.set(headersBuffer); + jointBuffer.set(bodyBuffer, headersBuffer.byteLength); + + return jointBuffer; +} diff --git a/packages/playground/remote/src/test/setup-fetch-network-transport.spec.ts b/packages/playground/remote/src/test/setup-fetch-network-transport.spec.ts new file mode 100644 index 0000000000..c86a123052 --- /dev/null +++ b/packages/playground/remote/src/test/setup-fetch-network-transport.spec.ts @@ -0,0 +1,52 @@ +import { handleRequest } from '../lib/setup-fetch-network-transport'; + +describe('handleRequest', () => { + it('Should return a correct response to a basic request', async () => { + const fetchMock = vitest.fn(async () => { + return { + status: 200, + statusText: 'OK', + headers: new Headers({ + 'Content-type': 'text/html', + }), + arrayBuffer: async () => { + return new TextEncoder().encode('Hello, world!'); + }, + }; + }); + const response = await handleRequest( + { + url: 'https://playground.wordpress.net/', + headers: { 'Content-type': 'text/html' }, + }, + fetchMock as any + ); + expect(new TextDecoder().decode(response)).toBe( + `HTTP/1.1 200 OK\r\ncontent-type: text/html\r\n\r\nHello, world!` + ); + }); + it('Should reject responses with malicious headers trying to terminate the headers section early', async () => { + const fetchMock = vitest.fn(async () => { + return { + status: 200, + statusText: 'OK', + headers: new Headers({ + 'Content-type': 'text/html✅', + }), + arrayBuffer: async () => { + return new TextEncoder().encode('Hello, world!'); + }, + }; + }); + const response = await handleRequest( + { + url: 'https://playground.wordpress.net/', + headers: { 'Content-type': 'text/html' }, + }, + fetchMock as any + ); + expect(new TextDecoder().decode(response)).toBe( + `HTTP/1.1 400 Invalid Request\r\ncontent-type: text/plain\r\n\r\nPlayground could not serve the request.` + ); + }); +}); diff --git a/packages/playground/website/public/plugin-proxy.php b/packages/playground/website/public/plugin-proxy.php index f3fb0c3763..4bc720f605 100644 --- a/packages/playground/website/public/plugin-proxy.php +++ b/packages/playground/website/public/plugin-proxy.php @@ -23,7 +23,7 @@ public function streamFromDirectory($name, $directory) $name = preg_replace('#[^a-zA-Z0-9\.\-_]#', '', $name); $zipUrl = "https://downloads.wordpress.org/$directory/$name"; try { - $this->streamHttpResponse($zipUrl, [ + $info = streamHttpResponse($zipUrl, 'GET', [], NULL, [ 'content-length', 'x-frame-options', 'last-modified', @@ -32,7 +32,13 @@ public function streamFromDirectory($name, $directory) 'age', 'vary', 'cache-Control' + ], [ + 'Content-Type: application/zip', + 'Content-Disposition: attachment; filename="plugin.zip"', ]); + if ($info['http_code'] > 299 || $info['http_code'] < 200) { + throw new ApiException('Request failed'); + } } catch (ApiException $e) { throw new ApiException("Plugin or theme '$name' not found"); } @@ -126,7 +132,7 @@ public function streamFromGithubReleases($repo, $name) { $zipUrl = "https://github.com/$repo/releases/latest/download/$name"; try { - $this->streamHttpResponse($zipUrl, [ + $info = streamHttpResponse($zipUrl, 'GET', [], NULL, [ 'content-length', 'x-frame-options', 'last-modified', @@ -135,7 +141,13 @@ public function streamFromGithubReleases($repo, $name) 'age', 'vary', 'cache-Control' + ], [ + 'Content-Type: application/zip', + 'Content-Disposition: attachment; filename="plugin.zip"', ]); + if ($info['http_code'] > 299 || $info['http_code'] < 200) { + throw new ApiException('Request failed'); + } } catch (ApiException $e) { throw new ApiException("Plugin or theme '$name' not found"); } @@ -168,63 +180,74 @@ protected function gitHubRequest($url, $decode = true) ]; } - private function streamHttpResponse($url, $allowed_headers = [], $default_headers = []) - { - $default_headers = array_merge([ - 'Content-Type: application/zip', - 'Content-Disposition: attachment; filename="plugin.zip"', - ], $default_headers); - $ch = curl_init($url); - curl_setopt_array( - $ch, - [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CONNECTTIMEOUT => 30, - CURLOPT_FAILONERROR => true, - CURLOPT_FOLLOWLOCATION => true, - ] - ); +} +function streamHttpResponse($url, $request_method = 'GET', $request_headers = [], $request_body = null, $allowed_response_headers = [], $default_response_headers = []) +{ + $ch = curl_init($url); + curl_setopt_array( + $ch, + [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 30, + CURLOPT_FAILONERROR => true, + CURLOPT_FOLLOWLOCATION => true, + ] + ); - $seen_headers = []; - curl_setopt( - $ch, - CURLOPT_HEADERFUNCTION, - function ($curl, $header_line) use ($seen_headers, $allowed_headers) { - $header_name = strtolower(substr($header_line, 0, strpos($header_line, ':'))); - $seen_headers[$header_name] = true; - if (in_array($header_name, $allowed_headers)) { - header($header_line); - } + if ($request_method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $request_body); + } else if ($request_method === 'HEAD') { + curl_setopt($ch, CURLOPT_NOBODY, true); + } + + if (count($request_headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers); + } + + $seen_headers = []; + curl_setopt( + $ch, + CURLOPT_HEADERFUNCTION, + function ($curl, $header_line) use ($seen_headers, $allowed_response_headers) { + if (strpos($header_line, ':') === false) { return strlen($header_line); } - ); - $extra_headers_sent = false; - curl_setopt( - $ch, - CURLOPT_WRITEFUNCTION, - function ($curl, $body) use (&$extra_headers_sent, $default_headers) { - if (!$extra_headers_sent) { - foreach ($default_headers as $header_line) { - $header_name = strtolower(substr($header_line, 0, strpos($header_line, ':'))); - if (!isset($seen_headers[strtolower($header_name)])) { - header($header_line); - } + $header_name = strtolower(substr($header_line, 0, strpos($header_line, ':'))); + $seen_headers[$header_name] = true; + $illegal_headers = ['transfer-encoding']; + $header_allowed = ( + NULL === $allowed_response_headers || in_array($header_name, $allowed_response_headers) + ) && !in_array($header_name, $illegal_headers); + if ($header_allowed) { + header($header_line); + } + return strlen($header_line); + } + ); + $extra_headers_sent = false; + curl_setopt( + $ch, + CURLOPT_WRITEFUNCTION, + function ($curl, $body) use (&$extra_headers_sent, $default_response_headers) { + if (!$extra_headers_sent) { + foreach ($default_response_headers as $header_line) { + $header_name = strtolower(substr($header_line, 0, strpos($header_line, ':'))); + if (!isset($seen_headers[$header_name])) { + header($header_line); } - $extra_headers_sent = true; } - echo $body; - flush(); - return strlen($body); + $extra_headers_sent = true; } - ); - curl_exec($ch); - $info = curl_getinfo($ch); - curl_close($ch); - if ($info['http_code'] > 299 || $info['http_code'] < 200) { - throw new ApiException('Request failed'); + echo $body; + flush(); + return strlen($body); } - } - + ); + curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + return $info; } $downloader = new PluginDownloader( @@ -232,7 +255,9 @@ function ($curl, $body) use (&$extra_headers_sent, $default_headers) { ); // Serve the request: -header('Access-Control-Allow-Origin: *'); +if (!array_key_exists('url', $_GET)) { + header('Access-Control-Allow-Origin: *'); +} $pluginResponse; try { /** @deprecated Plugins and themes downloads are no longer needed now that WordPress.org serves @@ -287,14 +312,66 @@ function ($curl, $body) use (&$extra_headers_sent, $default_headers) { $_GET['artifact'] ); } else if (isset($_GET['repo']) && isset($_GET['name'])) { - - // Only allow downloads from the block-interactivity-experiments repo for now. + // Only allow downloads from the block-interactivity-experiments repo for now. if ($_GET['repo'] !== 'WordPress/block-interactivity-experiments') { throw new ApiException('Invalid repo. Only "WordPress/block-interactivity-experiments" is allowed.'); } $downloader->streamFromGithubReleases($_GET['repo'], $_GET['name']); + } else if (isset($_GET['url'])) { + // Proxy the current request to $_GET['url'] and return the response, + // but only if the URL is allowlisted. + $url = $_GET['url']; + $allowed_domains = ['api.wordpress.org', 'w.org', 's.w.org']; + $parsed_url = parse_url($url); + if (!in_array($parsed_url['host'], $allowed_domains)) { + http_response_code(403); + echo "Error: The specified URL is not allowed."; + exit; + } + + /** + * Pass through the request headers we got from WordPress via fetch(), + * then filter out: + * + * * The browser-specific headers + * * Headers related to security to avoid leaking any auth information + * + * ...and pass the rest to the proxied request. + * + * @return array + */ + function get_request_headers() + { + $headers = []; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) !== 'HTTP_') { + continue; + } + $name = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($name, 5))))); + $lcname = strtolower($name); + if ( + $lcname === 'authorization' + || $lcname === 'cookie' + || $lcname === 'host' + || $lcname === 'origin' + || $lcname === 'referer' + || 0 === strpos($lcname, 'sec-') + ) { + continue; + } + $headers[$name] = $value; + } + return $headers; + } + streamHttpResponse( + $url, + $_SERVER['REQUEST_METHOD'], + get_request_headers(), + file_get_contents('php://input'), + null + ); } else { throw new ApiException('Invalid query parameters'); } diff --git a/packages/playground/website/src/components/playground-configuration-group/index.tsx b/packages/playground/website/src/components/playground-configuration-group/index.tsx index 4de5206455..c1fb4da0c8 100644 --- a/packages/playground/website/src/components/playground-configuration-group/index.tsx +++ b/packages/playground/website/src/components/playground-configuration-group/index.tsx @@ -63,6 +63,7 @@ export default function PlaygroundConfigurationGroup({ const [wpVersionChoices, setWPVersionChoices] = useState< Record >({}); + useEffect(() => { playground?.getSupportedWordPressVersions().then(({ all, latest }) => { const formOptions: Record = {}; diff --git a/packages/playground/website/src/lib/make-blueprint.tsx b/packages/playground/website/src/lib/make-blueprint.tsx index 6078c87ab7..09e627f7da 100644 --- a/packages/playground/website/src/lib/make-blueprint.tsx +++ b/packages/playground/website/src/lib/make-blueprint.tsx @@ -5,6 +5,7 @@ interface MakeBlueprintOptions { wp?: string; phpExtensionBundles?: string[]; landingPage?: string; + features?: Blueprint['features']; theme?: string; plugins?: string[]; } @@ -17,6 +18,7 @@ export function makeBlueprint(options: MakeBlueprintOptions): Blueprint { wp: options.wp as any, }, phpExtensionBundles: options.phpExtensionBundles as any, + features: options.features, steps: [ { step: 'login', diff --git a/packages/playground/website/src/main.tsx b/packages/playground/website/src/main.tsx index 0111d14b21..cbabcca27d 100644 --- a/packages/playground/website/src/main.tsx +++ b/packages/playground/website/src/main.tsx @@ -35,10 +35,17 @@ try { query.get('wp') || blueprint.preferredVersions!.wp || 'latest'; } } catch (e) { + const features: Blueprint['features'] = {}; + // Networking is enabled by default, so we only need to disable it + // if the query param is explicitly set to "no". + if (query.get('networking') === 'no') { + features['networking'] = false; + } blueprint = makeBlueprint({ php: query.get('php') || '8.0', wp: query.get('wp') || 'latest', theme: query.get('theme') || undefined, + features, plugins: query.getAll('plugin'), landingPage: query.get('url') || undefined, phpExtensionBundles: query.getAll('php-extension-bundle') || [], diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index bb4c382fa9..572d0dd6ee 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -17,25 +17,11 @@ import virtualModule from '../vite-virtual-module'; import { fileURLToPath } from 'node:url'; const proxy = { - '^/plugin-proxy.*&artifact=.*': { + '^/plugin-proxy': { target: 'https://playground.wordpress.net', changeOrigin: true, secure: true, }, - '/plugin-proxy': { - target: 'https://downloads.wordpress.org', - changeOrigin: true, - secure: true, - rewrite: (path: string) => { - const url = new URL(path, 'http://example.com'); - if (url.searchParams.has('plugin')) { - return `/plugin/${url.searchParams.get('plugin')}`; - } else if (url.searchParams.has('theme')) { - return `/theme/${url.searchParams.get('theme')}`; - } - throw new Error('Invalid request'); - }, - }, }; let buildVersion: string;