mirror of
https://github.com/gosticks/wordpress-develop.git
synced 2026-06-28 14:20:15 +00:00
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 #58853. git-svn-id: https://develop.svn.wordpress.org/trunk@56693 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
@@ -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' );
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 );
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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 = '<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 );
|
||||
}
|
||||
|
||||
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',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user