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

Replace injected html with sandboxing iframe #1392

Merged
merged 1 commit into from
Jun 28, 2017
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
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have been localized?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, see #1635

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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Sandbox" is a single word, shouldn't need the PascalCase-ing here.

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.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should keep this comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♀️ I mean, it's a direct copy, but with some react changes for how it handles the references, and lint fixes.


/**
* 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 );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works... most of the time. There's still something going on though that stops some embeds sending a resize when they load.

I'm going to tackle that specific problem in another PR. I have a good idea of how to solve it from looking at the embed code in Calypso, but I don't want to delay this and have this branch fall out of sync too much, as I'm not sure how long it will take to fully solve this.

} )();
`;
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 } />
);
}
}