Make WordPress Core

Changeset 56693


Ignore:
Timestamp:
09/26/2023 12:11:06 AM (8 months 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.