Skip to content

Commit

Permalink
Browser: Move patching the Gutenberg iframe into the service worker.
Browse files Browse the repository at this point in the history
As discussed in #42,
the Gutenberg plugin needs to be patched to ensure the editor iframe is
controlled by the service worker.

The previous patching approach was to patch the Gutenberg plugin in the
`install-plugin` step. This worked for the `install-plugin` step, but not for
the `import-site` step, because the Gutenberg plugin is already installed when
importing a site. That was fixed in another PR, but it's still not enough.

Turns out, there's a whole lot of ways to install the Gutenberg plugin:

* Install plugin step
* Import a site
* Install Gutenberg from the plugin directory
* Upload a Gutenberg zip

Since it's too difficult to patch Gutenberg in all these cases, this
commit blanket-patches all the scripts requested over the network whose
names seem to indicate they're related to the Gutenberg plugin.
  • Loading branch information
adamziel committed Jan 12, 2024
1 parent a9f7f1b commit b186f4e
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 156 deletions.
3 changes: 0 additions & 3 deletions packages/playground/blueprints/public/blueprint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,6 @@
"disableWpNewBlogNotification": {
"type": "boolean"
},
"makeEditorFrameControlled": {
"type": "boolean"
},
"prepareForRunningInsideWebBrowser": {
"type": "boolean"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export interface ApplyWordPressPatchesStep {
patchSecrets?: boolean;
disableSiteHealth?: boolean;
disableWpNewBlogNotification?: boolean;
makeEditorFrameControlled?: boolean;
prepareForRunningInsideWebBrowser?: boolean;
addFetchNetworkTransport?: boolean;
}
Expand Down Expand Up @@ -50,12 +49,6 @@ export const applyWordPressPatches: StepHandler<
if (options.disableWpNewBlogNotification === true) {
await patch.disableWpNewBlogNotification();
}
if (options.makeEditorFrameControlled === true) {
await makeEditorFrameControlled(php, patch.wordpressPath, [
`${patch.wordpressPath}/wp-includes/js/dist/block-editor.js`,
`${patch.wordpressPath}/wp-includes/js/dist/block-editor.min.js`,
]);
}
if (options.prepareForRunningInsideWebBrowser === true) {
await patch.prepareForRunningInsideWebBrowser();
}
Expand Down Expand Up @@ -196,127 +189,3 @@ function randomString(length: number) {
result += chars[Math.floor(Math.random() * chars.length)];
return result;
}

export async function applyGutenbergPatchOnce(playground: UniversalPHP) {
/**
* Ensures the block editor iframe is controlled by the playground
* service worker. Tl;dr it must use a HTTP URL as its src, not a
* data URL, blob URL, or a srcDoc like it does by default.
*
* @see https://github.com/WordPress/wordpress-playground/pull/668
*/
const documentRoot = await playground.documentRoot;
if (
(await playground.isDir(
documentRoot + '/wp-content/plugins/gutenberg'
)) &&
!(await playground.fileExists(documentRoot + '/.gutenberg-patched'))
) {
await playground.writeFile(documentRoot + '/.gutenberg-patched', '1');
await makeEditorFrameControlled(playground, documentRoot, [
`${documentRoot}/wp-content/plugins/gutenberg/build/block-editor/index.js`,
`${documentRoot}/wp-content/plugins/gutenberg/build/block-editor/index.min.js`,
]);
}
}

/**
*
* Pair the site editor's nested iframe to the Service Worker.
*
* Without the patch below, the site editor initiates network requests that
* aren't routed through the service worker. That's a known browser issue:
*
* * https://bugs.chromium.org/p/chromium/issues/detail?id=880768
* * https://bugzilla.mozilla.org/show_bug.cgi?id=1293277
* * https://github.com/w3c/ServiceWorker/issues/765
*
* The problem with iframes using srcDoc and src="about:blank" as they
* fail to inherit the root site's service worker.
*
* Gutenberg loads the site editor using <iframe srcDoc="<!doctype html">
* to force the standards mode and not the quirks mode:
*
* https://github.com/WordPress/gutenberg/pull/38855
*
* This commit patches the site editor to achieve the same result via
* <iframe src="/doctype.html"> and a doctype.html file containing just
* `<!doctype html>`. This allows the iframe to inherit the service worker
* and correctly load all the css, js, fonts, images, and other assets.
*
* Ideally this issue would be fixed directly in Gutenberg and the patch
* below would be removed.
*
* See https://github.com/WordPress/wordpress-playground/issues/42 for more details
*/
export async function makeEditorFrameControlled(
php: UniversalPHP,
wordpressPath: string,
blockEditorScripts: string[]
) {
const controlledIframe = `
/**
* A synchronous function to read a blob URL as text.
*
* @param {string} url
* @returns {string}
*/
const __playground_readBlobAsText = function (url) {
try {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.overrideMimeType('text/plain;charset=utf-8');
xhr.send();
return xhr.responseText;
} catch(e) {
return '';
} finally {
URL.revokeObjectURL(url);
}
}
window.__playground_ControlledIframe = window.wp.element.forwardRef(function (props, ref) {
const source = window.wp.element.useMemo(function () {
if (props.srcDoc) {
// WordPress <= 6.2 uses a srcDoc that only contains a doctype.
return '/wp-includes/empty.html';
} else if (props.src && props.src.startsWith('blob:')) {
// WordPress 6.3 uses a blob URL with doctype and a list of static assets.
// Let's pass the document content to empty.html and render it there.
return '/wp-includes/empty.html#' + encodeURIComponent(__playground_readBlobAsText(props.src));
} else {
// WordPress >= 6.4 uses a plain HTTPS URL that needs no correction.
return props.src;
}
}, [props.src]);
return (
window.wp.element.createElement('iframe', {
...props,
ref: ref,
src: source,
// Make sure there's no srcDoc, as it would interfere with the src.
srcDoc: undefined
})
)
});`;

for (const filePath of blockEditorScripts) {
if (!(await php.fileExists(filePath))) {
continue;
}
await updateFile(
php,
filePath,
// The original version of this function crashes WASM PHP, let's define an empty one instead.
(contents) =>
`${controlledIframe} ${contents.replace(
/\(\s*"iframe",/,
'(__playground_ControlledIframe,'
)}`
);
}
await php.writeFile(
`${wordpressPath}/wp-includes/empty.html`,
'<!doctype html><script>const hash = window.location.hash.substring(1); if ( hash ) document.write(decodeURIComponent(hash))</script>'
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { unzip } from './unzip';
import { dirname, joinPaths, phpVar } from '@php-wasm/util';
import { UniversalPHP } from '@php-wasm/universal';
import { wpContentFilesExcludedFromExport } from '../utils/wp-content-files-excluded-from-exports';
import {
applyGutenbergPatchOnce,
applyWordPressPatches,
} from './apply-wordpress-patches';
import { applyWordPressPatches } from './apply-wordpress-patches';

/**
* @inheritDoc importWordPressFiles
Expand Down Expand Up @@ -126,13 +123,6 @@ export const importWordPressFiles: StepHandler<
require ${upgradePhp};
`,
});

// Ensure the editor frame is controlled, see the
// applyGutenbergPatchOnce() function for details.
await applyWordPressPatches(playground, {
makeEditorFrameControlled: true,
});
await applyGutenbergPatchOnce(playground);
};

async function removePath(playground: UniversalPHP, path: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { StepHandler } from '.';
import { installAsset } from './install-asset';
import { activatePlugin } from './activate-plugin';
import { applyGutenbergPatchOnce } from './apply-wordpress-patches';
import { zipNameToHumanName } from '../utils/zip-name-to-human-name';

/**
Expand Down Expand Up @@ -82,8 +81,6 @@ export const installPlugin: StepHandler<InstallPluginStep<File>> = async (
progress
);
}

await applyGutenbergPatchOnce(playground);
} catch (error) {
console.error(
`Proceeding without the ${zipNiceName} plugin. Could not install it in wp-admin. ` +
Expand Down
130 changes: 130 additions & 0 deletions packages/playground/remote/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ initializeServiceWorker({
}
event.preventDefault();
async function asyncHandler() {
if (fullUrl.pathname.endsWith('/wp-includes/empty.html')) {
return emptyHtml();
}

const { staticAssetsDirectory } = await getScopedWpDetails(scope!);
const workerResponse = await convertFetchEventToPHPRequest(event);
if (
Expand All @@ -52,12 +56,138 @@ initializeServiceWorker({
);
return fetch(request);
}

// Path the block-editor.js file to ensure the site editor's iframe
// inherits the service worker.
// @see controlledIframe below for more details.
if (
// WordPress Core version of block-editor.js
unscopedUrl.pathname.endsWith('/js/dist/block-editor.js') ||
unscopedUrl.pathname.endsWith('/js/dist/block-editor.min.js') ||
// Gutenberg version of block-editor.js
unscopedUrl.pathname.endsWith('build/block-editor/index.js') ||
unscopedUrl.pathname.endsWith('build/block-editor/index.min.js')
) {
const script = await workerResponse.text();
const newScript = `${controlledIframe} ${script.replace(
/\(\s*"iframe",/,
'(__playground_ControlledIframe,'
)}`;
return new Response(newScript, {
status: workerResponse.status,
statusText: workerResponse.statusText,
headers: workerResponse.headers,
});
}

return workerResponse;
}
return asyncHandler();
},
});

/**
* Pair the site editor's nested iframe to the Service Worker.
*
* Without the patch below, the site editor initiates network requests that
* aren't routed through the service worker. That's a known browser issue:
*
* * https://bugs.chromium.org/p/chromium/issues/detail?id=880768
* * https://bugzilla.mozilla.org/show_bug.cgi?id=1293277
* * https://github.com/w3c/ServiceWorker/issues/765
*
* The problem with iframes using srcDoc and src="about:blank" as they
* fail to inherit the root site's service worker.
*
* Gutenberg loads the site editor using <iframe srcDoc="<!doctype html">
* to force the standards mode and not the quirks mode:
*
* https://github.com/WordPress/gutenberg/pull/38855
*
* This commit patches the site editor to achieve the same result via
* <iframe src="/doctype.html"> and a doctype.html file containing just
* `<!doctype html>`. This allows the iframe to inherit the service worker
* and correctly load all the css, js, fonts, images, and other assets.
*
* Ideally this issue would be fixed directly in Gutenberg and the patch
* below would be removed.
*
* See https://github.com/WordPress/wordpress-playground/issues/42 for more details
*
* ## Why does this code live in the service worker?
*
* There's many ways to install the Gutenberg plugin:
*
* * Install plugin step
* * Import a site
* * Install Gutenberg from the plugin directory
* * Upload a Gutenberg zip
*
* It's too difficult to patch Gutenberg in all these cases, so we blanket-patch
* all the scripts requested over the network whose names seem to indicate they're
* related to the Gutenberg plugin.
*/
const controlledIframe = `
/**
* A synchronous function to read a blob URL as text.
*
* @param {string} url
* @returns {string}
*/
const __playground_readBlobAsText = function (url) {
try {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.overrideMimeType('text/plain;charset=utf-8');
xhr.send();
return xhr.responseText;
} catch(e) {
return '';
} finally {
URL.revokeObjectURL(url);
}
}
window.__playground_ControlledIframe = window.wp.element.forwardRef(function (props, ref) {
const source = window.wp.element.useMemo(function () {
if (props.srcDoc) {
// WordPress <= 6.2 uses a srcDoc that only contains a doctype.
return '/wp-includes/empty.html';
} else if (props.src && props.src.startsWith('blob:')) {
// WordPress 6.3 uses a blob URL with doctype and a list of static assets.
// Let's pass the document content to empty.html and render it there.
return '/wp-includes/empty.html#' + encodeURIComponent(__playground_readBlobAsText(props.src));
} else {
// WordPress >= 6.4 uses a plain HTTPS URL that needs no correction.
return props.src;
}
}, [props.src]);
return (
window.wp.element.createElement('iframe', {
...props,
ref: ref,
src: source,
// Make sure there's no srcDoc, as it would interfere with the src.
srcDoc: undefined
})
)
});`;

/**
* The empty HTML file loaded by the patched editor iframe.
*/
function emptyHtml() {
return new Response(
'<!doctype html><script>const hash = window.location.hash.substring(1); if ( hash ) document.write(decodeURIComponent(hash))</script>',
{
status: 200,
headers: {
'content-type': 'text/html',
},
}
);
}

type WPModuleDetails = {
staticAssetsDirectory: string;
};
Expand Down
1 change: 0 additions & 1 deletion packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ try {
disableWpNewBlogNotification: true,
addPhpInfo: true,
disableSiteHealth: true,
makeEditorFrameControlled: true,
prepareForRunningInsideWebBrowser: true,
});
}
Expand Down
5 changes: 0 additions & 5 deletions packages/playground/website/cypress/fixtures/example.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/playground/website/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"cypressConfig": "packages/playground/website/cypress.config.ts",
"testingType": "e2e",
"devServerTarget": "playground-website:preview",
"browser": "chrome"
"browser": "chromium"
},
"dependsOn": ["build"]
},
Expand Down
1 change: 0 additions & 1 deletion packages/playground/website/public/wordpress.html
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@
wordpressPath: '/wordpress',
addPhpInfo: true,
disableSiteHealth: true,
makeEditorFrameControlled: true,
prepareForRunningInsideWebBrowser: true,
},
{
Expand Down

0 comments on commit b186f4e

Please sign in to comment.