Repository navigation
SandBox: Inject resize script into head to stop it leaking as text #79920
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
Changes from all commits
9652934
c3fce7f
e0ee615
bf9558d
177397c
1b406fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,11 +37,17 @@ export const VIEWPORT_UNIT_VALUE_REGEX = | |
| const observeAndResizeJS = function () { | ||
| const { MutationObserver } = window; | ||
|
|
||
| if ( ! MutationObserver || ! document.body || ! window.parent ) { | ||
| if ( ! MutationObserver || ! window.parent ) { | ||
| return; | ||
| } | ||
|
|
||
| function sendResize() { | ||
| // This runs in the document <head>, and as a `load`/`resize` listener, | ||
| // so the body may not exist yet. Bail rather than throw on a null body. | ||
| if ( ! document.body ) { | ||
| return; | ||
| } | ||
|
|
||
| const clientBoundingRect = document.body.getBoundingClientRect(); | ||
|
|
||
| window.parent.postMessage( | ||
|
|
@@ -54,60 +60,88 @@ const observeAndResizeJS = function () { | |
| ); | ||
| } | ||
|
|
||
| const observer = new MutationObserver( sendResize ); | ||
| observer.observe( document.body, { | ||
| attributes: true, | ||
| attributeOldValue: false, | ||
| characterData: true, | ||
| characterDataOldValue: false, | ||
| childList: true, | ||
| subtree: true, | ||
| } ); | ||
| // Runs the body-dependent setup once the document body is available. Reads | ||
| // `document.body` into a local so the null check narrows it for the rest of | ||
| // the function: this runs in <head> and via DOMContentLoaded, so the body | ||
| // isn't guaranteed to exist yet. | ||
| function connect() { | ||
| const { body } = document; | ||
| if ( ! body ) { | ||
| return; | ||
| } | ||
|
|
||
| window.addEventListener( 'load', sendResize, true ); | ||
| const observer = new MutationObserver( sendResize ); | ||
| observer.observe( body, { | ||
| attributes: true, | ||
| attributeOldValue: false, | ||
| characterData: true, | ||
| characterDataOldValue: false, | ||
| childList: true, | ||
| subtree: true, | ||
| } ); | ||
|
|
||
| // Hack: Remove viewport unit styles, as these are relative | ||
| // the iframe root and interfere with our mechanism for | ||
| // determining the unconstrained page bounds. | ||
| function removeViewportStyles( ruleOrNode: ElementCSSInlineStyle ) { | ||
| if ( ruleOrNode.style ) { | ||
| ( | ||
| [ 'width', 'height', 'minHeight', 'maxHeight' ] as const | ||
| ).forEach( function ( style ) { | ||
| if ( | ||
| /^\d*\.?\d+(?:vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/.test( | ||
| ruleOrNode.style[ style ] | ||
| ) | ||
| ) { | ||
| ruleOrNode.style[ style ] = ''; | ||
| } | ||
| } ); | ||
| } | ||
| } | ||
|
|
||
| // Hack: Remove viewport unit styles, as these are relative | ||
| // the iframe root and interfere with our mechanism for | ||
| // determining the unconstrained page bounds. | ||
| function removeViewportStyles( ruleOrNode: ElementCSSInlineStyle ) { | ||
| if ( ruleOrNode.style ) { | ||
| ( | ||
| [ 'width', 'height', 'minHeight', 'maxHeight' ] as const | ||
| ).forEach( function ( style ) { | ||
| if ( | ||
| /^\d*\.?\d+(?:vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/.test( | ||
| ruleOrNode.style[ style ] | ||
| ) | ||
| ) { | ||
| ruleOrNode.style[ style ] = ''; | ||
| Array.prototype.forEach.call( | ||
| document.querySelectorAll( '[style]' ), | ||
| removeViewportStyles | ||
| ); | ||
| Array.prototype.forEach.call( | ||
| document.styleSheets, | ||
| function ( stylesheet ) { | ||
| let rules; | ||
| try { | ||
| rules = stylesheet.cssRules || stylesheet.rules; | ||
| } catch { | ||
| // Reading `cssRules` throws a SecurityError for a | ||
| // cross-origin stylesheet (e.g. a remote <link> in the | ||
| // user's HTML). Skip those rather than aborting the setup. | ||
| return; | ||
| } | ||
| } ); | ||
| } | ||
| } | ||
| Array.prototype.forEach.call( rules, removeViewportStyles ); | ||
| } | ||
| ); | ||
|
|
||
| Array.prototype.forEach.call( | ||
| document.querySelectorAll( '[style]' ), | ||
| removeViewportStyles | ||
| ); | ||
| Array.prototype.forEach.call( | ||
| document.styleSheets, | ||
| function ( stylesheet ) { | ||
| Array.prototype.forEach.call( | ||
| stylesheet.cssRules || stylesheet.rules, | ||
| removeViewportStyles | ||
| ); | ||
| } | ||
| ); | ||
| body.style.position = 'absolute'; | ||
| body.style.width = '100%'; | ||
| body.setAttribute( 'data-resizable-iframe-connected', '' ); | ||
|
|
||
| document.body.style.position = 'absolute'; | ||
| document.body.style.width = '100%'; | ||
| document.body.setAttribute( 'data-resizable-iframe-connected', '' ); | ||
| sendResize(); | ||
| } | ||
|
|
||
| sendResize(); | ||
| window.addEventListener( 'load', sendResize, true ); | ||
|
|
||
| // Resize events can change the width of elements with 100% width, but we don't | ||
| // get an DOM mutations for that, so do the resize when the window is resized, too. | ||
| window.addEventListener( 'resize', sendResize, true ); | ||
|
|
||
| // This script is injected into the document <head> so it parses before the | ||
| // (possibly malformed) user content in <body>: an unclosed attribute quote | ||
| // in that content can't then swallow this <script> tag and dump its source | ||
| // into the preview as visible text. Because <head> runs before <body> | ||
| // exists, defer the body-dependent setup until the DOM is ready. | ||
| if ( document.body ) { | ||
| connect(); | ||
| } else { | ||
| document.addEventListener( 'DOMContentLoaded', connect ); | ||
| } | ||
| }; | ||
|
|
||
| // TODO: These styles shouldn't be coupled with WordPress. | ||
|
|
@@ -135,23 +169,48 @@ const style = ` | |
| `; | ||
|
|
||
| /** | ||
| * Builds the full HTML document string for the sandbox iframe content. | ||
| * Builds the full HTML document string for the sandbox iframe content. Shared | ||
| * by both the isolated (`srcdoc`) and same-origin (`contentDocument.write`) | ||
| * sandboxes so the two paths always emit the same markup — notably keeping the | ||
| * resize script in `<head>`, ahead of the user content, on both. A single | ||
| * builder means the write path cannot drift back to putting the script in | ||
| * `<body>` while the `srcdoc` path keeps it in `<head>`. | ||
| * | ||
| * `lang` is passed in rather than read here because the two callers source it | ||
| * from different documents: the isolated sandbox uses the module's `document`, | ||
| * while the same-origin sandbox uses the iframe's `ownerDocument`. They are | ||
| * normally the same document, but keeping the argument preserves each path's | ||
| * exact prior behavior. | ||
| * | ||
| * Exported for tests. | ||
| * | ||
| * @param props | ||
| * @param props.html User-supplied HTML for the iframe body. | ||
| * @param props.title Document title. | ||
| * @param props.type Optional class name for the `<html>`/`<body>` elements. | ||
| * @param props.styles CSS rule strings, injected as `<style>` tags in the head. | ||
| * @param props.scripts External script URLs, injected as `<script src>` in the | ||
| * body (embeds expect their scripts there). | ||
| * @param props.lang Language for the `<html lang>` attribute. | ||
| * @return The full `<!DOCTYPE html>…` document string. | ||
| */ | ||
| function buildSandBoxDocument( { | ||
| export function buildSandBoxDocument( { | ||
| html, | ||
| title, | ||
| type, | ||
| styles, | ||
| scripts, | ||
| lang, | ||
| }: { | ||
| html: string; | ||
| title: string; | ||
| type?: string; | ||
| styles: string[]; | ||
| scripts: string[]; | ||
| lang: string; | ||
| } ): string { | ||
| const htmlDoc = ( | ||
| <html lang={ document.documentElement.lang } className={ type }> | ||
| <html lang={ lang } className={ type }> | ||
| <head> | ||
| <title>{ title }</title> | ||
| <style dangerouslySetInnerHTML={ { __html: style } } /> | ||
|
|
@@ -161,18 +220,18 @@ function buildSandBoxDocument( { | |
| dangerouslySetInnerHTML={ { __html: rules } } | ||
| /> | ||
| ) ) } | ||
| </head> | ||
| <body | ||
| data-resizable-iframe-connected="data-resizable-iframe-connected" | ||
| className={ type } | ||
| > | ||
| <div dangerouslySetInnerHTML={ { __html: html } } /> | ||
| <script | ||
| type="text/javascript" | ||
| dangerouslySetInnerHTML={ { | ||
| __html: `(${ observeAndResizeJS.toString() })();`, | ||
| } } | ||
| /> | ||
| </head> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This it the meat of the PR moving the resize Because a script that has already been parsed in To keep the resize behaviour working, the body-dependent work is deferred until the DOM is ready. That's all the |
||
| <body | ||
| data-resizable-iframe-connected="data-resizable-iframe-connected" | ||
| className={ type } | ||
| > | ||
| <div dangerouslySetInnerHTML={ { __html: html } } /> | ||
| { scripts.map( ( src ) => ( | ||
| <script key={ src } src={ src } /> | ||
| ) ) } | ||
|
|
@@ -209,7 +268,18 @@ function IsolatedSandBox( { | |
| const [ height, setHeight ] = useState( 0 ); | ||
|
|
||
| const srcDoc = useMemo( | ||
| () => buildSandBoxDocument( { html, title, type, styles, scripts } ), | ||
| () => | ||
| buildSandBoxDocument( { | ||
| html, | ||
| title, | ||
|
ramonjd marked this conversation as resolved.
|
||
| type, | ||
| styles, | ||
| scripts, | ||
| // Read inside the memo: the `no-dom-globals-in-react-fc` lint | ||
| // rule forbids DOM globals in the render body. `lang` rarely | ||
| // changes after load, so leaving it out of the deps is fine. | ||
| lang: document.documentElement.lang, | ||
| } ), | ||
| [ html, title, type, styles, scripts ] | ||
| ); | ||
|
|
||
|
|
@@ -345,48 +415,25 @@ function SameOriginSandBox( { | |
| return; | ||
| } | ||
|
|
||
| // Put the html snippet into a html document, and then write it to the iframe's document | ||
| // we can use this in the future to inject custom styles or scripts. | ||
| // Scripts go into the body rather than the head, to support embedded content such as Instagram | ||
| // that expect the scripts to be part of the body. | ||
| const htmlDoc = ( | ||
| <html | ||
| lang={ ownerDocument.documentElement.lang } | ||
| className={ type } | ||
| > | ||
| <head> | ||
| <title>{ title }</title> | ||
| <style dangerouslySetInnerHTML={ { __html: style } } /> | ||
| { styles.map( ( rules, i ) => ( | ||
| <style | ||
| key={ i } | ||
| dangerouslySetInnerHTML={ { __html: rules } } | ||
| /> | ||
| ) ) } | ||
| </head> | ||
| <body | ||
| data-resizable-iframe-connected="data-resizable-iframe-connected" | ||
| className={ type } | ||
| > | ||
| <div dangerouslySetInnerHTML={ { __html: html } } /> | ||
| <script | ||
| type="text/javascript" | ||
| dangerouslySetInnerHTML={ { | ||
| __html: `(${ observeAndResizeJS.toString() })();`, | ||
| } } | ||
| /> | ||
| { scripts.map( ( src ) => ( | ||
| <script key={ src } src={ src } /> | ||
| ) ) } | ||
| </body> | ||
| </html> | ||
| ); | ||
|
|
||
| // Writing the document like this makes it act in the same way as if it was | ||
| // loaded over the network, so DOM creation and mutation, script execution, etc. | ||
| // all work as expected. | ||
| // Put the html snippet into a html document, and then write it to the | ||
| // iframe's document. The external `scripts` still go into the body to | ||
| // support embedded content such as Instagram that expects them there; | ||
| // only the resize helper lives in the head. | ||
| // | ||
| // Writing the document like this makes it act in the same way as if it | ||
| // was loaded over the network, so DOM creation and mutation, script | ||
| // execution, etc. all work as expected. | ||
| contentDocument.open(); | ||
| contentDocument.write( '<!DOCTYPE html>' + renderToString( htmlDoc ) ); | ||
| contentDocument.write( | ||
| buildSandBoxDocument( { | ||
| html, | ||
| title, | ||
| type, | ||
| styles, | ||
| scripts, | ||
| lang: ownerDocument.documentElement.lang, | ||
| } ) | ||
| ); | ||
| contentDocument.close(); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.