-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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 7 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 | ||
|
@@ -136,6 +136,28 @@ export function getEmbedEdit( title, icon ) { | |
return false; | ||
} | ||
|
||
/** | ||
* Finds the first iframe with a width and height and returns | ||
* an object with width and height attributes, empty object | ||
* if there is no iframe with width and height. | ||
* @param {string} html the preview HTML that possible contains an iframe with width and height set. | ||
* @return {Object} Object with extracted height and width if available. | ||
*/ | ||
getiFrameHeightWidth( 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 think this could be camel-cased as Iframe instead of iFrame. It looks like there's a |
||
const previewDom = document.createElement( 'div' ); | ||
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. Minor: DOM is an acronym and as such should be capitalized as |
||
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 walker = document.createTreeWalker( previewDom ); | ||
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 might be wrong, but I think it should be possible to simplify this to something like: const iframe = previewDom.querySelector( 'iframe' );
if ( iframe ) {
return {
height: iframe.height,
width: iframe.width,
};
} |
||
while ( walker.nextNode() ) { | ||
if ( 'IFRAME' === walker.currentNode.tagName ) { | ||
return { | ||
height: walker.currentNode.height, | ||
width: walker.currentNode.width, | ||
}; | ||
} | ||
} | ||
return {}; | ||
} | ||
|
||
/*** | ||
* Sets block attributes based on the preview data. | ||
*/ | ||
|
@@ -156,6 +178,46 @@ export function getEmbedEdit( title, icon ) { | |
if ( html || 'photo' === type ) { | ||
setAttributes( { type, providerNameSlug } ); | ||
} | ||
|
||
// If the embedded content is in an iframe with fixed width and height, we | ||
// calculate the aspect ratio and set an extra css class so that the rendered | ||
// content keeps the correct height no matter how wide the block is set to be. | ||
const { height, width } = this.getiFrameHeightWidth( html ); | ||
if ( undefined !== height && undefined !== width ) { | ||
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 wonder if most of this logic could be moved into |
||
const aspectRatio = ( width / 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, aspectRatioClassName ); | ||
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. Could avoid the concatenation here by making classnames do the work: const className = classnames( this.props.attributes.className, 'wp-has-aspect-ratio', aspectRatioClassName ); |
||
this.props.setAttributes( { className } ); | ||
} | ||
} | ||
} | ||
|
||
switchBackToURLInput() { | ||
|
@@ -257,6 +319,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 +346,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 +384,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,26 +79,13 @@ 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', | ||
|
@@ -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,8 +137,12 @@ class Sandbox extends Component { | |
body { | ||
margin: 0; | ||
} | ||
html, | ||
body, | ||
body > div, | ||
body > div > iframe { | ||
width: 100%; | ||
height: 100%; | ||
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. Note that this height property is necessary in order for the responsiveness to work, basically because every element that contains the embed needs an explicit height in order for a single percentage height to work properly. But if this component is used for things that aren't aspect ratio responsive, we need to scope it. Something like:
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. Ah, I see :) Ok, I'll see how it can be changed around to only get applied to things we're doing aspect ratio preservation on. 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. Feel free to send this PR right back at me if we can have a CSS class or separate style declaration for aspect ratio responsive stuff only. 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 would be cool to have an 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. Agreed. I was thinking |
||
} | ||
body > div > * { | ||
margin-top: 0 !important; /* has to have !important to override inline styles */ | ||
|
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.
Few minor things about the doc block.