Make WordPress Core

Changeset 56693


Ignore:
Timestamp:
09/26/2023 12:11:06 AM (3 years ago)
Author:
flixos90
Message:

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.

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/default-filters.php

    r56682 r56693  
    196196add_filter( 'the_content', 'shortcode_unautop' );
    197197add_filter( 'the_content', 'prepend_attachment' );
    198 add_filter( 'the_content', 'wp_filter_content_tags' );
    199198add_filter( 'the_content', 'wp_replace_insecure_home_url' );
     199add_filter( 'the_content', 'do_shortcode', 11 ); // AFTER wpautop().
     200add_filter( 'the_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
    200201
    201202add_filter( 'the_excerpt', 'wptexturize' );
    … …  
    204205add_filter( 'the_excerpt', 'wpautop' );
    205206add_filter( 'the_excerpt', 'shortcode_unautop' );
    206 add_filter( 'the_excerpt', 'wp_filter_content_tags' );
    207207add_filter( 'the_excerpt', 'wp_replace_insecure_home_url' );
     208add_filter( 'the_excerpt', 'wp_filter_content_tags', 12 );
    208209add_filter( 'get_the_excerpt', 'wp_trim_excerpt', 10, 2 );
    209210
    … …  
    231232add_filter( 'widget_text_content', 'wpautop' );
    232233add_filter( 'widget_text_content', 'shortcode_unautop' );
    233 add_filter( 'widget_text_content', 'wp_filter_content_tags' );
    234234add_filter( 'widget_text_content', 'wp_replace_insecure_home_url' );
    235235add_filter( 'widget_text_content', 'do_shortcode', 11 ); // Runs after wpautop(); note that $post global will be null when shortcodes run.
     236add_filter( 'widget_text_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
    236237
    237238add_filter( 'widget_block_content', 'do_blocks', 9 );
    238 add_filter( 'widget_block_content', 'wp_filter_content_tags' );
    239239add_filter( 'widget_block_content', 'do_shortcode', 11 );
     240add_filter( 'widget_block_content', 'wp_filter_content_tags', 12 ); // Runs after do_shortcode().
    240241
    241242add_filter( 'block_type_metadata', 'wp_migrate_old_typography_shape' );
    … …  
    626627add_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );
    627628
    628 // Shortcodes.
    629 add_filter( 'the_content', 'do_shortcode', 11 ); // AFTER wpautop().
    630 
    631629// Media.
    632630add_action( 'wp_playlist_scripts', 'wp_playlist_scripts' );
  • trunk/src/wp-includes/formatting.php

    r56682 r56693  
    39813981                 * is wasteful and can lead to bugs in the image counting logic.
    39823982                 */
    3983                 $filter_image_removed = remove_filter( 'the_content', 'wp_filter_content_tags' );
     3983                $filter_image_removed = remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
    39843984
    39853985                /*
    … …  
    40044004                 */
    40054005                if ( $filter_image_removed ) {
    4006                         add_filter( 'the_content', 'wp_filter_content_tags' );
     4006                        add_filter( 'the_content', 'wp_filter_content_tags', 12 );
    40074007                }
    40084008
  • trunk/src/wp-includes/media.php

    r56690 r56693  
    56505650
    56515651        /*
    5652          * Skip programmatically created images within post content as they need to be handled together with the other
    5653          * images within the post content.
     5652         * Skip programmatically created images within content blobs as they need to be handled together with the other
     5653         * images within the post content or widget content.
    56545654         * Without this clause, they would already be considered within their own context which skews the image count and
    56555655         * can result in the first post content image being lazy-loaded or an image further down the page being marked as a
    56565656         * high priority.
    56575657         */
    5658         // TODO: Handle shortcode images together with the content (see https://core.trac.wordpress.org/ticket/58853).
    5659         if ( 'the_content' !== $context && 'do_shortcode' !== $context && doing_filter( 'the_content' ) ) {
     5658        if (
     5659                'the_content' !== $context && doing_filter( 'the_content' ) ||
     5660                'widget_text_content' !== $context && doing_filter( 'widget_text_content' ) ||
     5661                'widget_block_content' !== $context && doing_filter( 'widget_block_content' )
     5662        ) {
    56605663                /** This filter is documented in wp-includes/media.php */
    56615664                return apply_filters( 'wp_get_loading_optimization_attributes', $loading_attrs, $tag_name, $attr, $context );
     5665
    56625666        }
    56635667
  • trunk/tests/phpunit/tests/formatting/wpTrimExcerpt.php

    r56598 r56693  
    130130                wp_trim_excerpt( '', $post );
    131131
    132                 $this->assertSame( 10, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' );
     132                $this->assertSame( 12, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' );
    133133        }
    134134
    … …  
    142142
    143143                // Remove wp_filter_content_tags() from 'the_content' filter generally.
    144                 remove_filter( 'the_content', 'wp_filter_content_tags' );
     144                remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
    145145
    146146                wp_trim_excerpt( '', $post );
  • trunk/tests/phpunit/tests/media.php

    r56690 r56693  
    49594959        }
    49604960
     4961        /**
     4962         * Tests that wp_filter_content_tags() and more specifically wp_get_loading_optimization_attributes() correctly
     4963         * handle shortcodes images together with the content that it is part of.
     4964         *
     4965         * Images within shortcodes as part of the content should be ignored by wp_get_loading_optimization_attributes() to
     4966         * avoid double processing. They should instead only be processed together with any other images as part of the
     4967         * content, to correctly count the original sequencing of those images.
     4968         *
     4969         * @ticket 58853
     4970         *
     4971         * @covers ::wp_filter_content_tags
     4972         * @covers ::wp_get_loading_optimization_attributes
     4973         */
     4974        public function test_wp_filter_content_tags_handles_shortcode_image_together_with_the_content() {
     4975                global $wp_query, $wp_the_query;
     4976
     4977                // Add shortcode that prints a large image, and a block type that wraps it.
     4978                add_shortcode(
     4979                        'full_image',
     4980                        static function ( $atts ) {
     4981                                $atts = shortcode_atts(
     4982                                        array(
     4983                                                'id' => 0,
     4984                                        ),
     4985                                        $atts,
     4986                                        'full_image'
     4987                                );
     4988                                return wp_get_attachment_image( (int) $atts['id'], 'full' );
     4989                        }
     4990                );
     4991
     4992                /*
     4993                 * Even though `do_shortcode()` runs before `wp_filter_content_tags()`, the image from the shortcode should not
     4994                 * receive any loading optimization attributes because it needs to be considered together with the rest of the
     4995                 * post content, within `wp_filter_content_tags()`.
     4996                 * Since the hard-coded image appears before the shortcode image, it should receive `fetchpriority="high"`,
     4997                 * despite the shortcode image being parsed before it.
     4998                 */
     4999                $post_content  = '<img src="example.jpg" width="800" height="600">' . "\n";
     5000                $post_content .= '[full_image id="' . self::$large_id . '"]';
     5001                $post_content  = wpautop( $post_content );
     5002
     5003                /*
     5004                 * Prepare the expected output:
     5005                 * 1. On the first image (hard-coded in the content), expect `fetchpriority="high"`.
     5006                 * 2. Replace the shortcode with its expected output, i.e. the full image. Expect neither
     5007                 * `fetchpriority="high"` nor `loading="lazy"`.
     5008                 */
     5009                $expected_content = $post_content;
     5010                $expected_content = str_replace(
     5011                        '<img src="example.jpg"',
     5012                        '<img fetchpriority="high" decoding="async" src="example.jpg"',
     5013                        $expected_content
     5014                );
     5015                $expected_content = str_replace(
     5016                        '[full_image id="' . self::$large_id . '"]',
     5017                        str_replace(
     5018                                '<img ',
     5019                                '<img decoding="async" ',
     5020                                wp_get_attachment_image(
     5021                                        self::$large_id,
     5022                                        'full',
     5023                                        false,
     5024                                        array(
     5025                                                'decoding'      => false,
     5026                                                'fetchpriority' => false,
     5027                                                'loading'       => false,
     5028                                        )
     5029                                )
     5030                        ),
     5031                        $expected_content
     5032                );
     5033
     5034                // Create post with the content.
     5035                $post_id = self::factory()->post->create(
     5036                        array(
     5037                                'post_content' => $post_content,
     5038                                'post_excerpt' => '',
     5039                        )
     5040                );
     5041
     5042                // We have to run a main query loop so that the first 'the_content' context images are not lazy-loaded.
     5043                $wp_query     = new WP_Query( array( 'post__in' => array( $post_id ) ) );
     5044                $wp_the_query = $wp_query;
     5045
     5046                $content = '';
     5047                while ( have_posts() ) {
     5048                        the_post();
     5049                        $content = get_echo( 'the_content' );
     5050                }
     5051
     5052                // Cleanup.
     5053                remove_shortcode( 'full_image' );
     5054
     5055                $this->assertSame( $expected_content, $content );
     5056        }
     5057
     5058        /**
     5059         * Tests that wp_filter_content_tags() and more specifically wp_get_loading_optimization_attributes() correctly
     5060         * handle shortcodes images within the content, including within a block.
     5061         *
     5062         * Images within shortcodes as part of the content should be ignored by wp_get_loading_optimization_attributes() to
     5063         * avoid double processing. They should instead only be processed together with any other images as part of the
     5064         * content, to correctly count the original sequencing of those images.
     5065         *
     5066         * @ticket 58853
     5067         *
     5068         * @covers ::wp_filter_content_tags
     5069         * @covers ::wp_get_loading_optimization_attributes
     5070         */
     5071        public function test_wp_filter_content_tags_handles_shortcode_images_also_in_blocks_within_the_content() {
     5072                global $wp_query, $wp_the_query;
     5073
     5074                // Disable addition of `decoding="async"` as it is irrelevant for this test.
     5075                add_filter(
     5076                        'wp_get_loading_optimization_attributes',
     5077                        static function ( $loading_attrs ) {
     5078                                if ( isset( $loading_attrs['decoding'] ) ) {
     5079                                        unset( $loading_attrs['decoding'] );
     5080                                }
     5081                                return $loading_attrs;
     5082                        }
     5083                );
     5084
     5085                // Add shortcode that prints a large image, and a block type that wraps it.
     5086                add_shortcode(
     5087                        'full_image',
     5088                        static function ( $atts ) {
     5089                                $atts = shortcode_atts(
     5090                                        array(
     5091                                                'id' => 0,
     5092                                        ),
     5093                                        $atts,
     5094                                        'full_image'
     5095                                );
     5096                                return wp_get_attachment_image( (int) $atts['id'], 'full' );
     5097                        }
     5098                );
     5099                register_block_type(
     5100                        'core/full-image-shortcode',
     5101                        array(
     5102                                'render_callback' => static function ( $atts ) {
     5103                                        if ( empty( $atts['id'] ) ) {
     5104                                                return '';
     5105                                        }
     5106                                        return do_shortcode( '[full_image id="' . $atts['id'] . '"]' );
     5107                                },
     5108                        )
     5109                );
     5110
     5111                /*
     5112                 * Include the following images:
     5113                 * 1. Using gallery shortcode. Expected `fetchpriority="high"`.
     5114                 * 2. Regular hard-coded image.
     5115                 * 3. Using custom shortcode within block.
     5116                 * 4. Regular hard-coded image. Expected `loading="lazy"`.
     5117                 *
     5118                 * The first image is expected to be prioritized because it is the first (large enough) content image.
     5119                 * The first three images are expected to not have lazy-loading because that is the default threshold for
     5120                 * omitting the attribute.
     5121                 * The fourth image is expected to be lazy-loaded as it is past the default threshold.
     5122                 *
     5123                 * The results will only be correct if all images are considered together. For example:
     5124                 * * If the image within the shortcode would only be parsed after the rest of the content, it would miss the
     5125                 * `fetchpriority="high"` attribute and instead incorrectly receive `loading="lazy"`. The second image would as
     5126                 * a result incorrectly receive `fetchpriority="high"`.
     5127                 * * If the image within the block would be parsed before the rest of the content, it would incorrectly receive
     5128                 * the `fetchpriority="high"` attribute. Then the first image would no longer receive the attribute.
     5129                 *
     5130                 * To ensure that this works:
     5131                 * * `wp_filter_content_tags()` must run after `do_blocks()` and `do_shortcode()`.
     5132                 * * `wp_get_loading_optimization_attributes()` must bail early if any images from the content blob are being
     5133                 * considered under a different context name than 'the_content'.
     5134                 */
     5135                $post_content  = '[gallery ids="' . self::$large_id . '" size="large"]' . "\n";
     5136                $post_content .= '<img src="example.jpg" width="800" height="600">' . "\n";
     5137                $post_content .= '<p>Some text.</p>' . "\n";
     5138                $post_content .= '<!-- wp:core/full-image-shortcode {"id":' . self::$large_id . '} --><!-- /wp:core/full-image-shortcode -->' . "\n";
     5139                $post_content .= '<img src="example2.jpg" width="800" height="600">';
     5140
     5141                $post_id = self::factory()->post->create(
     5142                        array(
     5143                                'post_content' => $post_content,
     5144                                'post_excerpt' => '',
     5145                        )
     5146                );
     5147
     5148                /*
     5149                 * Prepare the expected output:
     5150                 * 1. Replace the shortcode with its expected output (ID increased by 1 because of static variable within
     5151                 * the gallery_shortcode() function). Expect `fetchpriority="high"`, but not `loading="lazy"`.
     5152                 * 2. Do not modify the second image as it is hard-coded in the content and expected to be unchanged.
     5153                 * 3. Replace the block with its expected output, i.e. the full image from the shortcode within. Expect neither
     5154                 * `fetchpriority="high"` nor `loading="lazy"`.
     5155                 * 4. On the fourth image (hard-coded in the content), expect `loading="lazy"`.
     5156                 */
     5157                $expected_content = $post_content;
     5158                $expected_content = str_replace(
     5159                        '[gallery ids="' . self::$large_id . '" size="large"]',
     5160                        str_replace(
     5161                                array( ' loading="lazy"', '<img ' ),
     5162                                array( '', '<img fetchpriority="high" ' ),
     5163                                preg_replace_callback(
     5164                                        '/gallery-(\d+)/',
     5165                                        static function ( $match ) {
     5166                                                return 'gallery-' . ( (int) $match[1] + 1 );
     5167                                        },
     5168                                        do_shortcode( '[gallery ids="' . self::$large_id . '" size="large" id="' . $post_id . '"]' )
     5169                                )
     5170                        ),
     5171                        $expected_content
     5172                );
     5173                $expected_content = str_replace(
     5174                        '<!-- wp:core/full-image-shortcode {"id":' . self::$large_id . '} --><!-- /wp:core/full-image-shortcode -->',
     5175                        wp_get_attachment_image(
     5176                                self::$large_id,
     5177                                'full',
     5178                                false,
     5179                                array(
     5180                                        'fetchpriority' => false,
     5181                                        'loading'       => false,
     5182                                )
     5183                        ),
     5184                        $expected_content
     5185                );
     5186                $expected_content = str_replace(
     5187                        '<img src="example2.jpg"',
     5188                        '<img loading="lazy" src="example2.jpg"',
     5189                        $expected_content
     5190                );
     5191
     5192                // We have to run a main query loop so that the first 'the_content' context images are not lazy-loaded.
     5193                $wp_query     = new WP_Query( array( 'post__in' => array( $post_id ) ) );
     5194                $wp_the_query = $wp_query;
     5195
     5196                $content = '';
     5197                while ( have_posts() ) {
     5198                        the_post();
     5199                        $content = get_echo( 'the_content' );
     5200                }
     5201
     5202                // Cleanup.
     5203                remove_shortcode( 'full_image' );
     5204                unregister_block_type( 'core/full-image-shortcode' );
     5205
     5206                $this->assertSame( $expected_content, $content );
     5207        }
     5208
    49615209        private function reset_content_media_count() {
    49625210                // Get current value without increasing.
    … …  
    53195567
    53205568        /**
     5569         * Tests that the `do_shortcode` context results in a lazy-loaded image by default.
     5570         *
    53215571         * @ticket 58681
    5322          *
    5323          * @dataProvider data_wp_get_loading_optimization_attributes_in_shortcodes
    5324          */
    5325         public function test_wp_get_loading_optimization_attributes_in_shortcodes( $setup, $expected, $message ) {
     5572         * @ticket 58853
     5573         *
     5574         * @covers ::wp_get_loading_optimization_attributes
     5575         */
     5576        public function test_wp_get_loading_optimization_attributes_in_shortcodes() {
    53265577                $attr = $this->get_width_height_for_high_priority();
    5327                 $setup();
    5328 
    5329                 // The first image processed in a shortcode should have fetchpriority set to high.
     5578
     5579                // Shortcodes processed outside of content blobs like 'the_content' always get `loading="lazy"`.
    53305580                $this->assertSameSetsWithIndex(
    5331                         $expected,
     5581                        array(
     5582                                'decoding' => 'async',
     5583                                'loading'  => 'lazy',
     5584                        ),
    53325585                        wp_get_loading_optimization_attributes( 'img', $attr, 'do_shortcode' ),
    5333                         $message
    5334                 );
    5335         }
    5336 
    5337         public function data_wp_get_loading_optimization_attributes_in_shortcodes() {
    5338                 return array(
    5339                         'main_shortcode_image_should_have_fetchpriority_high'  => array(
    5340                                 'setup'    => function () {
    5341                                         global $wp_query;
    5342 
    5343                                         // Set WP_Query to be in the loop and the main query.
    5344                                         $wp_query->in_the_loop = true;
    5345                                         $this->set_main_query( $wp_query );
    5346                                 },
    5347                                 'expected' => array(
    5348                                         'decoding'      => 'async',
    5349                                         'fetchpriority' => 'high',
    5350                                 ),
    5351                                 'message'  => 'Fetch priority not applied to during shortcode rendering.',
    5352                         ),
    5353                         'main_shortcode_image_after_threshold_is_loading_lazy' => array(
    5354                                 'setup'    => function () {
    5355                                         global $wp_query;
    5356 
    5357                                         // Set WP_Query to be in the loop and the main query.
    5358                                         $wp_query->in_the_loop = true;
    5359                                         $this->set_main_query( $wp_query );
    5360 
    5361                                         // Set internal flags so lazy should be applied.
    5362                                         wp_high_priority_element_flag( false );
    5363                                         wp_increase_content_media_count( 3 );
    5364                                 },
    5365                                 'expected' => array(
    5366                                         'decoding' => 'async',
    5367                                         'loading'  => 'lazy',
    5368                                 ),
    5369                                 'message'  => 'Lazy-loading or decoding not applied to during shortcode rendering.',
    5370                         ),
    5371                         'shortcode_image_outside_of_the_loop_are_loaded_lazy'  => array(
    5372                                 'setup'    => function () {
    5373                                         // Avoid setting up the WP_Query object for the loop.
    5374                                         return;
    5375                                 },
    5376                                 'expected' => array(
    5377                                         'decoding' => 'async',
    5378                                         'loading'  => 'lazy',
    5379                                 ),
    5380                                 'message'  => 'Lazy-loading or decoding not applied to shortcodes outside the loop.',
    5381                         ),
     5586                        'Lazy-loading not applied to shortcodes outside the loop.'
     5587                );
     5588        }
     5589
     5590        /**
     5591         * Tests that the `do_shortcode` context does not result in loading optimization changes when used within a content
     5592         * blob.
     5593         *
     5594         * @ticket 58853
     5595         *
     5596         * @covers ::wp_get_loading_optimization_attributes
     5597         *
     5598         * @dataProvider data_get_filters_with_do_shortcode_callback
     5599         *
     5600         * @param string $filter_name The name of the filter to hook.
     5601         */
     5602        public function test_wp_get_loading_optimization_attributes_in_shortcodes_within_content_blob( $filter_name ) {
     5603                $result = null;
     5604
     5605                remove_all_filters( $filter_name );
     5606                add_filter(
     5607                        $filter_name,
     5608                        function ( $content ) use ( &$result ) {
     5609                                $attr   = $this->get_width_height_for_high_priority();
     5610                                $result = wp_get_loading_optimization_attributes( 'img', $attr, 'do_shortcode' );
     5611                                return $content;
     5612                        }
     5613                );
     5614                apply_filters( $filter_name, '' );
     5615
     5616                // Shortcodes processed within content blobs like 'the_content' should never get any loading optimization attributes.
     5617                $this->assertSame(
     5618                        array(),
     5619                        $result,
     5620                        'Loading optimization unexpectedly applied to shortcodes within content blob.'
     5621                );
     5622        }
     5623
     5624        /**
     5625         * Gets filters for content blobs that by default have a `do_shortcode()` callback.
     5626         *
     5627         * @return array[]
     5628         */
     5629        public function data_get_filters_with_do_shortcode_callback() {
     5630                return self::text_array_to_dataprovider(
     5631                        array(
     5632                                'the_content',
     5633                                'widget_text_content',
     5634                                'widget_block_content',
     5635                        )
    53825636                );
    53835637        }
Note: See TracChangeset for help on using the changeset viewer.