From 02a2f9c9f515eee1a10c4fa04d95b89c1a750caf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 11 Nov 2021 02:47:10 +0000 Subject: [PATCH] Embeds: Conditionally enqueue `wp-embed` only if needed and send `ready` message in case script loads after post embed windows. * Prevent loading `wp-embed` script unconditionally on every page in favor of conditionally enqueueing when a post embed is detected. The `wp-embed` script is also explicitly marked as being in the footer group. Sites which currently disable post embed scripts from being enqueued via `remove_action( 'wp_head', 'wp_oembed_add_host_js' )` will continue to do so. * Send a `ready` message from the host page to each post embed window in case the `iframe` loads before the `wp-embed` script does. When the `ready` message is received by the post embed window, it sends the same `height` message as it sends when it loads. * Eliminate use of `grunt-include` to inject emoji script and the post embed script. Instead obtain the script contents via `file_get_contents()` (as is done elsewhere in core) and utilize `wp_print_inline_script_tag()`/`wp_get_inline_script_tag()` to construct out the script. This simplifies the logic and allows the running of src without `SCRIPT_DEBUG` enabled. * For the embed code that users are provided to copy for embedding outside of WP, add the `secret` on the `blockquote` and `iframe`. This ensures the `blockquote` will be hidden when the `iframe` loads. The embed code in question is accessed here via `get_post_embed_html()`. Props westonruter, swissspidy, pento, flixos90, ocean90. Fixes #44632, #44306. git-svn-id: https://develop.svn.wordpress.org/trunk@52132 602fd350-edb4-49c9-b593-d223f7449a82 --- Gruntfile.js | 12 ---- package-lock.json | 6 -- package.json | 1 - src/js/_enqueues/lib/embed-template.js | 47 ++++++++++++--- src/js/_enqueues/wp/embed.js | 19 +++++- src/wp-includes/embed.php | 59 +++++++++++-------- src/wp-includes/formatting.php | 32 ++-------- src/wp-includes/script-loader.php | 2 +- .../phpunit/tests/oembed/getResponseData.php | 26 ++++++-- tests/phpunit/tests/oembed/template.php | 42 ++++++++++++- 10 files changed, 160 insertions(+), 86 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 25a29374ae..7eef7cda9a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1020,16 +1020,6 @@ module.exports = function(grunt) { dest: SOURCE_DIR } }, - includes: { - emoji: { - src: BUILD_DIR + 'wp-includes/formatting.php', - dest: '.' - }, - embed: { - src: BUILD_DIR + 'wp-includes/embed.php', - dest: '.' - } - }, replace: { 'emoji-regex': { options: { @@ -1593,8 +1583,6 @@ module.exports = function(grunt) { 'build:files', 'build:js', 'build:css', - 'includes:emoji', - 'includes:embed', 'replace:emoji-banner-text', 'replace:source-maps', 'verify:build' diff --git a/package-lock.json b/package-lock.json index aa3c52395c..ec59189b03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13422,12 +13422,6 @@ "integrity": "sha1-P376M2lvoFdwsoCU9EUIyvxdLto=", "dev": true }, - "grunt-includes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-includes/-/grunt-includes-1.1.0.tgz", - "integrity": "sha512-aZQfL+fiAonPI173QUjGyuCkaUTJci7+a5SkmSAbezUikwLban7Jp6W+vbA/Mnacmy+EPipnuK5kAF6O0SvrDw==", - "dev": true - }, "grunt-jsdoc": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/grunt-jsdoc/-/grunt-jsdoc-2.4.1.tgz", diff --git a/package.json b/package.json index 7dd5650323..00945bd7e3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "grunt-contrib-uglify": "~5.0.1", "grunt-contrib-watch": "~1.1.0", "grunt-file-append": "0.0.7", - "grunt-includes": "~1.1.0", "grunt-jsdoc": "2.4.1", "grunt-jsvalidate": "~0.2.2", "grunt-legacy-util": "^2.0.1", diff --git a/src/js/_enqueues/lib/embed-template.js b/src/js/_enqueues/lib/embed-template.js index 86fb7d2c84..ae58e4e967 100644 --- a/src/js/_enqueues/lib/embed-template.js +++ b/src/js/_enqueues/lib/embed-template.js @@ -18,6 +18,13 @@ }, '*' ); } + /** + * Send the height message to the parent window. + */ + function sendHeightMessage() { + sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); + } + function onLoad() { if ( loaded ) { return; @@ -138,13 +145,11 @@ } // Send this document's height to the parent (embedding) site. - sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); + sendHeightMessage(); // Send the document's height again after the featured image has been loaded. if ( featured_image ) { - featured_image.addEventListener( 'load', function() { - sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); - } ); + featured_image.addEventListener( 'load', sendHeightMessage ); } /** @@ -184,9 +189,36 @@ clearTimeout( resizing ); - resizing = setTimeout( function () { - sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); - }, 100 ); + resizing = setTimeout( sendHeightMessage, 100 ); + } + + /** + * Message handler. + * + * @param {MessageEvent} event + */ + function onMessage( event ) { + var data = event.data; + + if ( ! data ) { + return; + } + + if ( event.source !== window.parent ) { + return; + } + + if ( ! ( data.secret || data.message ) ) { + return; + } + + if ( data.secret !== secret ) { + return; + } + + if ( 'ready' === data.message ) { + sendHeightMessage(); + } } /** @@ -212,5 +244,6 @@ document.addEventListener( 'DOMContentLoaded', onLoad, false ); window.addEventListener( 'load', onLoad, false ); window.addEventListener( 'resize', onResize, false ); + window.addEventListener( 'message', onMessage, false ); } })( window, document ); diff --git a/src/js/_enqueues/wp/embed.js b/src/js/_enqueues/wp/embed.js index 887c7483fb..fa2934f379 100644 --- a/src/js/_enqueues/wp/embed.js +++ b/src/js/_enqueues/wp/embed.js @@ -27,6 +27,11 @@ return; } + /** + * Receive embed message. + * + * @param {MessageEvent} e + */ window.wp.receiveEmbedMessage = function( e ) { var data = e.data; @@ -102,9 +107,11 @@ iframeClone, i, source, secret; for ( i = 0; i < iframes.length; i++ ) { + /** @var {IframeElement} */ source = iframes[ i ]; - if ( ! source.getAttribute( 'data-secret' ) ) { + secret = source.getAttribute( 'data-secret' ); + if ( ! secret ) { /* Add secret to iframe */ secret = Math.random().toString( 36 ).substr( 2, 10 ); source.src += '#?secret=' + secret; @@ -117,6 +124,16 @@ iframeClone.removeAttribute( 'security' ); source.parentNode.replaceChild( iframeClone, source ); } + + /* + * Let post embed window know that the parent is ready for receiving the height message, in case the iframe + * loaded before wp-embed.js was loaded. When the ready message is received by the post embed window, the + * window will then (re-)send the height message right away. + */ + source.contentWindow.postMessage( { + message: 'ready', + secret: secret + }, '*' ); } } diff --git a/src/wp-includes/embed.php b/src/wp-includes/embed.php index c30c7bcd2e..e0f4bc937c 100644 --- a/src/wp-includes/embed.php +++ b/src/wp-includes/embed.php @@ -359,7 +359,25 @@ function wp_oembed_add_discovery_links() { * @since 4.4.0 */ function wp_oembed_add_host_js() { - wp_enqueue_script( 'wp-embed' ); + add_filter( 'embed_oembed_html', 'wp_maybe_enqueue_oembed_host_js' ); +} + +/** + * Enqueue the wp-embed script if the provided oEmbed HTML contains a post embed. + * + * In order to only enqueue the wp-embed script on pages that actually contain post embeds, this function checks if the + * provided HTML contains post embed markup and if so enqueues the script so that it will get printed in the footer. + * + * @since 5.9.0 + * + * @param string $html Embed markup. + * @return string Embed markup (without modifications). + */ +function wp_maybe_enqueue_oembed_host_js( $html ) { + if ( preg_match( '/]*wp-embedded-content/', $html ) ) { + wp_enqueue_script( 'wp-embed' ); + } + return $html; } /** @@ -450,32 +468,22 @@ function get_post_embed_html( $width, $height, $post = null ) { $embed_url = get_post_embed_url( $post ); - $output = '
' . get_the_title( $post ) . "
\n"; + $secret = wp_generate_password( 10, false ); + $embed_url .= "#?secret={$secret}"; - $output .= ""; + $output = wp_get_inline_script_tag( + file_get_contents( sprintf( ABSPATH . WPINC . '/js/wp-embed' . wp_scripts_get_suffix() . '.js' ) ) + ); $output .= sprintf( - '', + '
%3$s
', + esc_attr( $secret ), + esc_url( get_permalink( $post ) ), + get_the_title( $post ) + ); + + $output .= sprintf( + '', esc_url( $embed_url ), absint( $width ), absint( $height ), @@ -486,7 +494,8 @@ JS; get_the_title( $post ), get_bloginfo( 'name' ) ) - ) + ), + esc_attr( $secret ) ); /** diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 2e71cda202..38a4a113a9 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5756,8 +5756,7 @@ function _print_emoji_detection_script() { 'svgExt' => apply_filters( 'emoji_svg_ext', '.svg' ), ); - $version = 'ver=' . get_bloginfo( 'version' ); - $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/javascript"'; + $version = 'ver=' . get_bloginfo( 'version' ); if ( SCRIPT_DEBUG ) { $settings['source'] = array( @@ -5766,36 +5765,17 @@ function _print_emoji_detection_script() { /** This filter is documented in wp-includes/class.wp-scripts.php */ 'twemoji' => apply_filters( 'script_loader_src', includes_url( "js/twemoji.js?$version" ), 'twemoji' ), ); - - ?> - > - window._wpemojiSettings = ; - - - apply_filters( 'script_loader_src', includes_url( "js/wp-emoji-release.min.js?$version" ), 'concatemoji' ), ); - - /* - * If you're looking at a src version of this file, you'll see an "include" - * statement below. This is used by the `npm run build` process to directly - * include a minified version of wp-emoji-loader.js, instead of using the - * readfile() method from above. - * - * If you're looking at a build version of this file, you'll see a string of - * minified JavaScript. If you need to debug it, please turn on SCRIPT_DEBUG - * and edit wp-emoji-loader.js directly. - */ - ?> - > - window._wpemojiSettings = ; - include "js/wp-emoji-loader.min.js" - - add( 'wp-embed', "/wp-includes/js/wp-embed$suffix.js" ); + $scripts->add( 'wp-embed', "/wp-includes/js/wp-embed$suffix.js", array(), false, 1 ); // To enqueue media-views or media-editor, call wp_enqueue_media(). // Both rely on numerous settings, styles, and templates to operate correctly. diff --git a/tests/phpunit/tests/oembed/getResponseData.php b/tests/phpunit/tests/oembed/getResponseData.php index 5cec227d99..d782014b25 100644 --- a/tests/phpunit/tests/oembed/getResponseData.php +++ b/tests/phpunit/tests/oembed/getResponseData.php @@ -12,6 +12,24 @@ class Tests_oEmbed_Response_Data extends WP_UnitTestCase { self::touch( ABSPATH . WPINC . '/js/wp-embed.js' ); } + private function normalize_secret_attribute( $data ) { + if ( is_array( $data ) ) { + $html = $data['html']; + } else { + $html = $data; + } + + $html = preg_replace( '/secret=("?)\w+\1/', 'secret=__SECRET__', $html ); + + if ( is_array( $data ) ) { + $data['html'] = $html; + } else { + $data = $html; + } + + return $data; + } + public function test_get_oembed_response_data_non_existent_post() { $this->assertFalse( get_oembed_response_data( 0, 100 ) ); } @@ -36,9 +54,9 @@ class Tests_oEmbed_Response_Data extends WP_UnitTestCase { 'type' => 'rich', 'width' => 400, 'height' => 225, - 'html' => get_post_embed_html( 400, 225, $post ), + 'html' => $this->normalize_secret_attribute( get_post_embed_html( 400, 225, $post ) ), ), - $data + $this->normalize_secret_attribute( $data ) ); } @@ -72,9 +90,9 @@ class Tests_oEmbed_Response_Data extends WP_UnitTestCase { 'type' => 'rich', 'width' => 400, 'height' => 225, - 'html' => get_post_embed_html( 400, 225, $post ), + 'html' => $this->normalize_secret_attribute( get_post_embed_html( 400, 225, $post ) ), ), - $data + $this->normalize_secret_attribute( $data ) ); } diff --git a/tests/phpunit/tests/oembed/template.php b/tests/phpunit/tests/oembed/template.php index b330ae70cf..8146846791 100644 --- a/tests/phpunit/tests/oembed/template.php +++ b/tests/phpunit/tests/oembed/template.php @@ -4,6 +4,21 @@ * @group oembed */ class Tests_Embed_Template extends WP_UnitTestCase { + + public function set_up() { + parent::set_up(); + + global $wp_scripts; + $wp_scripts = null; + } + + public function tear_down() { + parent::tear_down(); + + global $wp_scripts; + $wp_scripts = null; + } + public function test_oembed_output_post() { $user = self::factory()->user->create_and_get( array( @@ -281,15 +296,36 @@ class Tests_Embed_Template extends WP_UnitTestCase { ) ); - $expected = ''; + $expected = ''; + $actual = get_post_embed_html( 200, 200, $post_id ); + $actual = preg_replace( '/secret=("?)\w+\1/', 'secret=__SECRET__', $actual ); - $this->assertStringEndsWith( $expected, get_post_embed_html( 200, 200, $post_id ) ); + $this->assertStringEndsWith( $expected, $actual ); } + /** @covers ::wp_oembed_add_host_js() */ public function test_add_host_js() { + remove_all_filters( 'embed_oembed_html' ); + wp_oembed_add_host_js(); - $this->assertTrue( wp_script_is( 'wp-embed' ) ); + $this->assertEquals( 10, has_filter( 'embed_oembed_html', 'wp_maybe_enqueue_oembed_host_js' ) ); + } + + /** @covers ::wp_maybe_enqueue_oembed_host_js() */ + function test_wp_maybe_enqueue_oembed_host_js() { + $scripts = wp_scripts(); + + $this->assertFalse( $scripts->query( 'wp-embed', 'enqueued' ) ); + + $post_embed = '
Embeds Changes in WordPress 4.5
'; + $non_post_embed = ''; + + wp_maybe_enqueue_oembed_host_js( $non_post_embed ); + $this->assertFalse( $scripts->query( 'wp-embed', 'enqueued' ) ); + + wp_maybe_enqueue_oembed_host_js( $post_embed ); + $this->assertTrue( $scripts->query( 'wp-embed', 'enqueued' ) ); } /**