-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from all commits
9255d29
5baab35
fdfa437
3720784
a81372e
f75ac26
6ed5c58
27c15c7
ea6d484
037607d
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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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, | ||
|
@@ -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' ); | ||
previewDom.innerHTML = html; | ||
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. 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. 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. Previously: #1334 (comment) (not as fun as it was previously since Photobucket appears to have since let their oEmbed API languish and die) 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. Looking at this now. It might have to be a regular expression that picks out the iframes, so we avoid creating actual elements. 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. Changed this to use a regex in #9770 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. Another simpler option might be |
||
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. | ||
*/ | ||
|
@@ -156,6 +209,8 @@ export function getEmbedEdit( title, icon ) { | |
if ( html || 'photo' === type ) { | ||
setAttributes( { type, providerNameSlug } ); | ||
} | ||
|
||
this.maybeSetAspectRatioClassName( html ); | ||
} | ||
|
||
switchBackToURLInput() { | ||
|
@@ -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" | ||
|
@@ -228,7 +284,7 @@ export function getEmbedEdit( title, icon ) { | |
<SandBox | ||
html={ html } | ||
title={ iframeTitle } | ||
type={ type } | ||
type={ sandboxClassnames } | ||
/> | ||
</div> | ||
); | ||
|
@@ -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 ); | ||
|
@@ -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, | ||
|
@@ -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> | ||
); | ||
}, | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
}, '*' ); | ||
} | ||
|
||
|
@@ -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 | ||
|
@@ -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; | ||
|
@@ -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 }> | ||
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. What is the 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. it's the |
||
<head> | ||
<title>{ this.props.title }</title> | ||
<style dangerouslySetInnerHTML={ { __html: style } } /> | ||
|
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 --> |
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 --> |
There was a problem hiding this comment.
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)