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

Set aspect ratio preserving CSS on embedded content with fixed size iframes #9500

Merged
merged 10 commits into from
Sep 7, 2018
125 changes: 105 additions & 20 deletions packages/block-library/src/embed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { parse } from 'url';
import { includes, kebabCase, toLower } from 'lodash';
import classnames from 'classnames';
import classnames from 'classnames/dedupe';

/**
* WordPress dependencies
Expand Down Expand Up @@ -43,6 +43,7 @@ export function getEmbedEdit( title, icon ) {
this.setUrl = this.setUrl.bind( this );
this.maybeSwitchBlock = this.maybeSwitchBlock.bind( this );
this.setAttributesFromPreview = this.setAttributesFromPreview.bind( this );
this.maybeSetAspectRatioClassName = this.maybeSetAspectRatioClassName.bind( this );

this.state = {
editingURL: false,
Expand Down Expand Up @@ -136,6 +137,58 @@ export function getEmbedEdit( title, icon ) {
return false;
}

/**
* Sets the appropriate CSS class to enforce an aspect ratio when the embed is resized
* if the HTML has an iframe with width and height set.
*
* @param {string} html The preview HTML that possibly contains an iframe with width and height set.
*/
maybeSetAspectRatioClassName( html ) {
const previewDom = document.createElement( 'div' );
Copy link
Member

Choose a reason for hiding this comment

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

Minor: DOM is an acronym and as such should be capitalized as previewDOM (reference)

previewDom.innerHTML = html;
Copy link
Member

Choose a reason for hiding this comment

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

I am very wary of the security implications of this line. This will allow JavaScript in the preview markup to be evaluated, even though we're never including it in the DOM. As a demonstration, paste the following into your console while viewing the editor:

document.createElement( 'div' ).innerHTML = '<img src=/ onerror=\'alert("haxd")\'>'

Now it's a question as to whether we consider embed preview markup to be "safe". Given that the Sandbox component exists, I'm operating on the assumption that the answer is no. Therefore, this is a security vulnerability if it comes to be released.

Copy link
Member

Choose a reason for hiding this comment

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

Previously: #1334 (comment) (not as fun as it was previously since Photobucket appears to have since let their oEmbed API languish and die)

Copy link
Member Author

Choose a reason for hiding this comment

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

Looking at this now. It might have to be a regular expression that picks out the iframes, so we avoid creating actual elements.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed this to use a regex in #9770

Copy link
Member

Choose a reason for hiding this comment

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

Another simpler option might be document.implementation.createHTMLDocument as a "sandbox" of sorts.

Example: https://github.com/aduth/hpq/blob/f17b3fdc9c5692e9f676f94c33a003d191c81bd6/src/index.js#L21-L23

const iframe = previewDom.querySelector( 'iframe' );

if ( ! iframe ) {
return;
}

if ( iframe.height && iframe.width ) {
const aspectRatio = ( iframe.width / iframe.height ).toFixed( 2 );
let aspectRatioClassName;

switch ( aspectRatio ) {
// Common video resolutions.
case '2.33':
aspectRatioClassName = 'wp-embed-aspect-21-9';
break;
case '2.00':
aspectRatioClassName = 'wp-embed-aspect-18-9';
break;
case '1.78':
aspectRatioClassName = 'wp-embed-aspect-16-9';
break;
case '1.33':
aspectRatioClassName = 'wp-embed-aspect-4-3';
break;
// Vertical video and instagram square video support.
case '1.00':
aspectRatioClassName = 'wp-embed-aspect-1-1';
break;
case '0.56':
aspectRatioClassName = 'wp-embed-aspect-9-16';
break;
case '0.50':
aspectRatioClassName = 'wp-embed-aspect-1-2';
break;
}

if ( aspectRatioClassName ) {
const className = classnames( this.props.attributes.className, 'wp-has-aspect-ratio', aspectRatioClassName );
this.props.setAttributes( { className } );
}
}
}

/***
* Sets block attributes based on the preview data.
*/
Expand All @@ -156,6 +209,8 @@ export function getEmbedEdit( title, icon ) {
if ( html || 'photo' === type ) {
setAttributes( { type, providerNameSlug } );
}

this.maybeSetAspectRatioClassName( html );
}

