Skip to content
Merged
10 changes: 9 additions & 1 deletion packages/block-library/src/cover/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,14 @@ function CoverEdit( {
} );
} )();
// Update the block only when the featured image changes.
}, [ mediaUrl ] );
// The other dependencies are stable references (dispatch actions / setters).
}, [
mediaUrl,
__unstableMarkNextChangeAsNotPersistent,
setAttributes,
setOverlayColor,
useFeaturedImage,
] );

// instead of destructuring the attributes
// we define the url and background type
Expand Down Expand Up @@ -703,6 +710,7 @@ function CoverEdit( {
style={ mediaStyle }
>
<SandBox
allowSameOrigin
html={ embedHtml }
title="Background video"
styles={ [
Expand Down
11 changes: 6 additions & 5 deletions packages/block-library/src/embed/embed-preview.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* Internal dependencies
*/
import { getPhotoHtml } from './util';

/**
* External dependencies
*/
Expand All @@ -17,6 +12,11 @@ import { BlockIcon } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
import { getAuthority } from '@wordpress/url';

/**
* Internal dependencies
*/
import { getPhotoHtml } from './util';

/**
* Internal dependencies
*/
Expand Down Expand Up @@ -75,6 +75,7 @@ export default function EmbedPreview( {
) : (
<div className="wp-block-embed__wrapper">
<SandBox
allowSameOrigin
html={ html }
scripts={ scripts }
title={ iframeTitle }
Expand Down
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

### Enhancements

- `Sandbox`: Add `allowSameOrigin` prop to opt in to `allow-same-origin` on the iframe sandbox. The default is now `false`, removing same-origin access for user-controlled content like the Custom HTML block ([#77212](https://github.com/WordPress/gutenberg/pull/77212)).
- `FormToggle`: Update disabled styles [#77208](https://github.com/WordPress/gutenberg/pull/77208).
- `RadioControl`: Add support for disabled radio group [#77127](https://github.com/WordPress/gutenberg/pull/77127).
- `AlignmentMatrixControl`, `Button`, `Calendar`, `CheckboxControl`, `CircularOptionPicker`, `ColorPalette`, `DropdownMenu`, `FormToggle`, `FormTokenField`, `RadioControl`, `Snackbar`, `TabPanel`, `ToggleControl`: Use `--wpds-cursor-control` for interactive cursor styling ([#76786](https://github.com/WordPress/gutenberg/pull/76786)).
Expand Down
198 changes: 189 additions & 9 deletions packages/components/src/sandbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useRef,
useState,
useEffect,
useMemo,
} from '@wordpress/element';
import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';

Expand All @@ -14,6 +15,8 @@ import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';
*/
import type { SandBoxProps } from './types';

type SandBoxContentProps = Omit< SandBoxProps, 'allowSameOrigin' >;

const observeAndResizeJS = function () {
const { MutationObserver } = window;

Expand Down Expand Up @@ -115,25 +118,184 @@ const style = `
`;

/**
* This component provides an isolated environment for arbitrary HTML via iframes.
* Builds the full HTML document string for the sandbox iframe content.
*/
function buildSandBoxDocument( {
html,
title,
type,
styles,
scripts,
}: {
html: string;
title: string;
type?: string;
styles: string[];
scripts: string[];
} ): string {
const htmlDoc = (
<html lang={ document.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>
);

return '<!DOCTYPE html>' + renderToString( htmlDoc );
}

/**
* Isolated sandbox that uses the `srcdoc` attribute to render content
* without `allow-same-origin`. This is the default for user-controlled
* content (e.g., the HTML block) where same-origin access would be a
* security risk.
*
* ```jsx
* import { SandBox } from '@wordpress/components';
* Because `srcdoc` is a declarative attribute, the browser automatically
* re-renders the content when the iframe is moved in the DOM (e.g.,
* block reordering), so no `load` event listener is needed.
* The `message` listener is re-synced on every `load` so
* it follows the iframe if it's reparented into a different document.
*/
function IsolatedSandBox( {
html = '',
title = '',
type,
styles = [],
scripts = [],
onFocus,
tabIndex,
}: SandBoxContentProps ) {
const ref = useRef< HTMLIFrameElement >( null );
const [ width, setWidth ] = useState( 0 );
const [ height, setHeight ] = useState( 0 );

const srcDoc = useMemo(
() => buildSandBoxDocument( { html, title, type, styles, scripts } ),
[ html, title, type, styles, scripts ]
);

useEffect( () => {
const iframe = ref.current;
if ( ! iframe ) {
return;
}

function checkMessageForResize( event: MessageEvent ) {
// Verify that the mounted element is the source of the message.
// iframe.contentWindow is accessible cross-origin as a
// WindowProxy reference, so this check still works without
// allow-same-origin.
if ( ! iframe || iframe.contentWindow !== event.source ) {
return;
}

// Attempt to parse the message data as JSON if passed as string.
let data = event.data || {};

if ( 'string' === typeof data ) {
try {
data = JSON.parse( data );
} catch {}
}

// Update the state only if the message is formatted as we expect,
// i.e. as an object with a 'resize' action.
if ( 'resize' !== data.action ) {
return;
}

setWidth( data.width );
setHeight( data.height );
}

let currentView: Window | null = null;
function syncListener() {
const view = iframe?.ownerDocument?.defaultView ?? null;
if ( view === currentView ) {
return;
}

currentView?.removeEventListener(
'message',
checkMessageForResize
);

currentView = view;
currentView?.addEventListener( 'message', checkMessageForResize );
}

syncListener();
iframe.addEventListener( 'load', syncListener );

return () => {
iframe.removeEventListener( 'load', syncListener );
currentView?.removeEventListener(
'message',
checkMessageForResize
);
};
}, [] );

return (
<iframe
ref={ useMergeRefs( [ ref, useFocusableIframe() ] ) }
title={ title }
tabIndex={ tabIndex }
className="components-sandbox"
sandbox="allow-scripts allow-presentation"
srcDoc={ srcDoc }
onFocus={ onFocus }
width={ Math.ceil( width ) }
height={ Math.ceil( height ) }
/>
);
}

/**
* Same-origin sandbox that writes to `contentDocument` directly. This
* preserves the parent page's URL as the iframe's document URL, which
* provides a valid Referer header for nested iframes (required by
* providers like YouTube).
*
* const MySandBox = () => (
* <SandBox html="<p>Content</p>" title="SandBox" type="embed" />
* );
* ```
* Only used when `allowSameOrigin` is true — i.e., for server-fetched
* oEmbed previews that are not directly user-controlled.
*
* This implementation is intentionally kept close to the original
* pre-refactor code to preserve past bugfixes:
* - load listener for iframe re-initialization after DOM moves (#21916)
* - forceRerender guard to avoid unnecessary full rewrites (#20176)
*/
function SandBox( {
function SameOriginSandBox( {
html = '',
title = '',
type,
styles = [],
scripts = [],
onFocus,
tabIndex,
}: SandBoxProps ) {
}: SandBoxContentProps ) {
const ref = useRef< HTMLIFrameElement >( null );
const [ width, setWidth ] = useState( 0 );
const [ height, setHeight ] = useState( 0 );
Expand Down Expand Up @@ -292,4 +454,22 @@ function SandBox( {
);
}

/**
* This component provides an isolated environment for arbitrary HTML via iframes.
*
* ```jsx
* import { SandBox } from '@wordpress/components';
*
* const MySandBox = () => (
* <SandBox html="<p>Content</p>" title="SandBox" type="embed" />
* );
* ```
*/
function SandBox( { allowSameOrigin = false, ...contentProps }: SandBoxProps ) {
if ( allowSameOrigin ) {
return <SameOriginSandBox { ...contentProps } />;
}
return <IsolatedSandBox { ...contentProps } />;
}

export default SandBox;
Loading
Loading