diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 525a9efe53eb8..d6f97a845f65e 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -1939,8 +1939,8 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo $where .= " AND p.post_status = 'publish'"; } - $op = $previous ? '<' : '>'; - $order = $previous ? 'DESC' : 'ASC'; + $comparison_operator = $previous ? '<' : '>'; + $order = $previous ? 'DESC' : 'ASC'; /** * Filters the JOIN clause in the SQL for an adjacent post query. @@ -1964,6 +1964,9 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo */ $join = apply_filters( "get_{$adjacent}_post_join", $join, $in_same_term, $excluded_terms, $taxonomy, $post ); + // Prepare the where clause for the adjacent post query. + $where_prepared = $wpdb->prepare( "WHERE (p.post_date $comparison_operator %s OR (p.post_date = %s AND p.ID $comparison_operator %d)) AND p.post_type = %s $where", $current_post_date, $current_post_date, $post->ID, $post->post_type ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $comparison_operator is a string literal, either '<' or '>'. + /** * Filters the WHERE clause in the SQL for an adjacent post query. * @@ -1977,6 +1980,7 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo * * @since 2.5.0 * @since 4.4.0 Added the `$taxonomy` and `$post` parameters. + * @since 6.9.0 Adds ID-based fallback for posts with identical dates in adjacent post queries. * * @param string $where The `WHERE` clause in the SQL. * @param bool $in_same_term Whether post should be in the same taxonomy term. @@ -1984,7 +1988,7 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo * @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true. * @param WP_Post $post WP_Post object. */ - $where = apply_filters( "get_{$adjacent}_post_where", $wpdb->prepare( "WHERE p.post_date $op %s AND p.post_type = %s $where", $current_post_date, $post->post_type ), $in_same_term, $excluded_terms, $taxonomy, $post ); + $where = apply_filters( "get_{$adjacent}_post_where", $where_prepared, $in_same_term, $excluded_terms, $taxonomy, $post ); /** * Filters the ORDER BY clause in the SQL for an adjacent post query. @@ -2000,12 +2004,13 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo * @since 2.5.0 * @since 4.4.0 Added the `$post` parameter. * @since 4.9.0 Added the `$order` parameter. + * @since 6.9.0 Adds ID sort to ensure deterministic ordering for posts with identical dates. * * @param string $order_by The `ORDER BY` clause in the SQL. * @param WP_Post $post WP_Post object. * @param string $order Sort order. 'DESC' for previous post, 'ASC' for next. */ - $sort = apply_filters( "get_{$adjacent}_post_sort", "ORDER BY p.post_date $order LIMIT 1", $post, $order ); + $sort = apply_filters( "get_{$adjacent}_post_sort", "ORDER BY p.post_date $order, p.ID $order LIMIT 1", $post, $order ); $query = "SELECT p.ID FROM $wpdb->posts AS p $join $where $sort"; $key = md5( $query ); diff --git a/tests/phpunit/tests/link/getAdjacentPost.php b/tests/phpunit/tests/link/getAdjacentPost.php index ce948c713ee1a..4d68493bfe8de 100644 --- a/tests/phpunit/tests/link/getAdjacentPost.php +++ b/tests/phpunit/tests/link/getAdjacentPost.php @@ -587,4 +587,196 @@ public function test_get_adjacent_post_cache() { $this->assertEquals( $post_four, get_adjacent_post( true, '', false ), 'Result of function call is wrong after after adding new term' ); $this->assertSame( get_num_queries() - $num_queries, 2, 'Number of queries run was not two after adding new term' ); } + + /** + * Test get_adjacent_post with posts having identical post_date. + * + * @ticket 8107 + */ + public function test_get_adjacent_post_with_identical_dates() { + $identical_date = '2024-01-01 12:00:00'; + + // Create posts with identical dates but different IDs. + $post_ids = array(); + for ( $i = 1; $i <= 5; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Test navigation from the middle post (ID: 3rd post). + $current_post_id = $post_ids[2]; // 3rd post + $this->go_to( get_permalink( $current_post_id ) ); + + // Previous post should be the 2nd post (lower ID, same date). + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertEquals( $post_ids[1], $previous->ID ); + + // Next post should be the 4th post (higher ID, same date). + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertEquals( $post_ids[3], $next->ID ); + } + + /** + * Test get_adjacent_post with mixed dates and identical dates. + * + * @ticket 8107 + */ + public function test_get_adjacent_post_mixed_dates_with_identical_groups() { + // Create posts with different dates. + $post_early = self::factory()->post->create( + array( + 'post_title' => 'Early Post', + 'post_date' => '2024-01-01 10:00:00', + ) + ); + + // Create multiple posts with identical date. + $identical_date = '2024-01-01 12:00:00'; + $post_ids = array(); + for ( $i = 1; $i <= 3; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Identical Post $i", + 'post_date' => $identical_date, + ) + ); + } + + $post_late = self::factory()->post->create( + array( + 'post_title' => 'Late Post', + 'post_date' => '2024-01-01 14:00:00', + ) + ); + + // Test from first identical post. + $this->go_to( get_permalink( $post_ids[0] ) ); + + // Previous should be the early post (different date). + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertEquals( $post_early, $previous->ID ); + + // Next should be the second identical post (same date, higher ID). + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertEquals( $post_ids[1], $next->ID ); + + // Test from middle identical post. + $this->go_to( get_permalink( $post_ids[1] ) ); + + // Previous should be the first identical post (same date, lower ID). + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertEquals( $post_ids[0], $previous->ID ); + + // Next should be the third identical post (same date, higher ID). + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertEquals( $post_ids[2], $next->ID ); + + // Test from last identical post. + $this->go_to( get_permalink( $post_ids[2] ) ); + + // Previous should be the second identical post (same date, lower ID). + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertEquals( $post_ids[1], $previous->ID ); + + // Next should be the late post (different date). + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertEquals( $post_late, $next->ID ); + } + + /** + * Test get_adjacent_post navigation through all posts with identical dates. + * + * @ticket 8107 + */ + public function test_get_adjacent_post_navigation_through_identical_dates() { + $identical_date = '2024-01-01 12:00:00'; + + // Create 4 posts with identical dates. + $post_ids = array(); + for ( $i = 1; $i <= 4; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Test navigation sequence: 1 -> 2 -> 3 -> 4. + $this->go_to( get_permalink( $post_ids[0] ) ); + + // From post 1, next should be post 2. + $next = get_adjacent_post( false, '', false ); + $this->assertEquals( $post_ids[1], $next->ID ); + + // From post 2, previous should be post 1, next should be post 3. + $this->go_to( get_permalink( $post_ids[1] ) ); + $previous = get_adjacent_post( false, '', true ); + $this->assertEquals( $post_ids[0], $previous->ID ); + $next = get_adjacent_post( false, '', false ); + $this->assertEquals( $post_ids[2], $next->ID ); + + // From post 3, previous should be post 2, next should be post 4. + $this->go_to( get_permalink( $post_ids[2] ) ); + $previous = get_adjacent_post( false, '', true ); + $this->assertEquals( $post_ids[1], $previous->ID ); + $next = get_adjacent_post( false, '', false ); + $this->assertEquals( $post_ids[3], $next->ID ); + + // From post 4, previous should be post 3. + $this->go_to( get_permalink( $post_ids[3] ) ); + $previous = get_adjacent_post( false, '', true ); + $this->assertEquals( $post_ids[2], $previous->ID ); + } + + /** + * Test get_adjacent_post with identical dates and category filtering. + * + * @ticket 8107 + */ + public function test_get_adjacent_post_identical_dates_with_category() { + $identical_date = '2024-01-01 12:00:00'; + $category_id = self::factory()->category->create( array( 'name' => 'Test Category' ) ); + + // Create posts with identical dates, some in category. + $post_ids = array(); + for ( $i = 1; $i <= 4; $i++ ) { + $post_id = self::factory()->post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + + // Add every other post to the category. + if ( 0 === $i % 2 ) { + wp_set_post_categories( $post_id, array( $category_id ) ); + } + + $post_ids[] = $post_id; + } + + // Test from post 2 (in category). + $this->go_to( get_permalink( $post_ids[1] ) ); + + // With category filtering, should only see posts in same category. + $previous = get_adjacent_post( true, '', true, 'category' ); + $this->assertSame( '', $previous ); // No previous post in category + + $next = get_adjacent_post( true, '', false, 'category' ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertEquals( $post_ids[3], $next->ID ); // Post 4 (in category) + } } diff --git a/tests/phpunit/tests/url.php b/tests/phpunit/tests/url.php index 37ceb69cba192..3734891eb19ac 100644 --- a/tests/phpunit/tests/url.php +++ b/tests/phpunit/tests/url.php @@ -569,4 +569,62 @@ public function test_url_functions_for_dots_in_paths() { ); } } + + /** + * Test get_adjacent_post with posts having identical post_date. + * + * @ticket 8107 + * @covers ::get_adjacent_post + */ + public function test_get_adjacent_post_with_identical_dates() { + $identical_date = gmdate( 'Y-m-d H:i:s', time() ); + + // Create 3 posts with identical dates but different IDs. + $post_ids = array(); + for ( $i = 1; $i <= 3; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Identical Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Test from the middle post (2nd post). + $GLOBALS['post'] = get_post( $post_ids[1] ); + + // Previous post should be the 1st post (lower ID, same date). + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertSame( $post_ids[0], $previous->ID ); + + // Next post should be the 3rd post (higher ID, same date). + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertSame( $post_ids[2], $next->ID ); + + // Test from the first post. + $GLOBALS['post'] = get_post( $post_ids[0] ); + + // Previous should be empty (no earlier posts). + $previous = get_adjacent_post( false, '', true ); + $this->assertSame( '', $previous ); + + // Next should be the 2nd post. + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertSame( $post_ids[1], $next->ID ); + + // Test from the last post. + $GLOBALS['post'] = get_post( $post_ids[2] ); + + // Previous should be the 2nd post. + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertSame( $post_ids[1], $previous->ID ); + + // Next should be empty (no later posts). + $next = get_adjacent_post( false, '', false ); + $this->assertSame( '', $next ); + } }