From beb50703339e54cdeae59ea81e4911ccda8f3d5e Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 29 Mar 2026 04:28:51 +0000 Subject: [PATCH 1/6] Feeds: Guard against empty array passed to max() in get_feed_build_date(). When `wp_list_pluck()` returns an empty array from posts that lack the `post_modified_gmt` property, `max()` throws a `ValueError` on PHP 8+. This adds an emptiness check before calling `max()`, allowing `$datetime` to remain `false` and fall through to the existing `get_lastpostmodified()` fallback. Includes a unit test that simulates the condition. Fixes #59956. --- src/wp-includes/feed.php | 4 +- tests/phpunit/tests/date/getFeedBuildDate.php | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/feed.php b/src/wp-includes/feed.php index 821a3eb9be804..7b6045ce9c2a9 100644 --- a/src/wp-includes/feed.php +++ b/src/wp-includes/feed.php @@ -733,7 +733,9 @@ function get_feed_build_date( $format ) { } // Determine the maximum modified time. - $datetime = date_create_immutable_from_format( 'Y-m-d H:i:s', max( $modified_times ), $utc ); + if ( ! empty( $modified_times ) ) { + $datetime = date_create_immutable_from_format( 'Y-m-d H:i:s', max( $modified_times ), $utc ); + } } if ( false === $datetime ) { diff --git a/tests/phpunit/tests/date/getFeedBuildDate.php b/tests/phpunit/tests/date/getFeedBuildDate.php index d884c0baef05c..54a4d0189c131 100644 --- a/tests/phpunit/tests/date/getFeedBuildDate.php +++ b/tests/phpunit/tests/date/getFeedBuildDate.php @@ -40,6 +40,45 @@ public function test_should_return_correct_feed_build_date() { $this->assertSame( '2018-07-23T03:13:23+00:00', get_feed_build_date( DATE_RFC3339 ) ); } + /** + * Test that get_feed_build_date() does not throw a ValueError + * when wp_list_pluck() returns an empty array from posts that + * lack the 'post_modified_gmt' property. + * + * @ticket 59956 + */ + public function test_should_not_error_when_modified_times_is_empty() { + global $wp_query; + + $datetime = new DateTimeImmutable( 'now', wp_timezone() ); + $datetime_utc = $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); + + // Create a real post so the fallback has something to return. + self::factory()->post->create( + array( + 'post_date' => $datetime->format( 'Y-m-d H:i:s' ), + ) + ); + + // Simulate a query where have_posts() is true but posts lack 'post_modified_gmt'. + $wp_query = new WP_Query(); + $incomplete_post = new stdClass(); + $incomplete_post->ID = 1; + $incomplete_post->post_title = 'Test'; + + $wp_query->posts = array( $incomplete_post ); + $wp_query->post_count = 1; + + $result = get_feed_build_date( DATE_RFC3339 ); + + $this->assertEqualsWithDelta( + strtotime( $datetime_utc->format( DATE_RFC3339 ) ), + strtotime( $result ), + 2, + 'Should fall back to last post modified when modified_times is empty.' + ); + } + /** * Test that get_feed_build_date() works with invalid post dates. * From eeb7bd58f6495468fff135bdb14925a7e072e9f4 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 29 Mar 2026 04:35:36 +0000 Subject: [PATCH 2/6] Fix PHPCS: align equals signs in test assignments. Aligns variable assignments in the test to satisfy the Generic.Formatting.MultipleStatementAlignment sniff. --- tests/phpunit/tests/date/getFeedBuildDate.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/date/getFeedBuildDate.php b/tests/phpunit/tests/date/getFeedBuildDate.php index 54a4d0189c131..e42d1074b4fa6 100644 --- a/tests/phpunit/tests/date/getFeedBuildDate.php +++ b/tests/phpunit/tests/date/getFeedBuildDate.php @@ -61,8 +61,9 @@ public function test_should_not_error_when_modified_times_is_empty() { ); // Simulate a query where have_posts() is true but posts lack 'post_modified_gmt'. - $wp_query = new WP_Query(); - $incomplete_post = new stdClass(); + $wp_query = new WP_Query(); + + $incomplete_post = new stdClass(); $incomplete_post->ID = 1; $incomplete_post->post_title = 'Test'; From 5b79996a4b8ab3c180b33d2f305fb6544c5ca129 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 29 Mar 2026 04:48:22 +0000 Subject: [PATCH 3/6] Tests: Fix test to match actual production trigger for empty modified_times. The production failure occurs when the posts array contains scalar values (e.g. post IDs) rather than post objects. wp_list_pluck() emits a _doing_it_wrong notice and returns an empty array, which then triggers the max() ValueError. Update the test to use an integer in the posts array and call setExpectedIncorrectUsage() to properly handle the expected notice. See #59956. --- tests/phpunit/tests/date/getFeedBuildDate.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/date/getFeedBuildDate.php b/tests/phpunit/tests/date/getFeedBuildDate.php index e42d1074b4fa6..9889508dc91e6 100644 --- a/tests/phpunit/tests/date/getFeedBuildDate.php +++ b/tests/phpunit/tests/date/getFeedBuildDate.php @@ -42,8 +42,8 @@ public function test_should_return_correct_feed_build_date() { /** * Test that get_feed_build_date() does not throw a ValueError - * when wp_list_pluck() returns an empty array from posts that - * lack the 'post_modified_gmt' property. + * when wp_list_pluck() returns an empty array because the posts + * array contains non-object, non-array values. * * @ticket 59956 */ @@ -60,16 +60,17 @@ public function test_should_not_error_when_modified_times_is_empty() { ) ); - // Simulate a query where have_posts() is true but posts lack 'post_modified_gmt'. + // Simulate a query where have_posts() is true but wp_list_pluck() + // returns an empty array because the posts array contains scalar + // values (neither objects nor arrays). This triggers the _doing_it_wrong + // notice in WP_List_Util::pluck() and produces an empty result. $wp_query = new WP_Query(); - $incomplete_post = new stdClass(); - $incomplete_post->ID = 1; - $incomplete_post->post_title = 'Test'; - - $wp_query->posts = array( $incomplete_post ); + $wp_query->posts = array( 1 ); $wp_query->post_count = 1; + $this->setExpectedIncorrectUsage( 'WP_List_Util::pluck' ); + $result = get_feed_build_date( DATE_RFC3339 ); $this->assertEqualsWithDelta( From 6b4c4c3d9d135f01017ebecf555d1773bfa3ca65 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 24 Apr 2026 08:34:29 -0400 Subject: [PATCH 4/6] Feeds: Address review feedback on get_feed_build_date() empty array fix. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace `! empty( $modified_times )` with truthy check `if ( $modified_times )`, aligning with the ongoing effort to avoid empty() in new core code. - Rewrite the test setup to use the real query codepath: self::factory()->post->create() + query_posts( [ 'posts__in' => [ $id ], 'fields' => 'ids' ] ). 'fields' => 'ids' makes WP_Query->posts an array of integer IDs, which causes wp_list_pluck() to emit _doing_it_wrong and return an empty array naturally — no more manual $wp_query property mutation. - Add $this->assertIsString( $result ) to narrow the string|false return type before it flows into strtotime(), keeping PHPStan happy at higher analysis levels. Addresses review feedback from @westonruter. Props westonruter. See #59956. --- src/wp-includes/feed.php | 2 +- tests/phpunit/tests/date/getFeedBuildDate.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/feed.php b/src/wp-includes/feed.php index 7b6045ce9c2a9..890ae6fe07ff5 100644 --- a/src/wp-includes/feed.php +++ b/src/wp-includes/feed.php @@ -733,7 +733,7 @@ function get_feed_build_date( $format ) { } // Determine the maximum modified time. - if ( ! empty( $modified_times ) ) { + if ( $modified_times ) { $datetime = date_create_immutable_from_format( 'Y-m-d H:i:s', max( $modified_times ), $utc ); } } diff --git a/tests/phpunit/tests/date/getFeedBuildDate.php b/tests/phpunit/tests/date/getFeedBuildDate.php index 9889508dc91e6..cea58411f2882 100644 --- a/tests/phpunit/tests/date/getFeedBuildDate.php +++ b/tests/phpunit/tests/date/getFeedBuildDate.php @@ -48,30 +48,30 @@ public function test_should_return_correct_feed_build_date() { * @ticket 59956 */ public function test_should_not_error_when_modified_times_is_empty() { - global $wp_query; - $datetime = new DateTimeImmutable( 'now', wp_timezone() ); $datetime_utc = $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); - // Create a real post so the fallback has something to return. - self::factory()->post->create( + $id = self::factory()->post->create( array( 'post_date' => $datetime->format( 'Y-m-d H:i:s' ), ) ); - // Simulate a query where have_posts() is true but wp_list_pluck() + // Use a query where have_posts() is true but wp_list_pluck() // returns an empty array because the posts array contains scalar // values (neither objects nor arrays). This triggers the _doing_it_wrong // notice in WP_List_Util::pluck() and produces an empty result. - $wp_query = new WP_Query(); - - $wp_query->posts = array( 1 ); - $wp_query->post_count = 1; + query_posts( + array( + 'posts__in' => array( $id ), + 'fields' => 'ids', + ) + ); $this->setExpectedIncorrectUsage( 'WP_List_Util::pluck' ); $result = get_feed_build_date( DATE_RFC3339 ); + $this->assertIsString( $result ); $this->assertEqualsWithDelta( strtotime( $datetime_utc->format( DATE_RFC3339 ) ), From 7962e11010b0e8e986115a7fd07666c1632fcd74 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 24 Apr 2026 13:34:59 -0400 Subject: [PATCH 5/6] Tests: Use multi-line block comment style in get_feed_build_date test. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow WordPress inline documentation standards section 5.2 — multi-line comments use /* */ with leading asterisks, not repeated //. Addresses follow-up feedback from @westonruter. Props westonruter. See #59956. --- tests/phpunit/tests/date/getFeedBuildDate.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/date/getFeedBuildDate.php b/tests/phpunit/tests/date/getFeedBuildDate.php index cea58411f2882..41a7c9dddda1e 100644 --- a/tests/phpunit/tests/date/getFeedBuildDate.php +++ b/tests/phpunit/tests/date/getFeedBuildDate.php @@ -57,10 +57,12 @@ public function test_should_not_error_when_modified_times_is_empty() { ) ); - // Use a query where have_posts() is true but wp_list_pluck() - // returns an empty array because the posts array contains scalar - // values (neither objects nor arrays). This triggers the _doing_it_wrong - // notice in WP_List_Util::pluck() and produces an empty result. + /* + * Use a query where have_posts() is true but wp_list_pluck() + * returns an empty array because the posts array contains scalar + * values (neither objects nor arrays). This triggers the _doing_it_wrong + * notice in WP_List_Util::pluck() and produces an empty result. + */ query_posts( array( 'posts__in' => array( $id ), From ec8dfad0d51489b696e79362cb2578a8089176d7 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 25 Apr 2026 01:46:05 +0000 Subject: [PATCH 6/6] Feeds: Resolve scalar post IDs in get_feed_build_date() to WP_Post objects. Replace wp_list_pluck() with array_map() + array_filter() that resolves each entry via get_post() / get_comment(), supporting WP_Query results where $posts contains integer IDs (fields => 'ids') instead of WP_Post objects. Previously, a feed query using fields => 'ids' produced scalar IDs that wp_list_pluck() could not pluck post_modified_gmt from. The function would silently fall through to get_lastpostmodified() and return the site-wide latest modified time instead of the modified time of the posts actually in the feed. Adds a regression test asserting the build date matches the modified time of the post in the feed when fields => 'ids' is used. Follow-up to [60079]. --- src/wp-includes/feed.php | 32 +++++++- tests/phpunit/tests/date/getFeedBuildDate.php | 74 +++++++++++++++---- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/wp-includes/feed.php b/src/wp-includes/feed.php index 890ae6fe07ff5..2d41b62160851 100644 --- a/src/wp-includes/feed.php +++ b/src/wp-includes/feed.php @@ -720,13 +720,37 @@ function get_feed_build_date( $format ) { $utc = new DateTimeZone( 'UTC' ); if ( ! empty( $wp_query ) && $wp_query->have_posts() ) { - // Extract the post modified times from the posts. - $modified_times = wp_list_pluck( $wp_query->posts, 'post_modified_gmt' ); + /* + * Resolve each entry to a WP_Post so we can read post_modified_gmt. + * Supports queries using fields => 'ids' where $posts contains + * integers, as well as the default WP_Post objects. + */ + $modified_times = array_filter( + array_map( + static function ( $post ) { + $post_object = get_post( $post ); + return $post_object instanceof WP_Post ? $post_object->post_modified_gmt : null; + }, + $wp_query->posts + ) + ); // If this is a comment feed, check those objects too. if ( $wp_query->is_comment_feed() && $wp_query->comment_count ) { - // Extract the comment modified times from the comments. - $comment_times = wp_list_pluck( $wp_query->comments, 'comment_date_gmt' ); + /* + * Resolve each entry to a WP_Comment so we can read + * comment_date_gmt. Supports comment queries using + * fields => 'ids' as well as the default WP_Comment objects. + */ + $comment_times = array_filter( + array_map( + static function ( $comment ) { + $comment_object = get_comment( $comment ); + return $comment_object instanceof WP_Comment ? $comment_object->comment_date_gmt : null; + }, + $wp_query->comments + ) + ); // Add the comment times to the post times for comparison. $modified_times = array_merge( $modified_times, $comment_times ); diff --git a/tests/phpunit/tests/date/getFeedBuildDate.php b/tests/phpunit/tests/date/getFeedBuildDate.php index 41a7c9dddda1e..b1351f53cf5fa 100644 --- a/tests/phpunit/tests/date/getFeedBuildDate.php +++ b/tests/phpunit/tests/date/getFeedBuildDate.php @@ -42,35 +42,31 @@ public function test_should_return_correct_feed_build_date() { /** * Test that get_feed_build_date() does not throw a ValueError - * when wp_list_pluck() returns an empty array because the posts - * array contains non-object, non-array values. + * when $wp_query->posts contains no entries that resolve to a + * WP_Post (e.g. invalid IDs that get_post() returns null for). * * @ticket 59956 */ public function test_should_not_error_when_modified_times_is_empty() { + global $wp_query; + $datetime = new DateTimeImmutable( 'now', wp_timezone() ); $datetime_utc = $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); - $id = self::factory()->post->create( + self::factory()->post->create( array( 'post_date' => $datetime->format( 'Y-m-d H:i:s' ), ) ); /* - * Use a query where have_posts() is true but wp_list_pluck() - * returns an empty array because the posts array contains scalar - * values (neither objects nor arrays). This triggers the _doing_it_wrong - * notice in WP_List_Util::pluck() and produces an empty result. + * Build a WP_Query where have_posts() is true but no entry can be + * resolved to a WP_Post. Setting post_count without populating posts + * with valid data exercises the empty $modified_times fallback path. */ - query_posts( - array( - 'posts__in' => array( $id ), - 'fields' => 'ids', - ) - ); - - $this->setExpectedIncorrectUsage( 'WP_List_Util::pluck' ); + $wp_query = new WP_Query(); + $wp_query->post_count = 1; + $wp_query->posts = array( PHP_INT_MAX ); // Non-existent post ID. $result = get_feed_build_date( DATE_RFC3339 ); $this->assertIsString( $result ); @@ -83,6 +79,54 @@ public function test_should_not_error_when_modified_times_is_empty() { ); } + /** + * Test that get_feed_build_date() returns the correct modified time + * when $wp_query->posts is an array of post IDs (from fields => 'ids') + * instead of WP_Post objects. + * + * Before this test, the function would fall back to get_lastpostmodified() + * and silently return the site-wide latest modified time instead of the + * latest modified time of the posts actually in the feed. + * + * @ticket 59956 + */ + public function test_should_return_correct_build_date_for_id_only_query() { + // Create two posts with different modified times. Post B is newer than Post A. + $older_post_id = self::factory()->post->create( + array( + 'post_date' => '2020-01-01 00:00:00', + 'post_date_gmt' => '2020-01-01 00:00:00', + ) + ); + + self::factory()->post->create( + array( + 'post_date' => '2024-06-15 12:00:00', + 'post_date_gmt' => '2024-06-15 12:00:00', + ) + ); + + /* + * Query for ONLY the older post using fields => 'ids'. The feed's + * must reflect the modified time of the older post, + * not the site-wide latest (which would be the newer post that is + * not in the feed). + */ + global $wp_query; + $wp_query = new WP_Query( + array( + 'p' => $older_post_id, + 'fields' => 'ids', + ) + ); + + $this->assertSame( + '2020-01-01T00:00:00+00:00', + get_feed_build_date( DATE_RFC3339 ), + 'Build date should match the modified time of the post in the feed, not the site-wide latest.' + ); + } + /** * Test that get_feed_build_date() works with invalid post dates. *