Skip to content

Commit

Permalink
Replace injected html with sandboxing iframe (#1392)
Browse files Browse the repository at this point in the history
  • Loading branch information
notnownikki authored Jun 28, 2017
1 parent 1041a8e commit a65909d
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 14 deletions.
10 changes: 7 additions & 3 deletions blocks/library/embed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { includes } from 'lodash';
*/
import { __, sprintf } from 'i18n';
import { Component } from 'element';
import { Button, Placeholder, HtmlEmbed, Spinner } from 'components';
import { Button, Placeholder, Spinner, SandBox } from 'components';

/**
* Internal dependencies
Expand All @@ -22,6 +22,7 @@ import BlockAlignmentToolbar from '../../block-alignment-toolbar';

const { attr, children } = query;

// These embeds do not work in sandboxes
const HOSTS_NO_PREVIEWS = [ 'facebook.com' ];

function getEmbedBlockSettings( { title, icon, category = 'embed' } ) {
Expand Down Expand Up @@ -72,7 +73,9 @@ function getEmbedBlockSettings( { title, icon, category = 'embed' } ) {
}

getPhotoHtml( photo ) {
const photoPreview = <p><img src={ photo.thumbnail_url } alt={ photo.title } /></p>;
// 100% width for the preview so it fits nicely into the document, some "thumbnails" are
// acually the full size photo.
const photoPreview = <p><img src={ photo.thumbnail_url } alt={ photo.title } width="100%" /></p>;
return wp.element.renderToString( photoPreview );
}

Expand Down Expand Up @@ -158,6 +161,7 @@ function getEmbedBlockSettings( { title, icon, category = 'embed' } ) {

const parsedUrl = parse( url );
const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedUrl.host.replace( /^www\./, '' ) );
const iframeTitle = 'Embedded content from ' + parsedUrl.host;
let typeClassName = 'blocks-embed';

if ( 'video' === type ) {
Expand All @@ -173,7 +177,7 @@ function getEmbedBlockSettings( { title, icon, category = 'embed' } ) {
<p className="components-placeholder__error">{ __( 'Previews for this are unavailable in the editor, sorry!' ) }</p>
</Placeholder>
) : (
<HtmlEmbed html={ html } />
<SandBox html={ html } title={ iframeTitle } />
) }
{ ( caption && caption.length > 0 ) || !! focus ? (
<Editable
Expand Down
3 changes: 2 additions & 1 deletion components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ export { default as ClipboardButton } from './clipboard-button';
export { default as Dashicon } from './dashicon';
export { default as FormToggle } from './form-toggle';
export { default as FormTokenField } from './form-token-field';
export { default as HtmlEmbed } from './html-embed';
export { default as IconButton } from './icon-button';
export { default as Panel } from './panel';
export { default as PanelHeader } from './panel/header';
export { default as PanelBody } from './panel/body';
export { default as Placeholder } from './placeholder';
export { default as ResizableIframe } from './resizable-iframe';
export { default as ResponsiveWrapper } from './responsive-wrapper';
export { default as SandBox } from './sandbox';
export { default as Spinner } from './spinner';
export { default as Toolbar } from './toolbar';
export { default as Popover } from './popover';
Expand Down
173 changes: 173 additions & 0 deletions components/resizable-iframe/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* Imported from Calypso, with some lint fixes and gutenburg specific changes.
*/

/**
* External dependencies
*/
import { omit } from 'lodash';

export default class ResizableIframe extends wp.element.Component {

constructor() {
super( ...arguments );
this.state = {
width: 0,
height: 0,
};
this.getFrameBody = this.getFrameBody.bind( this );
this.maybeConnect = this.maybeConnect.bind( this );
this.isFrameAccessible = this.isFrameAccessible.bind( this );
this.checkMessageForResize = this.checkMessageForResize.bind( this );
}

static get defaultProps() {
return {
onLoad: () => {},
onResize: () => {},
title: '',
};
}

componentDidMount() {
window.addEventListener( 'message', this.checkMessageForResize, false );
this.maybeConnect();
}

componentDidUpdate() {
this.maybeConnect();
}

componentWillUnmount() {
window.removeEventListener( 'message', this.checkMessageForResize );
}

getFrameBody() {
return this.iframe.contentDocument.body;
}

maybeConnect() {
if ( ! this.isFrameAccessible() ) {
return;
}

const body = this.getFrameBody();
if ( null !== body.getAttribute( 'data-resizable-iframe-connected' ) ) {
return;
}

const script = document.createElement( 'script' );
script.innerHTML = `
( function() {
var observer;
if ( ! window.MutationObserver || ! document.body || ! window.top ) {
return;
}
function sendResize() {
window.top.postMessage( {
action: 'resize',
width: document.body.offsetWidth,
height: document.body.offsetHeight
}, '*' );
}
observer = new MutationObserver( sendResize );
observer.observe( document.body, {
attributes: true,
attributeOldValue: false,
characterData: true,
characterDataOldValue: false,
childList: true,
subtree: true
} );
window.addEventListener( 'load', sendResize, 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 ) {
[ 'width', 'height', 'minHeight', 'maxHeight' ].forEach( function( style ) {
if ( /^\\d+(vmin|vmax|vh|vw)$/.test( ruleOrNode.style[ style ] ) ) {
ruleOrNode.style[ style ] = '';
}
} );
}
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 );
} );
document.body.style.position = 'absolute';
document.body.setAttribute( 'data-resizable-iframe-connected', '' );
// Make sure that we don't miss very quick loading documents here that the observer
// doesn't see load, but haven't completely loaded when we call sendResize for the
// first time.
setTimeout( sendResize, 1000 );
} )();
`;
body.appendChild( script );
}

isFrameAccessible() {
try {
return !! this.getFrameBody();
} catch ( e ) {
return false;
}
}

checkMessageForResize( event ) {
const iframe = this.iframe;

// 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 ( e ) {} // eslint-disable-line no-empty
}

// Verify that the mounted element is the source of the message
if ( ! iframe || iframe.contentWindow !== event.source ) {
return;
}

// Update the state only if the message is formatted as we expect, i.e.
// as an object with a 'resize' action, width, and height
const { action, width, height } = data;
const { width: oldWidth, height: oldHeight } = this.state;

if ( 'resize' === action && ( oldWidth !== width || oldHeight !== height ) ) {
this.setState( { width, height } );
this.props.onResize();
}
}

onLoad( event ) {
this.maybeConnect();
this.props.onLoad( event );
}

render() {
const omitProps = [ 'onResize' ];

if ( ! this.props.src ) {
omitProps.push( 'src' );
}
return (
<iframe
ref={ ( node ) => this.iframe = node }
title={ this.props.title }
scrolling="no"
{ ...omit( this.props, omitProps ) }
onLoad={ this.onLoad }
width={ this.props.width || this.state.width }
height={ this.props.height || this.state.height } />
);
}
}
25 changes: 15 additions & 10 deletions components/html-embed/index.js → components/sandbox/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/**
* WordPress dependencies
* Internal dependencies
*/
import { Component } from 'element';
import ResizableIframe from 'components/resizable-iframe';

// When embedding HTML from the WP oEmbed proxy, we need to insert it
// into a div and make sure any scripts get run. This component takes
// HTML and puts it into a div element, and creates and adds new script
// elements so all scripts get run as expected.
export default class Sandbox extends wp.element.Component {

export default class HtmlEmbed extends Component {
static get defaultProps() {
return {
html: '',
title: '',
};
}

componentDidMount() {
const body = this.node;
const { html = '' } = this.props;
const body = this.node.getFrameBody();
const { html } = this.props;

body.innerHTML = html;

Expand All @@ -32,7 +34,10 @@ export default class HtmlEmbed extends Component {

render() {
return (
<div ref={ ( node ) => this.node = node } />
<ResizableIframe
sandbox="allow-same-origin allow-scripts"
title={ this.props.title }
ref={ ( node ) => this.node = node } />
);
}
}

0 comments on commit a65909d

Please sign in to comment.