Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser: Move patching the Gutenberg iframe into the service worker #940

Merged
merged 6 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,6 @@ 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';

/**
* @inheritDoc importWordPressFiles
Expand Down Expand Up @@ -126,13 +122,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
8 changes: 6 additions & 2 deletions packages/playground/website/cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ describe('Query API', () => {
it('should load PHP 8.0 by default', () => {
cy.visit('/?url=/phpinfo.php');
cy.wordPressDocument()
.find('h1')
.find('h1', {
timeout: 60_000,
})
.should('contain', 'PHP Version 8.0');
});

it('should load PHP 7.4 when requested', () => {
cy.visit('/?php=7.4&url=/phpinfo.php');
cy.wordPressDocument()
.find('h1')
.find('h1', {
timeout: 60_000,
})
.should('contain', 'PHP Version 7.4');
});
});
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
Loading
Loading