diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 1a20c919d2..adba75fcd7 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -195,16 +195,17 @@ add_filter( 'the_content', 'convert_smilies', 20 );
add_filter( 'the_content', 'wpautop' );
add_filter( 'the_content', 'shortcode_unautop' );
add_filter( 'the_content', 'prepend_attachment' );
-add_filter( 'the_content', 'wp_filter_content_tags' );
add_filter( 'the_content', 'wp_replace_insecure_home_url' );
+add_filter( 'the_content', 'do_shortcode', 11 ); // AFTER wpautop().
+add_filter( 'the_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
add_filter( 'the_excerpt', 'wptexturize' );
add_filter( 'the_excerpt', 'convert_smilies' );
add_filter( 'the_excerpt', 'convert_chars' );
add_filter( 'the_excerpt', 'wpautop' );
add_filter( 'the_excerpt', 'shortcode_unautop' );
-add_filter( 'the_excerpt', 'wp_filter_content_tags' );
add_filter( 'the_excerpt', 'wp_replace_insecure_home_url' );
+add_filter( 'the_excerpt', 'wp_filter_content_tags', 12 );
add_filter( 'get_the_excerpt', 'wp_trim_excerpt', 10, 2 );
add_filter( 'the_post_thumbnail_caption', 'wptexturize' );
@@ -230,13 +231,13 @@ add_filter( 'widget_text_content', 'wptexturize' );
add_filter( 'widget_text_content', 'convert_smilies', 20 );
add_filter( 'widget_text_content', 'wpautop' );
add_filter( 'widget_text_content', 'shortcode_unautop' );
-add_filter( 'widget_text_content', 'wp_filter_content_tags' );
add_filter( 'widget_text_content', 'wp_replace_insecure_home_url' );
add_filter( 'widget_text_content', 'do_shortcode', 11 ); // Runs after wpautop(); note that $post global will be null when shortcodes run.
+add_filter( 'widget_text_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
add_filter( 'widget_block_content', 'do_blocks', 9 );
-add_filter( 'widget_block_content', 'wp_filter_content_tags' );
add_filter( 'widget_block_content', 'do_shortcode', 11 );
+add_filter( 'widget_block_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
add_filter( 'block_type_metadata', 'wp_migrate_old_typography_shape' );
@@ -625,9 +626,6 @@ add_action( 'change_locale', 'create_initial_taxonomies' );
add_action( 'template_redirect', 'redirect_canonical' );
add_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );
-// Shortcodes.
-add_filter( 'the_content', 'do_shortcode', 11 ); // AFTER wpautop().
-
// Media.
add_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
add_action( 'customize_controls_enqueue_scripts', 'wp_plupload_default_settings' );
diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
index 76f77c2a90..f94d3170a5 100644
--- a/src/wp-includes/formatting.php
+++ b/src/wp-includes/formatting.php
@@ -3980,7 +3980,7 @@ function wp_trim_excerpt( $text = '', $post = null ) {
* within the excerpt are stripped out. Modifying the tags here
* is wasteful and can lead to bugs in the image counting logic.
*/
- $filter_image_removed = remove_filter( 'the_content', 'wp_filter_content_tags' );
+ $filter_image_removed = remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
/*
* Temporarily unhook do_blocks() since excerpt_remove_blocks( $text )
@@ -4003,7 +4003,7 @@ function wp_trim_excerpt( $text = '', $post = null ) {
* which is generally used for the filter callback in WordPress core.
*/
if ( $filter_image_removed ) {
- add_filter( 'the_content', 'wp_filter_content_tags' );
+ add_filter( 'the_content', 'wp_filter_content_tags', 12 );
}
/* translators: Maximum number of words used in a post excerpt. */
diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php
index 13fa8e9930..988086e009 100644
--- a/src/wp-includes/media.php
+++ b/src/wp-includes/media.php
@@ -5649,16 +5649,20 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) {
}
/*
- * Skip programmatically created images within post content as they need to be handled together with the other
- * images within the post content.
+ * 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.
* Without this clause, they would already be considered within their own context which skews the image count and
* can result in the first post content image being lazy-loaded or an image further down the page being marked as a
* high priority.
*/
- // 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' ) ) {
+ 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' )
+ ) {
/** This filter is documented in wp-includes/media.php */
return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context );
+
}
/*
diff --git a/tests/phpunit/tests/formatting/wpTrimExcerpt.php b/tests/phpunit/tests/formatting/wpTrimExcerpt.php
index faa67d904a..0f9c6e9cb7 100644
--- a/tests/phpunit/tests/formatting/wpTrimExcerpt.php
+++ b/tests/phpunit/tests/formatting/wpTrimExcerpt.php
@@ -129,7 +129,7 @@ class Tests_Formatting_wpTrimExcerpt extends WP_UnitTestCase {
wp_trim_excerpt( '', $post );
- $this->assertSame( 10, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' );
+ $this->assertSame( 12, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' );
}
/**
@@ -141,7 +141,7 @@ class Tests_Formatting_wpTrimExcerpt extends WP_UnitTestCase {
$post = self::factory()->post->create();
// Remove wp_filter_content_tags() from 'the_content' filter generally.
- remove_filter( 'the_content', 'wp_filter_content_tags' );
+ remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
wp_trim_excerpt( '', $post );
diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php
index d70b54261a..87b25e8b3d 100644
--- a/tests/phpunit/tests/media.php
+++ b/tests/phpunit/tests/media.php
@@ -4958,6 +4958,254 @@ EOF;
$this->assertStringContainsString( $expected_image_tag, $output );
}
+ /**
+ * 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 = '
' . "\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(
+ '
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 .= '
' . "\n";
+ $post_content .= '
Some text.
' . "\n"; + $post_content .= '' . "\n"; + $post_content .= '
';
+
+ $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"', '
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 );
+ }
+
private function reset_content_media_count() {
// Get current value without increasing.
$content_media_count = wp_increase_content_media_count( 0 );
@@ -5318,67 +5566,73 @@ EOF;
}
/**
- * @ticket 58681
+ * Tests that the `do_shortcode` context results in a lazy-loaded image by default.
*
- * @dataProvider data_wp_get_loading_optimization_attributes_in_shortcodes
+ * @ticket 58681
+ * @ticket 58853
+ *
+ * @covers ::wp_get_loading_optimization_attributes
*/
- public function test_wp_get_loading_optimization_attributes_in_shortcodes( $setup, $expected, $message ) {
+ public function test_wp_get_loading_optimization_attributes_in_shortcodes() {
$attr = $this->get_width_height_for_high_priority();
- $setup();
- // The first image processed in a shortcode should have fetchpriority set to high.
+ // Shortcodes processed outside of content blobs like 'the_content' always get `loading="lazy"`.
$this->assertSameSetsWithIndex(
- $expected,
+ array(
+ 'decoding' => 'async',
+ 'loading' => 'lazy',
+ ),
wp_get_loading_optimization_attributes( 'img', $attr, 'do_shortcode' ),
- $message
+ 'Lazy-loading not applied to shortcodes outside the loop.'
);
}
- 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;
+ /**
+ * 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;
- // 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;
+ 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, '' );
- // Set WP_Query to be in the loop and the main query.
- $wp_query->in_the_loop = true;
- $this->set_main_query( $wp_query );
+ // 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.'
+ );
+ }
- // 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.',
- ),
+ /**
+ * 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',
+ )
);
}