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

Improve handling of Tumblr embeds #5926

Merged
merged 28 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9de2cfc
Sanitize Tumblr embeds
pierlon Feb 25, 2021
a414ddf
Fix lint errors
pierlon Feb 25, 2021
ebd31df
Add test for `https://teded.tumblr.com/post/184736320764/how-do-vacci…
pierlon Feb 25, 2021
9125cd6
Use `tagName` instead of `nodeName`
pierlon Feb 25, 2021
ca74ffa
Remove escaping of text
pierlon Feb 25, 2021
da37307
Make use of tagName, use constants where possible, add phpdoc
westonruter Feb 25, 2021
1475347
Check if `tumblr-post` is apart of the list of classes
pierlon Feb 25, 2021
c330597
Use callback to determine if script should be removed
pierlon Feb 26, 2021
7df8bbe
Use button as overflow element
pierlon Feb 26, 2021
3323625
Add bottom:0 to Tumblr overflow button
westonruter Feb 26, 2021
8ae2574
Make xpath predicate more specific and improve condition formatting
westonruter Mar 3, 2021
005ea45
Remove extraneous parentheses
westonruter Mar 3, 2021
5c4b667
Apply style rules for AMP iframe resize button via core sanitizer
pierlon Mar 3, 2021
6d4bef6
Fix indentation
pierlon Mar 3, 2021
f56f10c
Set button type
pierlon Mar 3, 2021
2740d7e
Add style rule to place overflow button in bottom left corner of AMP …
pierlon Mar 4, 2021
7da8a35
Make sure TT1 stylesheet is enqueued before adding inline style
westonruter Mar 4, 2021
f642be8
Opt to combine add_twentytwentyone_overflow_button_fix into amend_twe…
westonruter Mar 4, 2021
0c6666e
Make sure that Tumblr embed has a link to use as a placeholder
westonruter Mar 4, 2021
685bdc9
Make use of MarkupComparison
westonruter Mar 4, 2021
435b6ad
Test removal of Tumblr script when wpautop was not involved
westonruter Mar 4, 2021
bcbdf00
Add test for tumblr in embed block
westonruter Mar 4, 2021
8fa44dd
Remove extraneous sprintf()
westonruter Mar 4, 2021
c482fcb
Combine conditions
westonruter Mar 4, 2021
ce2b8b4
Use xpath query to obtain the placeholder
westonruter Mar 4, 2021
77c7367
Set placeholder attribute when we are sure we will match
westonruter Mar 4, 2021
8839cc1
Remove extraneous sprintf()
westonruter Mar 4, 2021
ad2e9be
Fix tests in WP 4.9
westonruter Mar 4, 2021
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
49 changes: 49 additions & 0 deletions includes/embeds/class-amp-base-embed-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @package AMP
*/

use AmpProject\Tag;

/**
* Class AMP_Base_Embed_Handler
*
Expand Down Expand Up @@ -151,4 +153,51 @@ protected function unwrap_p_element( DOMElement $node ) {
$parent_node->parentNode->replaceChild( $node, $parent_node );
}
}

/**
* Removes the node's nearest `<script>` sibling with a `src` attribute containing the base `src` URL provided.
*
* @since 2.1
*
* @param DOMElement $node The DOMNode to whose sibling is the script to be removed.
* @param callable $match_callback Callback which is passed the script element to determine if it is a match.
*/
protected function maybe_remove_script_sibling( DOMElement $node, callable $match_callback ) {
$next_element_sibling = $node->nextSibling;

while ( $next_element_sibling && ! ( $next_element_sibling instanceof DOMElement ) ) {
$next_element_sibling = $next_element_sibling->nextSibling;
}

if ( ! $next_element_sibling instanceof DOMElement ) {
return;
}

// Handle case where script is immediately following.
if ( Tag::SCRIPT === $next_element_sibling->tagName && $match_callback( $next_element_sibling ) ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
return;
}

// Handle case where script is wrapped in paragraph by wpautop.
if ( 'p' === $next_element_sibling->tagName ) {
/** @var DOMElement[] $children_elements */
$children_elements = array_values(
array_filter(
iterator_to_array( $next_element_sibling->childNodes ),
static function ( DOMNode $child ) {
return $child instanceof DOMElement;
}
)
);

if ( 1 !== count( $children_elements ) ) {
return;
}

if ( Tag::SCRIPT === $children_elements[0]->tagName && $match_callback( $children_elements[0] ) ) {
$next_element_sibling->parentNode->removeChild( $next_element_sibling );
}
}
}
}
111 changes: 88 additions & 23 deletions includes/embeds/class-amp-tumblr-embed-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,121 @@
* @since 0.7
*/

use AmpProject\Attribute;
use AmpProject\Dom\Document;
use AmpProject\Tag;

/**
* Class AMP_Tumblr_Embed_Handler
*
* @internal
*/
class AMP_Tumblr_Embed_Handler extends AMP_Base_Embed_Handler {

/**
* Default width.
*
* Tumblr embeds for web have a fixed width of 540px.
*
* @link https://tumblr.zendesk.com/hc/en-us/articles/226261028-Embed-pro-tips
* @var int
*/
protected $DEFAULT_WIDTH = 540;

/**
* Base URL used for identifying embeds.
*
* @var string
*/
protected $base_embed_url = 'https://embed.tumblr.com/embed/post/';

/**
* Register embed.
*/
public function register_embed() {
add_filter( 'embed_oembed_html', [ $this, 'filter_embed_oembed_html' ], 10, 2 );
// Not implemented.
}

/**
* Unregister embed.
*/
public function unregister_embed() {
remove_filter( 'embed_oembed_html', [ $this, 'filter_embed_oembed_html' ], 10 );
// Not implemented.
}

/**
* Filter oEmbed HTML for Tumblr to prepare it for AMP.
* Sanitizes Tumblr raw embeds to make them AMP compatible.
*
* @param string $cache Cache for oEmbed.
* @param string $url Embed URL.
* @return string Embed.
* @param Document $dom DOM.
*/
public function filter_embed_oembed_html( $cache, $url ) {
$parsed_url = wp_parse_url( $url );
if ( false === strpos( $parsed_url['host'], 'tumblr.com' ) ) {
return $cache;
public function sanitize_raw_embeds( Document $dom ) {
$nodes = $dom->xpath->query(
sprintf(
'//div[ @class and @data-href and contains( concat( " ", normalize-space( @class ), " " ), " tumblr-post " ) and starts-with( @data-href, "%s" ) ]',
$this->base_embed_url
)
);

if ( 0 === $nodes->length ) {
return;
}

// @todo The iframe will not get sized properly.
if ( preg_match( '#data-href="(?P<href>https://embed.tumblr.com/embed/post/\w+/\w+)"#', $cache, $matches ) ) {
$cache = AMP_HTML_Utils::build_tag(
foreach ( $nodes as $node ) {
$iframe_src = $node->getAttribute( 'data-href' );

$attributes = [
'src' => $iframe_src,
'layout' => 'responsive',
'width' => $this->args['width'],
'height' => $this->args['height'],
'resizable' => '',
'sandbox' => 'allow-scripts allow-popups allow-same-origin',
];

$amp_element = AMP_DOM_Utils::create_node(
$dom,
'amp-iframe',
$attributes
);

// Add an overflow element to allow the amp-iframe to be manually resized.
$overflow_element = AMP_DOM_Utils::create_node(
$dom,
'button',
[
'width' => $this->args['width'],
'height' => $this->args['height'],
'layout' => 'responsive',
'sandbox' => 'allow-scripts allow-popups', // The allow-scripts is needed to allow the iframe to render; allow-popups needed to allow clicking.
'src' => $matches['href'],
],
sprintf( '<a placeholder href="%s">Tumblr</a>', $url )
'overflow' => '',
pierlon marked this conversation as resolved.
Show resolved Hide resolved
]
);
}
$overflow_element->textContent = __( 'See more', 'amp' );
$amp_element->appendChild( $overflow_element );
westonruter marked this conversation as resolved.
Show resolved Hide resolved

// Append the original link as a placeholder node.
if ( $node->firstChild instanceof DOMElement && Tag::A === $node->firstChild->tagName ) {
$placeholder_element = $node->firstChild;
$placeholder_element->setAttribute( Attribute::PLACEHOLDER, '' );
$amp_element->appendChild( $placeholder_element );
}
pierlon marked this conversation as resolved.
Show resolved Hide resolved

$this->maybe_remove_script_sibling(
$node,
static function ( DOMElement $script ) {
if ( ! $script->hasAttribute( Attribute::SRC ) ) {
return false;
}

return $cache;
$parsed_url = wp_parse_url( $script->getAttribute( Attribute::SRC ) );

return (
isset( $parsed_url['host'], $parsed_url['path'] )
&&
'assets.tumblr.com' === $parsed_url['host']
&&
'/post.js' === $parsed_url['path']
);
}
);

$node->parentNode->replaceChild( $amp_element, $node );
}
}
}

16 changes: 16 additions & 0 deletions includes/sanitizers/class-amp-core-theme-sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ protected static function get_theme_features_config( $theme_slug ) {
'amend_twentytwentyone_sub_menu_toggles' => [],
'add_twentytwentyone_mobile_modal' => [],
'add_twentytwentyone_sub_menu_fix' => [],
'add_twentytwentyone_amp_iframe_button_fix' => [],
];

// Dark mode button toggle is only supported in the Customizer for now.
Expand Down Expand Up @@ -2225,6 +2226,21 @@ public function add_twentytwentyone_sub_menu_fix() {
$this->dom->body->setAttribute( 'tabindex', '-1' );
}

/**
* In Twenty Twenty-One, the button used to resize `amp-iframe` elements can appear transparent when it is hovered
* over. To resolve this potential issue, the theme's background color is used as the background color for the button
* when it is in the hovered state.
*/
public static function add_twentytwentyone_amp_iframe_button_fix() {
add_action(
'wp_enqueue_scripts',
static function () {
$style = 'button[overflow]:hover { background-color: var(--global--color-background); }';
wp_add_inline_style( 'twenty-twenty-one-style', $style );
}
);
}

/**
* Sanitize the sub-menus in the Twenty Twenty-One theme.
*/
Expand Down
155 changes: 155 additions & 0 deletions tests/php/test-amp-tumblr-embed-handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php
/**
* Class AMP_Tumblr_Embed_Handler_Test
*
* @package AMP
*/

/**
* Tests for Tumblr embeds.
*
* @coversDefaultClass \AMP_Tumblr_Embed_Handler
*/
class AMP_Tumblr_Embed_Handler_Test extends WP_UnitTestCase {

/**
* Set up.
*/
public function setUp() {
parent::setUp();

// Mock the HTTP request.
add_filter( 'pre_http_request', [ $this, 'mock_http_request' ], 10, 3 );
}

/**
* Tear down.
*/
public function tearDown() {
remove_filter( 'pre_http_request', [ $this, 'mock_http_request' ] );
parent::tearDown();
}

/**
* Mock HTTP request.
*
* @param mixed $pre Whether to preempt an HTTP request's return value. Default false.
* @param mixed $r HTTP request arguments.
* @param string $url The request URL.
* @return array Response data.
*/
public function mock_http_request( $pre, $r, $url ) {
if ( in_array( 'external-http', $_SERVER['argv'], true ) ) {
return $pre;
}

if ( false === strpos( $url, 'tumblr.com' ) ) {
return $pre;
}

if ( false !== strpos( $url, 'grant-wood-american-gothic' ) ) {
$body = '{"cache_age":3600,"url":"https:\/\/ifpaintingscouldtext.tumblr.com\/post\/92003045635\/grant-wood-american-gothic-1930","provider_url":"https:\/\/www.tumblr.com","provider_name":"Tumblr","author_name":"If Paintings Could Text","version":"1.0","author_url":"https:\/\/ifpaintingscouldtext.tumblr.com\/","type":"rich","html":"\u003Cdiv class=\u0022tumblr-post\u0022 data-href=\u0022https:\/\/embed.tumblr.com\/embed\/post\/2JT2XTaiTxO08wh21dqQrw\/92003045635\u0022 data-did=\u00227ce4825965cbd8bfd208f6aae43de7a528859aee\u0022 \u003E\u003Ca href=\u0022https:\/\/ifpaintingscouldtext.tumblr.com\/post\/92003045635\/grant-wood-american-gothic-1930\u0022\u003Ehttps:\/\/ifpaintingscouldtext.tumblr.com\/post\/92003045635\/grant-wood-american-gothic-1930\u003C\/a\u003E\u003C\/div\u003E\u003Cscript async src=\u0022https:\/\/assets.tumblr.com\/post.js\u0022\u003E\u003C\/script\u003E","height":null,"width":540}';
} elseif ( false !== strpos( $url, 'how-do-vaccines-work' ) ) {
$body = '{"cache_age":3600,"url":"https:\/\/teded.tumblr.com\/post\/184736320764\/how-do-vaccines-work","provider_url":"https:\/\/www.tumblr.com","provider_name":"Tumblr","author_name":"TED-Ed - Gifs worth sharing","version":"1.0","author_url":"https:\/\/teded.tumblr.com\/","type":"rich","html":"\u003Cdiv class=\u0022tumblr-post\u0022 data-href=\u0022https:\/\/embed.tumblr.com\/embed\/post\/O6_eRR6K-z9QGTzdU5HrhQ\/184736320764\u0022 data-did=\u0022523d09cda8bc0da2f871ffea606ff71c80405725\u0022 \u003E\u003Ca href=\u0022https:\/\/teded.tumblr.com\/post\/184736320764\/how-do-vaccines-work\u0022\u003Ehttps:\/\/teded.tumblr.com\/post\/184736320764\/how-do-vaccines-work\u003C\/a\u003E\u003C\/div\u003E\u003Cscript async src=\u0022https:\/\/assets.tumblr.com\/post.js\u0022\u003E\u003C\/script\u003E","height":null,"width":540}';
} else {
$body = '';
}

return [
'body' => $body,
'response' => [
'code' => 200,
'message' => 'OK',
],
];
}

/**
* Get conversion data.
*
* @return array
*/
public function get_conversion_data() {
return [
'no_embed' => [
'<p>Hello world.</p>',
'<p>Hello world.</p>' . PHP_EOL,
],

'url_simple' => [
'https://ifpaintingscouldtext.tumblr.com/post/92003045635/grant-wood-american-gothic-1930' . PHP_EOL,
westonruter marked this conversation as resolved.
Show resolved Hide resolved
'<amp-iframe src="https://embed.tumblr.com/embed/post/2JT2XTaiTxO08wh21dqQrw/92003045635" layout="responsive" width="540" height="480" resizable="" sandbox="allow-scripts allow-popups allow-same-origin"><button overflow="">See more</button><a href="https://ifpaintingscouldtext.tumblr.com/post/92003045635/grant-wood-american-gothic-1930" placeholder="">https://ifpaintingscouldtext.tumblr.com/post/92003045635/grant-wood-american-gothic-1930</a></amp-iframe>' . PHP_EOL . PHP_EOL,
],

'url_simple_2' => [
'https://teded.tumblr.com/post/184736320764/how-do-vaccines-work' . PHP_EOL,
'<amp-iframe src="https://embed.tumblr.com/embed/post/O6_eRR6K-z9QGTzdU5HrhQ/184736320764" layout="responsive" width="540" height="480" resizable="" sandbox="allow-scripts allow-popups allow-same-origin"><button overflow="">See more</button><a href="https://teded.tumblr.com/post/184736320764/how-do-vaccines-work" placeholder="">https://teded.tumblr.com/post/184736320764/how-do-vaccines-work</a></amp-iframe>' . PHP_EOL . PHP_EOL,
],
];
}

/**
* Test conversion.
*
* @covers ::sanitize_raw_embeds()
* @dataProvider get_conversion_data
*
* @param string $source Source.
* @param string $expected Expected content.
*/
public function test__conversion( $source, $expected ) {
$embed = new AMP_Tumblr_Embed_Handler();

$filtered_content = apply_filters( 'the_content', $source );
$dom = AMP_DOM_Utils::get_dom_from_content( $filtered_content );
$embed->sanitize_raw_embeds( $dom );

$content = AMP_DOM_Utils::get_content_from_dom( $dom );

$this->assertEquals( $expected, $content );
}

/**
* Data for test_get_scripts().
*
* @return array Data.
*/
public function get_scripts_data() {
return [
'not_converted' => [
'<p>Hello World.</p>',
[],
],
'converted' => [
'https://ifpaintingscouldtext.tumblr.com/post/92003045635/grant-wood-american-gothic-1930' . PHP_EOL,
[ 'amp-iframe' => true ],
],
];
}

/**
* Test AMP_Tag_And_Attribute_Sanitizer::get_scripts().
*
* @dataProvider get_scripts_data
*
* @param string $source Source content.
* @param array $expected Expected scripts.
*/
public function test_get_scripts( $source, $expected ) {
$embed = new AMP_Tumblr_Embed_Handler();

$filtered_content = apply_filters( 'the_content', $source );
$dom = AMP_DOM_Utils::get_dom_from_content( $filtered_content );
$embed->sanitize_raw_embeds( $dom );

$whitelist_sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom );
$whitelist_sanitizer->sanitize();

$scripts = array_merge(
$embed->get_scripts(),
$whitelist_sanitizer->get_scripts()
);

$this->assertEquals( $expected, $scripts );
}
}
Loading