From 0bdb3324df10a3a670e41e4079c4a3394cf4c768 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Mon, 27 Nov 2023 19:08:20 +0100 Subject: [PATCH] Browser: Disable the network support by default and add a UI control to enable it (#812) Disables the network support by default and adds a UI control to enable it. Why? https://github.com/WordPress/wordpress-playground/pull/724 introduced support for wp_safe_remote_get() request and enabled it by default on playground.wordpress.net. The problem is, all the requests block rendering of WordPress pages and noticeably slow down the site. Let's disable it by default for a lightweight user experience, and then add an easy way to enable it, for example in the configuration modal. Closes #755 ## Testing Instructions * Go to http://localhost:5400/website-server/?url=/wp-admin/plugin-install.php * Confirm the plugins aren't loaded and you see a communicative error message * Go to the settings modal * Enable the network support * Confirm the plugins are loaded now CleanShot 2023-11-27 at 17 10 45@2x --- .github/workflows/ci.yml | 4 +- .../docs/site/docs/08-query-api/01-index.md | 2 +- .../docs/09-blueprints-api/03-data-format.md | 2 +- .../11-architecture/02-wasm-php-overview.md | 2 +- .../playground/blueprints/src/lib/compile.ts | 4 +- ...ges-for-plugins-and-themes-directories.php | 4 +- .../playground/website/cypress/e2e/app.cy.ts | 191 +++++++++++------- .../website/cypress/support/commands.ts | 10 + packages/playground/website/project.json | 3 +- .../playground-configuration-group/form.tsx | 29 ++- .../reload-with-new-configuration.ts | 5 + .../website/src/lib/resolve-blueprint.ts | 4 +- packages/playground/website/src/main.tsx | 1 + 13 files changed, 175 insertions(+), 86 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 494bf9e07a..2e96434975 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,14 +35,14 @@ jobs: - uses: actions/checkout@v3 - uses: ./.github/actions/prepare-playground - run: ./node_modules/.bin/cypress install --force - - run: npx nx affected --target=e2e --configuration=ci + - run: npx nx affected --target=e2e --configuration=ci --verbose build: runs-on: ubuntu-latest needs: [lint-and-typecheck] steps: - uses: actions/checkout@v3 - uses: ./.github/actions/prepare-playground - - run: npx nx affected --target=build --parallel=3 + - run: npx nx affected --target=build --parallel=3 --verbose # Deploy documentation job deploy_docs: 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 1a2ddce654..916062f9cc 100644 --- a/packages/docs/site/docs/08-query-api/01-index.md +++ b/packages/docs/site/docs/08-query-api/01-index.md @@ -27,7 +27,7 @@ You can go ahead and try it out. The Playground will automatically install the t | `wp` | `latest` | Loads the specified WordPress version. Supported values: `5.9`, `6.0`, `6.1`, `6.2`, `6.3`, `latest`, `nightly`, `beta` | | `blueprint-url` | | The URL of the Blueprint that will be used to configure this Playground instance. | | `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` | +| `networking` | `yes` or `no` | Enables or disables the networking support for Playground. Defaults to `no` | | `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 eb8e22509a..109586a8fc 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 @@ -63,4 +63,4 @@ The `phpExtensionBundles` property is an array of PHP extension bundles to load. 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. +- `networking`: Defaults to `false`. 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/docs/site/docs/11-architecture/02-wasm-php-overview.md b/packages/docs/site/docs/11-architecture/02-wasm-php-overview.md index b169dd422b..b860d80c7f 100644 --- a/packages/docs/site/docs/11-architecture/02-wasm-php-overview.md +++ b/packages/docs/site/docs/11-architecture/02-wasm-php-overview.md @@ -20,4 +20,4 @@ When it comes to networking, WebAssembly programs are limited to calling JavaScr In Node.js, the answer involves a WebSocket to TCP socket proxy, [Asyncify](https://emscripten.org/docs/porting/asyncify.html), and patching deep PHP internals like php_select. It's complex, but there's a reward. The Node.js-targeted PHP build can request web APIs, install composer packages, and even connect to a MySQL server. -In the browser, networking is not supported yet. Initiating a HTTPS connection involves opening a raw TCP socket which is not possible in the browser. There is an [open GitHub issue](https://github.com/WordPress/wordpress-playground/issues/85) that explores possible ways of addressing this problem. +In the browser, networking is supported to a limited extent. Network calls initiated using `wp_safe_remote_get`, like the ones in the plugin directory or the font library, are translated into `fetch()` calls and succeed if the remote server sends the correct CORS headers. However, a full support for arbitrary HTTPS connection involves opening a raw TCP socket which is not possible in the browser. There is an [open GitHub issue](https://github.com/WordPress/wordpress-playground/issues/85) that explores possible ways of addressing this problem. diff --git a/packages/playground/blueprints/src/lib/compile.ts b/packages/playground/blueprints/src/lib/compile.ts index 21e0dc5e56..1feef66572 100644 --- a/packages/playground/blueprints/src/lib/compile.ts +++ b/packages/playground/blueprints/src/lib/compile.ts @@ -167,8 +167,8 @@ export function compileBlueprint( blueprint.phpExtensionBundles || [] ), features: { - // Enable networking by default - networking: blueprint.features?.networking ?? true, + // Disable networking by default + networking: blueprint.features?.networking ?? false, }, run: async (playground: UniversalPHP) => { try { diff --git a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/2-nice-error-messages-for-plugins-and-themes-directories.php b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/2-nice-error-messages-for-plugins-and-themes-directories.php index 7ab5405410..d367b1a320 100644 --- a/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/2-nice-error-messages-for-plugins-and-themes-directories.php +++ b/packages/playground/blueprints/src/lib/steps/apply-wordpress-patches/wp-content/mu-plugins/2-nice-error-messages-for-plugins-and-themes-directories.php @@ -12,7 +12,7 @@ if($res instanceof WP_Error) { $res = new WP_Error( 'plugins_api_failed', - 'Playground does not yet support connecting to the plugin directory yet. You can still upload plugins or install them using the Query API (e.g. ?plugin=coblocks).' + 'Enable networking support in Playground settings to access the Plugins directory. Network access is an experimental, opt-in feature. If you don\'t want to use it, you can still upload plugins or install them using the Query API (e.g. ?plugin=coblocks).' ); } return $res; @@ -29,7 +29,7 @@ } if($translation === 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.') { - return 'Playground does not yet support connecting to the themes directory yet. You can still upload a theme or install it using the Query API (e.g. ?theme=pendant).'; + return 'Enable networking support in Playground settings to access the Themes directory. Network access is an experimental, opt-in feature. If you don\'t want to use it, you can still upload themes or install them using the Query API (e.g. ?theme=pendant).'; } return $translation; } ); diff --git a/packages/playground/website/cypress/e2e/app.cy.ts b/packages/playground/website/cypress/e2e/app.cy.ts index f2a13e4b35..945c52058e 100644 --- a/packages/playground/website/cypress/e2e/app.cy.ts +++ b/packages/playground/website/cypress/e2e/app.cy.ts @@ -33,7 +33,7 @@ describe('Query API', () => { describe('option `wp`', () => { it('should load WordPress latest by default', () => { - cy.visit('/?networking=no&url=/wp-admin/'); + cy.visit('/?url=/wp-admin/'); const expectedBodyClass = 'branch-' + LatestSupportedWordPressVersion.replace('.', '-'); cy.wordPressDocument() @@ -42,16 +42,14 @@ describe('Query API', () => { }); it('should load WordPress 6.3 when requested', () => { - cy.visit('/?networking=no&wp=6.3&url=/wp-admin'); + cy.visit('/?wp=6.3&url=/wp-admin'); cy.wordPressDocument().find(`body.branch-6-3`).should('exist'); }); }); describe('option `php-extension-bundle`', () => { it('should load the specified PHP extensions', () => { - cy.visit( - '/?networking=no&php-extension-bundle=kitchen-sink&url=/phpinfo.php' - ); + cy.visit('/?php-extension-bundle=kitchen-sink&url=/phpinfo.php'); cy.wordPressDocument() .its('body') .should('contain', '--enable-xmlwriter'); @@ -60,13 +58,10 @@ describe('Query API', () => { describe('option `networking`', () => { it('should disable networking when requested', () => { - cy.visit('/?networking=no&url=/wp-admin/plugin-install.php'); + cy.visit('/?url=/wp-admin/plugin-install.php'); cy.wordPressDocument() .find('.notice.error') - .should( - 'contain', - 'does not yet support connecting to the plugin directory' - ); + .should('contain', 'Enable networking support in Playground'); }); it('should enable networking when requested', () => { @@ -97,7 +92,7 @@ describe('Query API', () => { describe('option `url`', () => { it('should load the specified URL', () => { - cy.visit('/?url=/wp-admin/&networking=no'); + cy.visit('/?url=/wp-admin/'); cy.wordpressPath().should('contain', '/wp-admin/'); cy.wordPressDocument() .find('#adminmenu') @@ -107,11 +102,11 @@ describe('Query API', () => { describe('option `mode`', () => { it('lack of mode=seamless should a WordPress in a simulated browser UI', () => { - cy.visit('/?networking=no'); + cy.visit('/'); cy.get('[data-cy="simulated-browser"]').should('exist'); }); it('mode=seamless should load a fullscreen WordPress', () => { - cy.visit('/?networking=no&mode=seamless'); + cy.visit('/?mode=seamless'); cy.get('[data-cy="simulated-browser"]').should('not.exist'); }); }); @@ -187,7 +182,7 @@ describe('Query API', () => { }); describe('playground-website', () => { - beforeEach(() => cy.visit('/?networking=no')); + beforeEach(() => cy.visit('/')); it('should reflect the URL update from the navigation bar in the WordPress site', () => { cy.setWordPressUrl('/wp-admin'); @@ -195,83 +190,141 @@ describe('playground-website', () => { }); // Test all PHP versions for completeness - SupportedPHPVersions.forEach((version) => { - it('should switch PHP version to ' + version, () => { + describe('PHP version switcher', () => { + SupportedPHPVersions.forEach((version) => { + it('should switch PHP version to ' + version, () => { + // Update settings in Playground configurator + cy.get('button#configurator').click(); + cy.get('select#php-version').select(version); + cy.get('#modal-content button[type=submit]').click(); + // Wait for the page to finish reloading + cy.url().should('contain', `php=${version}`); + cy.document().should('exist'); + + // Go to phpinfo + cy.setWordPressUrl('/phpinfo.php'); + cy.wordPressDocument() + .find('h1') + .should('contain', 'PHP Version ' + version); + }); + }); + }); + + // Only test the latest PHP version to save time + describe('PHP extensions bundle', () => { + it('should load additional PHP extensions when requested', () => { // Update settings in Playground configurator cy.get('button#configurator').click(); - cy.get('select#php-version').select(version); + cy.get('select#php-version').select(LatestSupportedPHPVersion); + cy.get('input[name=with-extensions]').check(); cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish reloading - cy.url().should('contain', `&php=${version}`); + // Wait for the page to finish loading cy.document().should('exist'); // Go to phpinfo cy.setWordPressUrl('/phpinfo.php'); cy.wordPressDocument() - .find('h1') - .should('contain', 'PHP Version ' + version); + .its('body') + .should('contain', '--enable-xmlwriter'); + }); + + it('should not load additional PHP extensions when not requested', () => { + // Update settings in Playground configurator + cy.get('button#configurator').click(); + cy.get('select#php-version').select(LatestSupportedPHPVersion); + cy.get('#modal-content button[type=submit]').click(); + // Wait for the page to finish loading + cy.document().should('exist'); + + // Go to phpinfo + cy.setWordPressUrl('/phpinfo.php'); + cy.wordPressDocument() + .its('body') + .should('contain', '--without-libxml'); }); }); - // Only test the latest PHP version to save time - it('should load additional PHP extensions when requested', () => { - // Update settings in Playground configurator + // Test all WordPress versions for completeness + describe('WordPress version selector', () => { + for (const version in SupportedWordPressVersions) { + if (version === 'beta') { + continue; + } + // @ts-ignore + let versionMessage = 'Version ' + version; + if (version === 'nightly') { + versionMessage = 'You are using a development version'; + } + + it('should switch WordPress version to ' + version, () => { + // Update settings in Playground configurator + cy.get('button#configurator').click(); + cy.get(`select#wp-version option[value="${version}"]`).should( + 'exist' + ); + cy.get('select#wp-version').select(`${version}`); + cy.get('#modal-content button[type=submit]').click(); + // Wait for the page to finish loading + cy.url().should('contain', `&wp=${version}`); + + // Go to phpinfo + cy.setWordPressUrl('/wp-admin'); + cy.wordPressDocument() + .find('#footer-upgrade') + .should('contain', versionMessage); + }); + } + }); +}); + +/** + * These tests only check if the modal UI updates the URL correctly. + * The actual networking functionality is tested in the Query API tests. + */ +describe('Website UI – Networking support', () => { + it('should display an unchecked networking checkbox by default', () => { + cy.visit('/'); + cy.get('button#configurator').click(); - cy.get('select#php-version').select(LatestSupportedPHPVersion); - cy.get('input[name=with-extensions]').check(); - cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish loading - cy.document().should('exist'); + cy.get('input[name=with-networking]').should('not.be.checked'); + }); - // Go to phpinfo - cy.setWordPressUrl('/phpinfo.php'); - cy.wordPressDocument() - .its('body') - .should('contain', '--enable-xmlwriter'); + it('should display a checked networking checkbox when networking is enabled', () => { + cy.visit('/?networking=yes'); + + cy.get('button#configurator').click(); + cy.get('input[name=with-networking]').should('be.checked'); }); - it('should not load additional PHP extensions when not requested', () => { + it('should enable networking when requested', () => { + cy.visit('/'); + // Update settings in Playground configurator cy.get('button#configurator').click(); - cy.get('select#php-version').select(LatestSupportedPHPVersion); + cy.get('input[name=with-networking]').check(); cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish loading + + // Wait for the page to reload cy.document().should('exist'); + cy.get('#modal-content button[type=submit]').should('not.exist'); - // Go to phpinfo - cy.setWordPressUrl('/phpinfo.php'); - cy.wordPressDocument() - .its('body') - .should('contain', '--without-libxml'); + // Confirm the URL was updated correctly + cy.relativeUrl().should('contain', 'networking=yes'); }); - // Test all WordPress versions for completeness - for (const version in SupportedWordPressVersions) { - if (version === 'beta') { - continue; - } - // @ts-ignore - let versionMessage = 'Version ' + version; - if (version === 'nightly') { - versionMessage = 'You are using a development version'; - } + it('should disable networking when requested', () => { + cy.visit('/?networking=yes'); - it('should switch WordPress version to ' + version, () => { - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get(`select#wp-version option[value="${version}"]`).should( - 'exist' - ); - cy.get('select#wp-version').select(`${version}`); - cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish loading - cy.url().should('contain', `&wp=${version}`); + // Update settings in Playground configurator + cy.get('button#configurator').click(); + cy.get('input[name=with-networking]').uncheck(); + cy.get('#modal-content button[type=submit]').click(); - // Go to phpinfo - cy.setWordPressUrl('/wp-admin'); - cy.wordPressDocument() - .find('#footer-upgrade') - .should('contain', versionMessage); - }); - } + // Wait for the page to reload + cy.document().should('exist'); + cy.get('#modal-content button[type=submit]').should('not.exist'); + + // Confirm the URL was updated correctly + cy.relativeUrl().should('not.contain', 'networking=yes'); + }); }); diff --git a/packages/playground/website/cypress/support/commands.ts b/packages/playground/website/cypress/support/commands.ts index 16407764fa..595fcbf052 100644 --- a/packages/playground/website/cypress/support/commands.ts +++ b/packages/playground/website/cypress/support/commands.ts @@ -17,6 +17,7 @@ declare namespace Cypress { setWordPressUrl(url: string): void; wordPressDocument(): Chainable; wordpressPath(): Chainable; + relativeUrl(): Chainable; } } @@ -45,3 +46,12 @@ Cypress.Commands.add('wordpressPath', () => { .find('#wp') .its('0.contentWindow.location.pathname'); }); + +Cypress.Commands.add('relativeUrl', () => { + // relative part of the current top-level URL + return cy.url().then((href) => { + const url = new URL(href); + console.log({ href }); + return href.substring(url.origin.length); + }); +}); diff --git a/packages/playground/website/project.json b/packages/playground/website/project.json index 93ce3ee596..783036eb63 100644 --- a/packages/playground/website/project.json +++ b/packages/playground/website/project.json @@ -37,7 +37,8 @@ "mode": "development" }, "production": { - "mode": "production" + "mode": "production", + "logLevel": "info" } }, "dependsOn": ["^build"] diff --git a/packages/playground/website/src/components/playground-configuration-group/form.tsx b/packages/playground/website/src/components/playground-configuration-group/form.tsx index 291a93573e..ca4c6fc901 100644 --- a/packages/playground/website/src/components/playground-configuration-group/form.tsx +++ b/packages/playground/website/src/components/playground-configuration-group/form.tsx @@ -13,6 +13,7 @@ export interface PlaygroundConfiguration { wp: string; php: SupportedPHPVersion; withExtensions: boolean; + withNetworking: boolean; storage: StorageType; resetSite?: boolean; } @@ -44,6 +45,9 @@ export function PlaygroundConfigurationForm({ const [withExtensions, setWithExtensions] = useState( initialData.withExtensions ); + const [withNetworking, setWithNetworking] = useState( + initialData.withNetworking + ); const [wp, setWp] = useState(initialData.wp); const handleStorageChange = async ( event: React.ChangeEvent @@ -69,7 +73,8 @@ export function PlaygroundConfigurationForm({ storage, wp, resetSite, - withExtensions: withExtensions, + withExtensions, + withNetworking, }); } @@ -280,6 +285,20 @@ export function PlaygroundConfigurationForm({ />   Load extensions: libxml, mbstring, iconv, gd +
{/* - * Without an empty option, React sometimes says - * the current selected version is "nightly" when - * `wp` is actually "6.4". - */} + * Without an empty option, React sometimes says + * the current selected version is "nightly" when + * `wp` is actually "6.4". + */} {Object.keys(supportedWPVersions).map((version) => (