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

[Patch WordPress] Ensure block editor iframe is controlled by a service worker #668

Merged
merged 2 commits into from
Oct 9, 2023
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
99 changes: 61 additions & 38 deletions packages/playground/blueprints/src/lib/steps/install-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const installPlugin: StepHandler<InstallPluginStep<File>> = async (
);
}

await maybeApplyGutenbergPatch(playground);
await applyGutenbergPatchOnce(playground);
} catch (error) {
console.error(
`Proceeding without the ${zipNiceName} plugin. Could not install it in wp-admin. ` +
Expand All @@ -93,64 +93,87 @@ export const installPlugin: StepHandler<InstallPluginStep<File>> = async (
}
};

async function maybeApplyGutenbergPatch(playground: UniversalPHP) {
async function applyGutenbergPatchOnce(playground: UniversalPHP) {
/**
* The patch below is no longer necessary as it's been fixed upstream:
* 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.
*
* https://github.com/WordPress/gutenberg/pull/50875
* @see https://github.com/WordPress/wordpress-playground/pull/668
*
* It can be removed in the next few WordPress releases.
*
* ---
*
* 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
* The code below repeated in the WordPress bundler in
* compile-wordpress/build-assets/controlled-iframe.js.
*/

if (
(await playground.isDir('/wordpress/wp-content/plugins/gutenberg')) &&
!(await playground.fileExists('/wordpress/.gutenberg-patched'))
) {
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
})
)
});`;

await playground.writeFile('/wordpress/.gutenberg-patched', '1');
await updateFile(
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.js`,
(contents) =>
controlledIframe +
contents.replace(
/srcDoc:("[^"]+"|[^,]+)/g,
'src:"/wp-includes/empty.html"'
/\(\s*"iframe",/g,
'(window.__playground_ControlledIframe,'
)
);
await updateFile(
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.min.js`,
(contents) =>
controlledIframe +
contents.replace(
/srcDoc:("[^"]+"|[^,]+)/g,
'src:"/wp-includes/empty.html"'
/\(\s*"iframe",/g,
'(window.__playground_ControlledIframe,'
)
);
}
Expand Down
14 changes: 10 additions & 4 deletions packages/playground/compile-wordpress/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@ RUN cp -r wordpress wordpress-static && \
# below would be removed.
#
# See https://github.com/WordPress/wordpress-playground/issues/42 for more details

RUN echo '<!doctype html>' > wordpress-static/wp-includes/empty.html && \
sed -E 's#srcDoc:\s*("[^"]+"|[^,\n}]+)#src:"/wp-includes/empty.html"#g' -i wordpress-static/wp-includes/js/dist/block-editor.min.js && \
sed -E 's#srcDoc:\s*("[^"]+"|[^,\n}]+)#src:"/wp-includes/empty.html"#g' -i wordpress-static/wp-includes/js/dist/block-editor.js
COPY ./build-assets/controlled-iframe.js /root/

RUN echo '<!doctype html><script>const hash = window.location.hash.substring(1); if ( hash ) document.write(decodeURIComponent(hash))</script>' > wordpress-static/wp-includes/empty.html
# Replace a vanilla HTML iframe with Playground's ControlledIframe component in the block editor.
RUN sed -E 's#\(\s*"iframe",#(__playground_ControlledIframe,#g' -i wordpress-static/wp-includes/js/dist/block-editor.js && \
cat /root/controlled-iframe.js wordpress-static/wp-includes/js/dist/block-editor.js >> /root/block-editor.js && \
mv /root/block-editor.js wordpress-static/wp-includes/js/dist/ && \
sed -E 's#\(\s*"iframe",#(__playground_ControlledIframe,#g' -i wordpress-static/wp-includes/js/dist/block-editor.min.js && \
cat /root/controlled-iframe.js wordpress-static/wp-includes/js/dist/block-editor.min.js >> /root/block-editor.min.js && \
mv /root/block-editor.min.js wordpress-static/wp-includes/js/dist/;

# Move the static files to the final output directory
RUN mkdir /root/output/$OUT_FILENAME
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Ensures the block editor iframe is controlled by the playground
* service worker. The block-editor.js is patched to use `__playground_ControlledIframe`
* instead of the plain HTML iframe element.
*
* @see https://github.com/WordPress/wordpress-playground/pull/668
* @see https://github.com/WordPress/wordpress-playground/issues/42
*
* This code is repeated in the Gutenberg plugin patcher in
* playground/blueprints/src/lib/steps/install-plugin.ts
*/

/**
* 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
})
)
});
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype 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
@@ -1,4 +1,47 @@
this["wp"] = this["wp"] || {}; this["wp"]["blockEditor"] =
/**
* 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
})
)
});this["wp"] = this["wp"] || {}; this["wp"]["blockEditor"] =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
Expand Down Expand Up @@ -32271,7 +32314,7 @@ function Iframe(_ref3, ref) {
key: id
});
}), head);
return Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_1__["createElement"])(_wordpress_element__WEBPACK_IMPORTED_MODULE_1__["Fragment"], null, tabIndex >= 0 && before, Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_1__["createElement"])("iframe", Object(_babel_runtime_helpers_esm_extends__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"])({}, props, {
return Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_1__["createElement"])(_wordpress_element__WEBPACK_IMPORTED_MODULE_1__["Fragment"], null, tabIndex >= 0 && before, Object(_wordpress_element__WEBPACK_IMPORTED_MODULE_1__["createElement"])(__playground_ControlledIframe, Object(_babel_runtime_helpers_esm_extends__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"])({}, props, {
ref: Object(_wordpress_compose__WEBPACK_IMPORTED_MODULE_4__["useMergeRefs"])([ref, setRef]),
tabIndex: tabIndex,
title: Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_3__["__"])('Editor canvas')
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype 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
@@ -1,4 +1,47 @@
/******/ (function() { // webpackBootstrap
/**
* 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
})
)
});/******/ (function() { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ 6411:
Expand Down Expand Up @@ -20940,11 +20983,11 @@ function Iframe(_ref3, ref) {
key: id
});
}), head);
return (0,external_wp_element_namespaceObject.createElement)(external_wp_element_namespaceObject.Fragment, null, tabIndex >= 0 && before, (0,external_wp_element_namespaceObject.createElement)("iframe", _extends({}, props, {
return (0,external_wp_element_namespaceObject.createElement)(external_wp_element_namespaceObject.Fragment, null, tabIndex >= 0 && before, (0,external_wp_element_namespaceObject.createElement)(__playground_ControlledIframe, _extends({}, props, {
ref: (0,external_wp_compose_namespaceObject.useMergeRefs)([ref, setRef]),
tabIndex: tabIndex // Correct doctype is required to enable rendering in standards mode
,
src:"/wp-includes/empty.html",
srcDoc: "<!doctype html>",
title: (0,external_wp_i18n_namespaceObject.__)('Editor canvas')
}), iframeDocument && (0,external_wp_element_namespaceObject.createPortal)((0,external_wp_element_namespaceObject.createElement)(external_wp_element_namespaceObject.Fragment, null, (0,external_wp_element_namespaceObject.createElement)("head", {
ref: headRef
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html>
<!doctype html><script>const hash = window.location.hash.substring(1); if ( hash ) document.write(decodeURIComponent(hash))</script>
Loading
Loading