Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/wp-includes/link-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
*
Expand All @@ -1977,14 +1980,15 @@ 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.
* @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided.
* @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.
Expand All @@ -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 );
Expand Down
192 changes: 192 additions & 0 deletions tests/phpunit/tests/link/getAdjacentPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
58 changes: 58 additions & 0 deletions tests/phpunit/tests/url.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
Loading