Skip to content

Commit

Permalink
WP: Apply the "controlled iframe" patch in JavaScript, not in Dockerf…
Browse files Browse the repository at this point in the history
…ile (#703)

This PR moves applying a WordPress patch from the Dockerfile, where WP
is built into a `.data` file, to JavaScript, where it can be applied to
any arbitrary WordPress installation.

This is needed to patch WordPress branches in Pull Request previewer.
See #700.

## Testing instructions:

1. Run Playground with `npm run start`
2. Go to the block editor
3. Confirm that inserter's CSS is loaded (black + icon)
4. Install the Gutenberg plugin
5. Confirm that inserter's CSS is still loaded

Related to #668, #646.
  • Loading branch information
adamziel authored Oct 16, 2023
1 parent b0f45fa commit eaf5761
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 171 deletions.
3 changes: 3 additions & 0 deletions packages/playground/blueprints/public/blueprint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@
},
"disableWpNewBlogNotification": {
"type": "boolean"
},
"makeEditorFrameControlled": {
"type": "boolean"
}
},
"required": ["step"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ApplyWordPressPatchesStep {
patchSecrets?: boolean;
disableSiteHealth?: boolean;
disableWpNewBlogNotification?: boolean;
makeEditorFrameControlled?: boolean;
}

export const applyWordPressPatches: StepHandler<
Expand All @@ -40,6 +41,12 @@ 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`,
]);
}
};

class WordPressPatcher {
Expand Down Expand Up @@ -123,3 +130,105 @@ function randomString(length: number) {
result += chars[Math.floor(Math.random() * chars.length)];
return result;
}

/**
*
* 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>'
);
}
81 changes: 3 additions & 78 deletions packages/playground/blueprints/src/lib/steps/install-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StepHandler } from '.';
import { zipNameToHumanName } from './common';
import { installAsset } from './install-asset';
import { activatePlugin } from './activate-plugin';
import { makeEditorFrameControlled } from './apply-wordpress-patches';

/**
* @inheritDoc installPlugin
Expand Down Expand Up @@ -100,92 +101,16 @@ async function applyGutenbergPatchOnce(playground: UniversalPHP) {
* data URL, blob URL, or a srcDoc like it does by default.
*
* @see https://github.com/WordPress/wordpress-playground/pull/668
*
* 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,
await makeEditorFrameControlled(playground, '/wordpress', [
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.js`,
(contents) =>
controlledIframe +
contents.replace(
/\(\s*"iframe",/g,
'(window.__playground_ControlledIframe,'
)
);
await updateFile(
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.min.js`,
(contents) =>
controlledIframe +
contents.replace(
/\(\s*"iframe",/g,
'(window.__playground_ControlledIframe,'
)
);
]);
}
}

async function updateFile(
playground: UniversalPHP,
path: string,
callback: (contents: string) => string
) {
return await playground.writeFile(
path,
callback(await playground.readFileAsText(path))
);
}
40 changes: 3 additions & 37 deletions packages/playground/compile-wordpress/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,43 +43,6 @@ RUN cp -r wordpress wordpress-static && \
# Remove all empty directories
find . -type d -empty -delete

# 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
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
RUN mv wordpress-static/* /root/output/$OUT_FILENAME/
Expand Down Expand Up @@ -111,6 +74,9 @@ RUN cd wordpress && \
find ./ -type f -name '*.js' \
-not -path '*/wp-includes/blocks/*/*.min.js' \
-not -name 'wp-emoji-loader.min.js' \
# This file is patched in JavaScript and needs to
# be served from VFS. See #703
-not -path '*/wp-includes/js/dist/block-editor*.js' \
-delete

RUN cd wordpress && \
Expand Down

This file was deleted.

0 comments on commit eaf5761

Please sign in to comment.