<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56693] trunk: Media: Ensure images within shortcodes are correctly considered for loading optimization attributes.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56693">56693</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56693","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>flixos90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-09-26 00:11:06 +0000 (Tue, 26 Sep 2023)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Media: Ensure images within shortcodes are correctly considered for loading optimization attributes.
Prior to this change, images added in shortcodes would be considered separately from all other images within post content, which led to incorrect application of the loading optimization attributes `loading="lazy"` and `fetchpriority="high"`.
This changeset changes the filter priority of `wp_filter_content_tags()` from the default `10` to `12` on the various content filters it is hooked in, in order to run that function after parsing shortcodes. While this may technically be considered a backward compatibility break, substantial research and lack of any relevant usage led to the assessment that the change is acceptable given its benefits.
An additional related fix included is that now the duplicate processing of images is prevented not only for post content blobs (`the_content` filter), but also for widget content blobs (`widget_text_content` and `widget_block_content` filters).
Props joemcgill, mukesh27, costdev, spacedmonkey, flixos90.
Fixes <a href="https://core.trac.wordpress.org/ticket/58853">#58853</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesformattingphp">trunk/src/wp-includes/formatting.php</a></li>
<li><a href="#trunksrcwpincludesmediaphp">trunk/src/wp-includes/media.php</a></li>
<li><a href="#trunktestsphpunittestsformattingwpTrimExcerptphp">trunk/tests/phpunit/tests/formatting/wpTrimExcerpt.php</a></li>
<li><a href="#trunktestsphpunittestsmediaphp">trunk/tests/phpunit/tests/media.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2023-09-26 00:07:56 UTC (rev 56692)
+++ trunk/src/wp-includes/default-filters.php 2023-09-26 00:11:06 UTC (rev 56693)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -195,8 +195,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'wpautop' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'shortcode_unautop' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'prepend_attachment' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_filter( 'the_content', 'wp_filter_content_tags' );
</del><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_content', 'wp_replace_insecure_home_url' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_filter( 'the_content', 'do_shortcode', 11 ); // AFTER wpautop().
+add_filter( 'the_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_excerpt', 'wptexturize' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_excerpt', 'convert_smilies' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -203,8 +204,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_excerpt', 'convert_chars' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_excerpt', 'wpautop' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_excerpt', 'shortcode_unautop' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_filter( 'the_excerpt', 'wp_filter_content_tags' );
</del><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_excerpt', 'wp_replace_insecure_home_url' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_filter( 'the_excerpt', 'wp_filter_content_tags', 12 );
</ins><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'get_the_excerpt', 'wp_trim_excerpt', 10, 2 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'the_post_thumbnail_caption', 'wptexturize' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -230,13 +231,13 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_text_content', 'convert_smilies', 20 );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_text_content', 'wpautop' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_text_content', 'shortcode_unautop' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_filter( 'widget_text_content', 'wp_filter_content_tags' );
</del><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_text_content', 'wp_replace_insecure_home_url' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_text_content', 'do_shortcode', 11 ); // Runs after wpautop(); note that $post global will be null when shortcodes run.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_filter( 'widget_text_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_block_content', 'do_blocks', 9 );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_filter( 'widget_block_content', 'wp_filter_content_tags' );
</del><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'widget_block_content', 'do_shortcode', 11 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_filter( 'widget_block_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'block_type_metadata', 'wp_migrate_old_typography_shape' );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -625,9 +626,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'template_redirect', 'redirect_canonical' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-// Shortcodes.
-add_filter( 'the_content', 'do_shortcode', 11 ); // AFTER wpautop().
-
</del><span class="cx" style="display: block; padding: 0 10px"> // Media.
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'customize_controls_enqueue_scripts', 'wp_plupload_default_settings' );
</span></span></pre></div>
<a id="trunksrcwpincludesformattingphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/formatting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/formatting.php 2023-09-26 00:07:56 UTC (rev 56692)
+++ trunk/src/wp-includes/formatting.php 2023-09-26 00:11:06 UTC (rev 56693)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3980,7 +3980,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * within the excerpt are stripped out. Modifying the tags here
</span><span class="cx" style="display: block; padding: 0 10px"> * is wasteful and can lead to bugs in the image counting logic.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $filter_image_removed = remove_filter( 'the_content', 'wp_filter_content_tags' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $filter_image_removed = remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px"> * Temporarily unhook do_blocks() since excerpt_remove_blocks( $text )
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4003,7 +4003,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * which is generally used for the filter callback in WordPress core.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $filter_image_removed ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- add_filter( 'the_content', 'wp_filter_content_tags' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ add_filter( 'the_content', 'wp_filter_content_tags', 12 );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /* translators: Maximum number of words used in a post excerpt. */
</span></span></pre></div>
<a id="trunksrcwpincludesmediaphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/media.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/media.php 2023-09-26 00:07:56 UTC (rev 56692)
+++ trunk/src/wp-includes/media.php 2023-09-26 00:11:06 UTC (rev 56693)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5649,16 +5649,20 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /*
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Skip programmatically created images within post content as they need to be handled together with the other
- * images within the post content.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Skip programmatically created images within content blobs as they need to be handled together with the other
+ * images within the post content or widget content.
</ins><span class="cx" style="display: block; padding: 0 10px"> * Without this clause, they would already be considered within their own context which skews the image count and
</span><span class="cx" style="display: block; padding: 0 10px"> * can result in the first post content image being lazy-loaded or an image further down the page being marked as a
</span><span class="cx" style="display: block; padding: 0 10px"> * high priority.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // TODO: Handle shortcode images together with the content (see https://core.trac.wordpress.org/ticket/58853).
- if ( 'the_content' !== $context && 'do_shortcode' !== $context && doing_filter( 'the_content' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if (
+ 'the_content' !== $context && doing_filter( 'the_content' ) ||
+ 'widget_text_content' !== $context && doing_filter( 'widget_text_content' ) ||
+ 'widget_block_content' !== $context && doing_filter( 'widget_block_content' )
+ ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> /** This filter is documented in wp-includes/media.php */
</span><span class="cx" style="display: block; padding: 0 10px"> return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /*
</span></span></pre></div>
<a id="trunktestsphpunittestsformattingwpTrimExcerptphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/formatting/wpTrimExcerpt.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/formatting/wpTrimExcerpt.php 2023-09-26 00:07:56 UTC (rev 56692)
+++ trunk/tests/phpunit/tests/formatting/wpTrimExcerpt.php 2023-09-26 00:11:06 UTC (rev 56693)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -129,7 +129,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp_trim_excerpt( '', $post );
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->assertSame( 10, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->assertSame( 12, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -141,7 +141,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $post = self::factory()->post->create();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> // Remove wp_filter_content_tags() from 'the_content' filter generally.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- remove_filter( 'the_content', 'wp_filter_content_tags' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> wp_trim_excerpt( '', $post );
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunktestsphpunittestsmediaphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/media.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/media.php 2023-09-26 00:07:56 UTC (rev 56692)
+++ trunk/tests/phpunit/tests/media.php 2023-09-26 00:11:06 UTC (rev 56693)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4958,6 +4958,254 @@
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertStringContainsString( $expected_image_tag, $output );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+ * Tests that wp_filter_content_tags() and more specifically wp_get_loading_optimization_attributes() correctly
+ * handle shortcodes images together with the content that it is part of.
+ *
+ * Images within shortcodes as part of the content should be ignored by wp_get_loading_optimization_attributes() to
+ * avoid double processing. They should instead only be processed together with any other images as part of the
+ * content, to correctly count the original sequencing of those images.
+ *
+ * @ticket 58853
+ *
+ * @covers ::wp_filter_content_tags
+ * @covers ::wp_get_loading_optimization_attributes
+ */
+ public function test_wp_filter_content_tags_handles_shortcode_image_together_with_the_content() {
+ global $wp_query, $wp_the_query;
+
+ // Add shortcode that prints a large image, and a block type that wraps it.
+ add_shortcode(
+ 'full_image',
+ static function ( $atts ) {
+ $atts = shortcode_atts(
+ array(
+ 'id' => 0,
+ ),
+ $atts,
+ 'full_image'
+ );
+ return wp_get_attachment_image( (int) $atts['id'], 'full' );
+ }
+ );
+
+ /*
+ * Even though `do_shortcode()` runs before `wp_filter_content_tags()`, the image from the shortcode should not
+ * receive any loading optimization attributes because it needs to be considered together with the rest of the
+ * post content, within `wp_filter_content_tags()`.
+ * Since the hard-coded image appears before the shortcode image, it should receive `fetchpriority="high"`,
+ * despite the shortcode image being parsed before it.
+ */
+ $post_content = '<img src="example.jpg" width="800" height="600">' . "\n";
+ $post_content .= '[full_image id="' . self::$large_id . '"]';
+ $post_content = wpautop( $post_content );
+
+ /*
+ * Prepare the expected output:
+ * 1. On the first image (hard-coded in the content), expect `fetchpriority="high"`.
+ * 2. Replace the shortcode with its expected output, i.e. the full image. Expect neither
+ * `fetchpriority="high"` nor `loading="lazy"`.
+ */
+ $expected_content = $post_content;
+ $expected_content = str_replace(
+ '<img src="example.jpg"',
+ '<img fetchpriority="high" decoding="async" src="example.jpg"',
+ $expected_content
+ );
+ $expected_content = str_replace(
+ '[full_image id="' . self::$large_id . '"]',
+ str_replace(
+ '<img ',
+ '<img decoding="async" ',
+ wp_get_attachment_image(
+ self::$large_id,
+ 'full',
+ false,
+ array(
+ 'decoding' => false,
+ 'fetchpriority' => false,
+ 'loading' => false,
+ )
+ )
+ ),
+ $expected_content
+ );
+
+ // Create post with the content.
+ $post_id = self::factory()->post->create(
+ array(
+ 'post_content' => $post_content,
+ 'post_excerpt' => '',
+ )
+ );
+
+ // We have to run a main query loop so that the first 'the_content' context images are not lazy-loaded.
+ $wp_query = new WP_Query( array( 'post__in' => array( $post_id ) ) );
+ $wp_the_query = $wp_query;
+
+ $content = '';
+ while ( have_posts() ) {
+ the_post();
+ $content = get_echo( 'the_content' );
+ }
+
+ // Cleanup.
+ remove_shortcode( 'full_image' );
+
+ $this->assertSame( $expected_content, $content );
+ }
+
+ /**
+ * Tests that wp_filter_content_tags() and more specifically wp_get_loading_optimization_attributes() correctly
+ * handle shortcodes images within the content, including within a block.
+ *
+ * Images within shortcodes as part of the content should be ignored by wp_get_loading_optimization_attributes() to
+ * avoid double processing. They should instead only be processed together with any other images as part of the
+ * content, to correctly count the original sequencing of those images.
+ *
+ * @ticket 58853
+ *
+ * @covers ::wp_filter_content_tags
+ * @covers ::wp_get_loading_optimization_attributes
+ */
+ public function test_wp_filter_content_tags_handles_shortcode_images_also_in_blocks_within_the_content() {
+ global $wp_query, $wp_the_query;
+
+ // Disable addition of `decoding="async"` as it is irrelevant for this test.
+ add_filter(
+ 'wp_get_loading_optimization_attributes',
+ static function ( $loading_attrs ) {
+ if ( isset( $loading_attrs['decoding'] ) ) {
+ unset( $loading_attrs['decoding'] );
+ }
+ return $loading_attrs;
+ }
+ );
+
+ // Add shortcode that prints a large image, and a block type that wraps it.
+ add_shortcode(
+ 'full_image',
+ static function ( $atts ) {
+ $atts = shortcode_atts(
+ array(
+ 'id' => 0,
+ ),
+ $atts,
+ 'full_image'
+ );
+ return wp_get_attachment_image( (int) $atts['id'], 'full' );
+ }
+ );
+ register_block_type(
+ 'core/full-image-shortcode',
+ array(
+ 'render_callback' => static function ( $atts ) {
+ if ( empty( $atts['id'] ) ) {
+ return '';
+ }
+ return do_shortcode( '[full_image id="' . $atts['id'] . '"]' );
+ },
+ )
+ );
+
+ /*
+ * Include the following images:
+ * 1. Using gallery shortcode. Expected `fetchpriority="high"`.
+ * 2. Regular hard-coded image.
+ * 3. Using custom shortcode within block.
+ * 4. Regular hard-coded image. Expected `loading="lazy"`.
+ *
+ * The first image is expected to be prioritized because it is the first (large enough) content image.
+ * The first three images are expected to not have lazy-loading because that is the default threshold for
+ * omitting the attribute.
+ * The fourth image is expected to be lazy-loaded as it is past the default threshold.
+ *
+ * The results will only be correct if all images are considered together. For example:
+ * * If the image within the shortcode would only be parsed after the rest of the content, it would miss the
+ * `fetchpriority="high"` attribute and instead incorrectly receive `loading="lazy"`. The second image would as
+ * a result incorrectly receive `fetchpriority="high"`.
+ * * If the image within the block would be parsed before the rest of the content, it would incorrectly receive
+ * the `fetchpriority="high"` attribute. Then the first image would no longer receive the attribute.
+ *
+ * To ensure that this works:
+ * * `wp_filter_content_tags()` must run after `do_blocks()` and `do_shortcode()`.
+ * * `wp_get_loading_optimization_attributes()` must bail early if any images from the content blob are being
+ * considered under a different context name than 'the_content'.
+ */
+ $post_content = '[gallery ids="' . self::$large_id . '" size="large"]' . "\n";
+ $post_content .= '<img src="example.jpg" width="800" height="600">' . "\n";
+ $post_content .= '<p>Some text.</p>' . "\n";
+ $post_content .= '<!-- wp:core/full-image-shortcode {"id":' . self::$large_id . '} --><!-- /wp:core/full-image-shortcode -->' . "\n";
+ $post_content .= '<img src="example2.jpg" width="800" height="600">';
+
+ $post_id = self::factory()->post->create(
+ array(
+ 'post_content' => $post_content,
+ 'post_excerpt' => '',
+ )
+ );
+
+ /*
+ * Prepare the expected output:
+ * 1. Replace the shortcode with its expected output (ID increased by 1 because of static variable within
+ * the gallery_shortcode() function). Expect `fetchpriority="high"`, but not `loading="lazy"`.
+ * 2. Do not modify the second image as it is hard-coded in the content and expected to be unchanged.
+ * 3. Replace the block with its expected output, i.e. the full image from the shortcode within. Expect neither
+ * `fetchpriority="high"` nor `loading="lazy"`.
+ * 4. On the fourth image (hard-coded in the content), expect `loading="lazy"`.
+ */
+ $expected_content = $post_content;
+ $expected_content = str_replace(
+ '[gallery ids="' . self::$large_id . '" size="large"]',
+ str_replace(
+ array( ' loading="lazy"', '<img ' ),
+ array( '', '<img fetchpriority="high" ' ),
+ preg_replace_callback(
+ '/gallery-(\d+)/',
+ static function ( $match ) {
+ return 'gallery-' . ( (int) $match[1] + 1 );
+ },
+ do_shortcode( '[gallery ids="' . self::$large_id . '" size="large" id="' . $post_id . '"]' )
+ )
+ ),
+ $expected_content
+ );
+ $expected_content = str_replace(
+ '<!-- wp:core/full-image-shortcode {"id":' . self::$large_id . '} --><!-- /wp:core/full-image-shortcode -->',
+ wp_get_attachment_image(
+ self::$large_id,
+ 'full',
+ false,
+ array(
+ 'fetchpriority' => false,
+ 'loading' => false,
+ )
+ ),
+ $expected_content
+ );
+ $expected_content = str_replace(
+ '<img src="example2.jpg"',
+ '<img loading="lazy" src="example2.jpg"',
+ $expected_content
+ );
+
+ // We have to run a main query loop so that the first 'the_content' context images are not lazy-loaded.
+ $wp_query = new WP_Query( array( 'post__in' => array( $post_id ) ) );
+ $wp_the_query = $wp_query;
+
+ $content = '';
+ while ( have_posts() ) {
+ the_post();
+ $content = get_echo( 'the_content' );
+ }
+
+ // Cleanup.
+ remove_shortcode( 'full_image' );
+ unregister_block_type( 'core/full-image-shortcode' );
+
+ $this->assertSame( $expected_content, $content );
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> private function reset_content_media_count() {
</span><span class="cx" style="display: block; padding: 0 10px"> // Get current value without increasing.
</span><span class="cx" style="display: block; padding: 0 10px"> $content_media_count = wp_increase_content_media_count( 0 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5318,67 +5566,73 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Tests that the `do_shortcode` context results in a lazy-loaded image by default.
+ *
</ins><span class="cx" style="display: block; padding: 0 10px"> * @ticket 58681
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @ticket 58853
</ins><span class="cx" style="display: block; padding: 0 10px"> *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @dataProvider data_wp_get_loading_optimization_attributes_in_shortcodes
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @covers ::wp_get_loading_optimization_attributes
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- public function test_wp_get_loading_optimization_attributes_in_shortcodes( $setup, $expected, $message ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_wp_get_loading_optimization_attributes_in_shortcodes() {
</ins><span class="cx" style="display: block; padding: 0 10px"> $attr = $this->get_width_height_for_high_priority();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $setup();
</del><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // The first image processed in a shortcode should have fetchpriority set to high.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Shortcodes processed outside of content blobs like 'the_content' always get `loading="lazy"`.
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->assertSameSetsWithIndex(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $expected,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ array(
+ 'decoding' => 'async',
+ 'loading' => 'lazy',
+ ),
</ins><span class="cx" style="display: block; padding: 0 10px"> wp_get_loading_optimization_attributes( 'img', $attr, 'do_shortcode' ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $message
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'Lazy-loading not applied to shortcodes outside the loop.'
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- public function data_wp_get_loading_optimization_attributes_in_shortcodes() {
- return array(
- 'main_shortcode_image_should_have_fetchpriority_high' => array(
- 'setup' => function () {
- global $wp_query;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+ * Tests that the `do_shortcode` context does not result in loading optimization changes when used within a content
+ * blob.
+ *
+ * @ticket 58853
+ *
+ * @covers ::wp_get_loading_optimization_attributes
+ *
+ * @dataProvider data_get_filters_with_do_shortcode_callback
+ *
+ * @param string $filter_name The name of the filter to hook.
+ */
+ public function test_wp_get_loading_optimization_attributes_in_shortcodes_within_content_blob( $filter_name ) {
+ $result = null;
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // Set WP_Query to be in the loop and the main query.
- $wp_query->in_the_loop = true;
- $this->set_main_query( $wp_query );
- },
- 'expected' => array(
- 'decoding' => 'async',
- 'fetchpriority' => 'high',
- ),
- 'message' => 'Fetch priority not applied to during shortcode rendering.',
- ),
- 'main_shortcode_image_after_threshold_is_loading_lazy' => array(
- 'setup' => function () {
- global $wp_query;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ remove_all_filters( $filter_name );
+ add_filter(
+ $filter_name,
+ function ( $content ) use ( &$result ) {
+ $attr = $this->get_width_height_for_high_priority();
+ $result = wp_get_loading_optimization_attributes( 'img', $attr, 'do_shortcode' );
+ return $content;
+ }
+ );
+ apply_filters( $filter_name, '' );
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // Set WP_Query to be in the loop and the main query.
- $wp_query->in_the_loop = true;
- $this->set_main_query( $wp_query );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Shortcodes processed within content blobs like 'the_content' should never get any loading optimization attributes.
+ $this->assertSame(
+ array(),
+ $result,
+ 'Loading optimization unexpectedly applied to shortcodes within content blob.'
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // Set internal flags so lazy should be applied.
- wp_high_priority_element_flag( false );
- wp_increase_content_media_count( 3 );
- },
- 'expected' => array(
- 'decoding' => 'async',
- 'loading' => 'lazy',
- ),
- 'message' => 'Lazy-loading or decoding not applied to during shortcode rendering.',
- ),
- 'shortcode_image_outside_of_the_loop_are_loaded_lazy' => array(
- 'setup' => function () {
- // Avoid setting up the WP_Query object for the loop.
- return;
- },
- 'expected' => array(
- 'decoding' => 'async',
- 'loading' => 'lazy',
- ),
- 'message' => 'Lazy-loading or decoding not applied to shortcodes outside the loop.',
- ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+ * Gets filters for content blobs that by default have a `do_shortcode()` callback.
+ *
+ * @return array[]
+ */
+ public function data_get_filters_with_do_shortcode_callback() {
+ return self::text_array_to_dataprovider(
+ array(
+ 'the_content',
+ 'widget_text_content',
+ 'widget_block_content',
+ )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre>
</div>
</div>
</body>
</html>