diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index dd98b5e622..335e415798 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5652,13 +5652,9 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { * can result in the first post content image being lazy-loaded or an image further down the page being marked as a * high priority. */ - switch ( $context ) { - case 'the_post_thumbnail': - case 'wp_get_attachment_image': - case 'widget_media_image': - if ( doing_filter( 'the_content' ) ) { - return $loading_attrs; - } + // 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' ) ) { + return $loading_attrs; } /* @@ -5709,64 +5705,56 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } if ( null === $maybe_in_viewport ) { - switch ( $context ) { - // Consider elements with these header-specific contexts to be in viewport. - case 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER: - case 'get_header_image_tag': - $maybe_in_viewport = true; - $maybe_increase_count = true; - break; - // Count main content elements and detect whether in viewport. - case 'the_content': - case 'the_post_thumbnail': - case 'do_shortcode': - // Only elements within the main query loop have special handling. - if ( ! is_admin() && in_the_loop() && is_main_query() ) { - /* - * Get the content media count, since this is a main query - * content element. This is accomplished by "increasing" - * the count by zero, as the only way to get the count is - * to call this function. - * The actual count increase happens further below, based - * on the `$increase_count` flag set here. - */ - $content_media_count = wp_increase_content_media_count( 0 ); - $increase_count = true; + $header_enforced_contexts = array( + 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER => true, + 'get_header_image_tag' => true, + ); - // If the count so far is below the threshold, `loading` attribute is omitted. - if ( $content_media_count < wp_omit_loading_attr_threshold() ) { - $maybe_in_viewport = true; - } else { - $maybe_in_viewport = false; - } - } - /* - * For the 'the_post_thumbnail' context, the following case - * clause needs to be considered as well, therefore skip the - * break statement here if the viewport has not been - * determined. - */ - if ( 'the_post_thumbnail' !== $context || null !== $maybe_in_viewport ) { - break; - } - // phpcs:ignore Generic.WhiteSpace.ScopeIndent.Incorrect - // Consider elements before the loop as being in viewport. - case 'wp_get_attachment_image': - case 'widget_media_image': - if ( - // Only apply for main query but before the loop. - $wp_query->before_loop && $wp_query->is_main_query() - /* - * Any image before the loop, but after the header has started should not be lazy-loaded, - * except when the footer has already started which can happen when the current template - * does not include any loop. - */ - && did_action( 'get_header' ) && ! did_action( 'get_footer' ) - ) { - $maybe_in_viewport = true; - $maybe_increase_count = true; - } - break; + /** + * Filters the header-specific contexts. + * + * @since 6.4.0 + * + * @param array $default_header_enforced_contexts Map of contexts for which elements should be considered + * in the header of the page, as $context => $enabled + * pairs. The $enabled should always be true. + */ + $header_enforced_contexts = apply_filters( 'wp_loading_optimization_force_header_contexts', $header_enforced_contexts ); + + // Consider elements with these header-specific contexts to be in viewport. + if ( isset( $header_enforced_contexts[ $context ] ) ) { + $maybe_in_viewport = true; + $maybe_increase_count = true; + } elseif ( ! is_admin() && in_the_loop() && is_main_query() ) { + /* + * Get the content media count, since this is a main query + * content element. This is accomplished by "increasing" + * the count by zero, as the only way to get the count is + * to call this function. + * The actual count increase happens further below, based + * on the `$increase_count` flag set here. + */ + $content_media_count = wp_increase_content_media_count( 0 ); + $increase_count = true; + + // If the count so far is below the threshold, `loading` attribute is omitted. + if ( $content_media_count < wp_omit_loading_attr_threshold() ) { + $maybe_in_viewport = true; + } else { + $maybe_in_viewport = false; + } + } elseif ( + // Only apply for main query but before the loop. + $wp_query->before_loop && $wp_query->is_main_query() + /* + * Any image before the loop, but after the header has started should not be lazy-loaded, + * except when the footer has already started which can happen when the current template + * does not include any loop. + */ + && did_action( 'get_header' ) && ! did_action( 'get_footer' ) + ) { + $maybe_in_viewport = true; + $maybe_increase_count = true; } } diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index ebd2f767e1..f90378b111 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4301,15 +4301,6 @@ EOF; // Set as main query. $this->set_main_query( $query ); - /* - * For contexts other than for the main content, still return 'lazy' even in the loop - * and in the main query, and do not increase the content media count. - */ - $this->assertSame( - array( 'loading' => 'lazy' ), - wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) - ); - // First three element are not lazy loaded. However, first image is loaded with fetchpriority high. $this->assertSame( array( 'fetchpriority' => 'high' ), @@ -4339,6 +4330,184 @@ EOF; } } + /** + * Tests that wp_get_loading_optimization_attributes() returns fetchpriority=high and increases the count for arbitrary contexts in the main loop. + * + * @ticket 58894 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_optimization_attributes_arbitrary_contexts + * + * @param string $context Context for the element for which the loading optimization attribute is requested. + */ + public function test_wp_get_loading_optimization_attributes_with_arbitrary_contexts_in_main_loop( $context ) { + $attr = $this->get_width_height_for_high_priority(); + + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'The "loading" attribute should be "lazy" when not in the loop or the main query.' + ); + + $query = $this->get_new_wp_query_for_published_post(); + + // Set as main query. + $this->set_main_query( $query ); + + while ( have_posts() ) { + the_post(); + + $this->assertSame( + array( 'fetchpriority' => 'high' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'The "fetchpriority" attribute should be "high" while in the loop and the main query.' + ); + + // Images with a certain minimum size in the arbitrary contexts of the page are also counted towards the threshold. + $this->assertSame( 1, wp_increase_content_media_count( 0 ), 'The content media count should be 1.' ); + } + } + + /** + * Tests that wp_get_loading_optimization_attributes() does not return lazy loading attributes when arbitrary contexts are used before the main query loop. + * + * @ticket 58894 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_optimization_attributes_arbitrary_contexts + * + * @param string $context Context for the element for which the loading optimization attribute is requested. + */ + public function test_wp_get_loading_optimization_attributes_with_arbitrary_contexts_before_main_query_loop( $context ) { + $attr = $this->get_width_height_for_high_priority(); + + $query = $this->get_new_wp_query_for_published_post(); + + // Set as main query. + $this->set_main_query( $query ); + + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'The "loading" attribute should be "lazy" before the main query loop.' + ); + + while ( have_posts() ) { + the_post(); + + $this->assertSame( + array( 'fetchpriority' => 'high' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'The "fetchpriority" attribute should be "high" while in the loop and the main query.' + ); + + $this->assertArrayNotHasKey( + 'loading', + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'No "loading" attribute should be present on the second image in the main query loop.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_get_loading_optimization_attributes_arbitrary_contexts() { + return array( + array( 'wp_get_attachment_image' ), + array( 'something_completely_arbitrary' ), + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns empty array for arbitrary context. + * + * @ticket 58894 + * + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_loading_optimization_attributes_should_return_empty_array_for_any_arbitrary_context() { + remove_all_filters( 'the_content' ); + + $result = null; + add_filter( + 'the_content', + function( $content ) use ( &$result ) { + $attr = $this->get_width_height_for_high_priority(); + $result = wp_get_loading_optimization_attributes( 'img', $attr, 'something_completely_arbitrary' ); + return $content; + } + ); + apply_filters( 'the_content', '' ); + + $this->assertSame( array(), $result ); + } + + /** + * @ticket 58894 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_optimization_attributes_header_context + * + * @param string $context The context for the header. + */ + public function test_wp_get_loading_optimization_attributes_header_contexts( $context ) { + $attr = $this->get_width_height_for_high_priority(); + + $this->assertArrayNotHasKey( + 'loading', + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Images in the header context should not be lazy-loaded.' + ); + + add_filter( 'wp_loading_optimization_force_header_contexts', '__return_empty_array' ); + + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Images in the header context should get lazy-loaded after the wp_loading_optimization_force_header_contexts filter.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_get_loading_optimization_attributes_header_context() { + return array( + array( 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER ), + array( 'get_header_image_tag' ), + ); + } + + /** + * @ticket 58894 + * + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_loading_optimization_force_header_contexts_filter() { + $attr = $this->get_width_height_for_high_priority(); + + add_filter( + 'wp_loading_optimization_force_header_contexts', + function( $context ) { + $contexts['something_completely_arbitrary'] = true; + return $contexts; + } + ); + + $this->assertSame( + array( 'fetchpriority' => 'high' ), + wp_get_loading_optimization_attributes( 'img', $attr, 'something_completely_arbitrary' ) + ); + } + /** * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value before loop but after get_header if not main query. *