diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 6b9e9d0bb80b..38b89601fb6d 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -108,6 +108,14 @@ class WP_Query { */ public $current_post = -1; + /** + * Whether the caller is before the loop. + * + * @since 6.3.0 + * @var bool + */ + public $before_loop = true; + /** * Whether the loop has started and the caller is in the loop. * @@ -517,6 +525,7 @@ public function init() { $this->post_count = 0; $this->current_post = -1; $this->in_the_loop = false; + $this->before_loop = true; unset( $this->request ); unset( $this->post ); unset( $this->comments ); @@ -3631,6 +3640,7 @@ public function the_post() { } $this->in_the_loop = true; + $this->before_loop = false; if ( -1 == $this->current_post ) { // Loop has just started. /** @@ -3671,6 +3681,8 @@ public function have_posts() { // Do some cleaning up after the loop. $this->rewind_posts(); } elseif ( 0 === $this->post_count ) { + $this->before_loop = false; + /** * Fires if no results are found in a post query. * diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 3ea286341451..b7d525555bd6 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5490,30 +5490,52 @@ function wp_get_webp_info( $filename ) { * * @since 5.9.0 * + * @global WP_Query $wp_query WordPress Query object. + * * @param string $context Context for the element for which the `loading` attribute value is requested. * @return string|bool The default `loading` attribute value. Either 'lazy', 'eager', or a boolean `false`, to indicate * that the `loading` attribute should be skipped. */ function wp_get_loading_attr_default( $context ) { + global $wp_query; + // Skip lazy-loading for the overall block template, as it is handled more granularly. if ( 'template' === $context ) { return false; } // Do not lazy-load images in the header block template part, as they are likely above the fold. + // For classic themes, this is handled in the condition below using the 'get_header' action. $header_area = WP_TEMPLATE_PART_AREA_HEADER; if ( "template_part_{$header_area}" === $context ) { return false; } - /* - * Skip programmatically created images within post content as they need to be handled together with the other - * images within the post content. - * Without this clause, they would already be counted below which skews the number and can result in the first - * post content image being lazy-loaded only because there are images elsewhere in the post content. - */ - if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) && doing_filter( 'the_content' ) ) { - return false; + // Special handling for programmatically created image tags. + if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) ) { + /* + * Skip programmatically created images within post content as they need to be handled together with the other + * images within the post content. + * Without this clause, they would already be counted below which skews the number and can result in the first + * post content image being lazy-loaded only because there are images elsewhere in the post content. + */ + if ( doing_filter( 'the_content' ) ) { + return false; + } + + // Conditionally skip lazy-loading on images before the loop. + 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' ) + ) { + return false; + } } /* diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index cf7c72b06d5e..c38a17d1cfbe 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -3559,8 +3559,6 @@ public function data_attachment_permalinks_based_on_parent_status() { * @param string $context */ public function test_wp_get_loading_attr_default( $context ) { - global $wp_query, $wp_the_query; - // Return 'lazy' by default. $this->assertSame( 'lazy', wp_get_loading_attr_default( 'test' ) ); $this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) ); @@ -3568,7 +3566,7 @@ public function test_wp_get_loading_attr_default( $context ) { // Return 'lazy' if not in the loop or the main query. $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); - $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); + $query = $this->get_new_wp_query_for_published_post(); $this->reset_content_media_count(); $this->reset_omit_loading_attr_filter(); @@ -3579,7 +3577,7 @@ public function test_wp_get_loading_attr_default( $context ) { $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); // Set as main query. - $wp_the_query = $wp_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. @@ -3613,10 +3611,8 @@ public function data_wp_get_loading_attr_default() { * @ticket 53675 */ public function test_wp_omit_loading_attr_threshold_filter() { - global $wp_query, $wp_the_query; - - $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); - $wp_the_query = $wp_query; + $query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $query ); $this->reset_content_media_count(); $this->reset_omit_loading_attr_filter(); @@ -3640,8 +3636,6 @@ public function test_wp_omit_loading_attr_threshold_filter() { * @ticket 53675 */ public function test_wp_filter_content_tags_with_wp_get_loading_attr_default() { - global $wp_query, $wp_the_query; - $img1 = get_image_tag( self::$large_id, '', '', '', 'large' ); $iframe1 = ''; $img2 = get_image_tag( self::$large_id, '', '', '', 'medium' ); @@ -3659,8 +3653,8 @@ public function test_wp_filter_content_tags_with_wp_get_loading_attr_default() { $content_expected = $img1 . $iframe1 . $lazy_img2 . $lazy_img3 . $lazy_iframe2; $content_expected = wp_img_tag_add_decoding_attr( $content_expected, 'the_content' ); - $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); - $wp_the_query = $wp_query; + $query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $query ); $this->reset_content_media_count(); $this->reset_omit_loading_attr_filter(); @@ -3698,6 +3692,142 @@ public function test_wp_omit_loading_attr_threshold() { $this->assertSame( 1, $omit_threshold ); } + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header if not main query. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_before_loop_if_not_main_query( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->reset_content_media_count(); + $this->reset_omit_loading_attr_filter(); + + do_action( 'get_header' ); + + // Lazy if not main query. + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header in main query but header was not called. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_before_loop_in_main_query_but_header_not_called( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + $this->reset_content_media_count(); + $this->reset_omit_loading_attr_filter(); + + // Lazy if header not called. + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value before loop but after get_header for main query. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_before_loop_if_main_query( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + $this->reset_content_media_count(); + $this->reset_omit_loading_attr_filter(); + + do_action( 'get_header' ); + $this->assertFalse( wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value after get_header and after loop. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_after_loop( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + $this->reset_content_media_count(); + $this->reset_omit_loading_attr_filter(); + + do_action( 'get_header' ); + + while ( have_posts() ) { + the_post(); + } + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute if no loop. + * + * @ticket 58211 + * + * @covers ::wp_get_loading_attr_default + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_attr_default_no_loop( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + $this->reset_content_media_count(); + $this->reset_omit_loading_attr_filter(); + + // Ensure header and footer is called. + do_action( 'get_header' ); + do_action( 'get_footer' ); + + // Load lazy if the there is no loop and footer was called. + $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_get_loading_attr_default_before_and_no_loop() { + return array( + array( 'wp_get_attachment_image' ), + array( 'the_post_thumbnail' ), + ); + } + /** * Tests that wp_filter_content_tags() does not add loading="lazy" to the first * image in the loop when using a block theme. @@ -4166,6 +4296,34 @@ static function() use ( $threshold ) { } ); } + + /** + * Returns a new WP_Query. + * + * @global WP_Query $wp_query WordPress Query object. + * + * @return WP_Query a new query. + */ + public function get_new_wp_query_for_published_post() { + global $wp_query; + + // New query to $wp_query. update global for the loop. + $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) ); + + return $wp_query; + } + + /** + * Sets a query as main query. + * + * @global WP_Query $wp_the_query WordPress Query object. + * + * @param WP_Query $query query to be set as main query. + */ + public function set_main_query( $query ) { + global $wp_the_query; + $wp_the_query = $query; + } } /** diff --git a/tests/phpunit/tests/query.php b/tests/phpunit/tests/query.php index 9c4ca439a51e..52c6ebfc12ec 100644 --- a/tests/phpunit/tests/query.php +++ b/tests/phpunit/tests/query.php @@ -897,4 +897,71 @@ public function test_query_tag_404_does_not_throw_warning() { $this->assertFalse( $q->is_tax() ); $this->assertFalse( $q->is_tag( 'non-existent-tag' ) ); } + + /** + * Test if $before_loop is true before loop. + * + * @ticket 58211 + */ + public function test_before_loop_value_set_true_before_the_loop() { + // Get a new query with 3 posts. + $query = $this->get_new_wp_query_with_posts( 3 ); + + $this->assertTrue( $query->before_loop ); + } + + /** + * Test $before_loop value is set to false when the loop starts. + * + * @ticket 58211 + * + * @covers WP_Query::the_post + */ + public function test_before_loop_value_set_to_false_in_loop_with_post() { + // Get a new query with 2 posts. + $query = $this->get_new_wp_query_with_posts( 2 ); + + while ( $query->have_posts() ) { + // $before_loop should be set false as soon as the_post is called for the first time. + $query->the_post(); + + $this->assertFalse( $query->before_loop ); + break; + } + } + + /** + * Test $before_loop value is set to false when there is no post in the loop. + * + * @ticket 58211 + * + * @covers WP_Query::have_posts + */ + public function test_before_loop_set_false_after_loop_with_no_post() { + // New query without any posts in the result. + $query = new WP_Query( + array( + 'category_name' => 'non-existent-category', + ) + ); + + // There will not be any posts, so the loop will never actually enter. + while ( $query->have_posts() ) { + $query->the_post(); + } + + // Still, this should be false as there are no results and entering the loop was attempted. + $this->assertFalse( $query->before_loop ); + } + + /** + * Get a new query with a given number of posts. + * + * @param int $no_of_posts Number of posts to be added in the query. + */ + public function get_new_wp_query_with_posts( $no_of_posts ) { + $post_ids = self::factory()->post->create_many( $no_of_posts ); + $query = new WP_Query( array( 'post__in' => $post_ids ) ); + return $query; + } }