switchBackToURLInput() {
Expand Down Expand Up @@ -218,6 +273,7 @@ export function getEmbedEdit( title, icon ) {
const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedUrl.host.replace( /^www\./, '' ) );
// translators: %s: host providing embed content e.g: www.youtube.com
const iframeTitle = sprintf( __( 'Embedded content from %s' ), parsedUrl.host );
const sandboxClassnames = classnames( type, className );
const embedWrapper = 'wp-embed' === type ? (
<div
className="wp-block-embed__wrapper"
Expand All @@ -228,7 +284,7 @@ export function getEmbedEdit( title, icon ) {
<SandBox
html={ html }
title={ iframeTitle }
type={ type }
type={ sandboxClassnames }
/>
</div>
);
Expand Down Expand Up @@ -257,6 +313,24 @@ export function getEmbedEdit( title, icon ) {
};
}

const embedAttributes = {
url: {
type: 'string',
},
caption: {
type: 'array',
source: 'children',
selector: 'figcaption',
default: [],
},
type: {
type: 'string',
},
providerNameSlug: {
type: 'string',
},
};

function getEmbedBlockSettings( { title, description, icon, category = 'embed', transforms, keywords = [] } ) {
// translators: %s: Name of service (e.g. VideoPress, YouTube)
const blockDescription = description || sprintf( __( 'Add a block that displays content pulled from other sites, like Twitter, Instagram or YouTube.' ), title );
Expand All @@ -266,23 +340,7 @@ function getEmbedBlockSettings( { title, description, icon, category = 'embed',
icon,
category,
keywords,
attributes: {
url: {
type: 'string',
},
caption: {
type: 'array',
source: 'children',
selector: 'figcaption',
default: [],
},
type: {
type: 'string',
},
providerNameSlug: {
type: 'string',
},
},
attributes: embedAttributes,

supports: {
align: true,
Expand Down Expand Up @@ -320,11 +378,38 @@ function getEmbedBlockSettings( { title, description, icon, category = 'embed',

return (
<figure className={ embedClassName }>
{ `\n${ url }\n` /* URL needs to be on its own line. */ }
<div className="wp-block-embed__wrapper">
{ `\n${ url }\n` /* URL needs to be on its own line. */ }
</div>
{ ! RichText.isEmpty( caption ) && <RichText.Content tagName="figcaption" value={ caption } /> }
</figure>
);
},

deprecated: [
{
attributes: embedAttributes,
save( { attributes } ) {
const { url, caption, type, providerNameSlug } = attributes;

if ( ! url ) {
return null;
}

const embedClassName = classnames( 'wp-block-embed', {
[ `is-type-${ type }` ]: type,
[ `is-provider-${ providerNameSlug }` ]: providerNameSlug,
} );

return (
<figure className={ embedClassName }>
{ `\n${ url }\n` /* URL needs to be on its own line. */ }
{ ! RichText.isEmpty( caption ) && <RichText.Content tagName="figcaption" value={ caption } /> }
</figure>
);
},
},
],
};
}

Expand Down
55 changes: 55 additions & 0 deletions packages/block-library/src/embed/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,59 @@
figcaption {
@include caption-style();
}

// Add responsiveness to common aspect ratios.
&.wp-embed-aspect-21-9 .wp-block-embed__wrapper,
&.wp-embed-aspect-18-9 .wp-block-embed__wrapper,
&.wp-embed-aspect-16-9 .wp-block-embed__wrapper,
&.wp-embed-aspect-4-3 .wp-block-embed__wrapper,
&.wp-embed-aspect-1-1 .wp-block-embed__wrapper,
&.wp-embed-aspect-9-16 .wp-block-embed__wrapper,
&.wp-embed-aspect-1-2 .wp-block-embed__wrapper {
position: relative;

&::before {
content: "";
display: block;
padding-top: 50%; // Default to 2:1 aspect ratio.
}

iframe {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
}
}

&.wp-embed-aspect-21-9 .wp-block-embed__wrapper::before {
padding-top: 42.85%; // 9 / 21 * 100
}

&.wp-embed-aspect-18-9 .wp-block-embed__wrapper::before {
padding-top: 50%; // 9 / 18 * 100
}

&.wp-embed-aspect-16-9 .wp-block-embed__wrapper::before {
padding-top: 56.25%; // 9 / 16 * 100
}

&.wp-embed-aspect-4-3 .wp-block-embed__wrapper::before {
padding-top: 75%; // 3 / 4 * 100
}

&.wp-embed-aspect-1-1 .wp-block-embed__wrapper::before {
padding-top: 100%; // 1 / 1 * 100
}

&.wp-embed-aspect-9-6 .wp-block-embed__wrapper::before {
padding-top: 66.66%; // 6 / 9 * 100
}

&.wp-embed-aspect-1-2 .wp-block-embed__wrapper::before {
padding-top: 200%; // 2 / 1 * 100
}
}
40 changes: 11 additions & 29 deletions packages/components/src/sandbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,31 +79,18 @@ class Sandbox extends Component {
const observeAndResizeJS = `
( function() {
var observer;
var aspectRatio = false;
var iframe = false;

if ( ! window.MutationObserver || ! document.body || ! window.parent ) {
return;
}

function sendResize() {
var clientBoundingRect = document.body.getBoundingClientRect();
var height = aspectRatio ? Math.ceil( clientBoundingRect.width / aspectRatio ) : clientBoundingRect.height;

if ( iframe && aspectRatio ) {
// This is embedded content delivered in an iframe with a fixed aspect ratio,
// so set the height correctly and stop processing. The DOM mutation will trigger
// another event and the resize message will get posted.
if ( iframe.height != height ) {
iframe.height = height;
return;
}
}

window.parent.postMessage( {
action: 'resize',
width: clientBoundingRect.width,
height: height,
height: clientBoundingRect.height,
}, '*' );
}

Expand Down Expand Up @@ -139,20 +126,6 @@ class Sandbox extends Component {
document.body.style.width = '100%';
document.body.setAttribute( 'data-resizable-iframe-connected', '' );

// Make embedded content in an iframe with a fixed size responsive,
// keeping the correct aspect ratio.
var potentialIframe = document.body.children[0];
if ( 'DIV' === potentialIframe.tagName || 'SPAN' === potentialIframe.tagName ) {
potentialIframe = potentialIframe.children[0];
}
if ( potentialIframe && 'IFRAME' === potentialIframe.tagName ) {
if ( potentialIframe.width ) {
iframe = potentialIframe;
aspectRatio = potentialIframe.width / potentialIframe.height;
potentialIframe.width = '100%';
}
}

sendResize();

// Resize events can change the width of elements with 100% width, but we don't
Expand All @@ -164,9 +137,18 @@ class Sandbox extends Component {
body {
margin: 0;
}
html,
body,
body > div,
body > div > iframe {
width: 100%;
}
html.wp-has-aspect-ratio,
body.wp-has-aspect-ratio,
body.wp-has-aspect-ratio > div,
body.wp-has-aspect-ratio > div > iframe {
height: 100%;
}
body > div > * {
margin-top: 0 !important; /* has to have !important to override inline styles */
margin-bottom: 0 !important;
Expand All @@ -176,7 +158,7 @@ class Sandbox extends Component {
// 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
const htmlDoc = (
<html lang={ document.documentElement.lang }>
<html lang={ document.documentElement.lang } className={ this.props.type }>
Copy link
Member

Choose a reason for hiding this comment

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

What is the type prop meant to represent?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's the type that the oembed response contains, it was meant for styling video and image content differently, however I'm not sure it's actually used any more.

<head>
<title>{ this.props.title }</title>
<style dangerouslySetInnerHTML={ { __html: style } } />
Expand Down
4 changes: 3 additions & 1 deletion test/integration/full-content/fixtures/core__embed.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- wp:core/embed {"url":"https://example.com/"} -->
<figure class="wp-block-embed">
https://example.com/
<div class="wp-block-embed__wrapper">
https://example.com/
</div>
<figcaption>Embedded content from an example URL</figcaption>
</figure>
<!-- /wp:core/embed -->
2 changes: 1 addition & 1 deletion test/integration/full-content/fixtures/core__embed.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
]
},
"innerBlocks": [],
"originalContent": "<figure class=\"wp-block-embed\">\n https://example.com/\n <figcaption>Embedded content from an example URL</figcaption>\n</figure>"
"originalContent": "<figure class=\"wp-block-embed\">\n <div class=\"wp-block-embed__wrapper\">\n https://example.com/\n </div>\n <figcaption>Embedded content from an example URL</figcaption>\n</figure>"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"url": "https://example.com/"
},
"innerBlocks": [],
"innerHTML": "\n<figure class=\"wp-block-embed\">\n https://example.com/\n <figcaption>Embedded content from an example URL</figcaption>\n</figure>\n"
"innerHTML": "\n<figure class=\"wp-block-embed\">\n <div class=\"wp-block-embed__wrapper\">\n https://example.com/\n </div>\n <figcaption>Embedded content from an example URL</figcaption>\n</figure>\n"
},
{
"attrs": {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- wp:embed {"url":"https://example.com/"} -->
<figure class="wp-block-embed">
<figure class="wp-block-embed"><div class="wp-block-embed__wrapper">
https://example.com/
<figcaption>Embedded content from an example URL</figcaption></figure>
</div><figcaption>Embedded content from an example URL</figcaption></figure>
<!-- /wp:embed -->