Skip to content

Commit 8c268c6

Browse files
committed
Prefer published pages in get_page_by_path() when slugs conflict
Added post_status to the SQL SELECT in get_page_by_path() and replaced the first-match-wins loop with a rank-scoring system using $type_rank (requested type vs attachment) and $status_rank (publish vs everything else), so a published page always wins over a draft sharing the same slug. Also fixed $post_type array handling using in_array() instead of ===.
1 parent a49176e commit 8c268c6

File tree

2 files changed

+173
-5
lines changed

2 files changed

+173
-5
lines changed

src/wp-includes/post.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6133,7 +6133,7 @@ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
61336133
$post_types = esc_sql( $post_types );
61346134
$post_type_in_string = "'" . implode( "','", $post_types ) . "'";
61356135
$sql = "
6136-
SELECT ID, post_name, post_parent, post_type
6136+
SELECT ID, post_name, post_parent, post_type, post_status
61376137
FROM $wpdb->posts
61386138
WHERE post_name IN ($in_string)
61396139
AND post_type IN ($post_type_in_string)
@@ -6143,7 +6143,10 @@ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
61436143

61446144
$revparts = array_reverse( $parts );
61456145

6146-
$found_id = 0;
6146+
$found_id = 0;
6147+
$found_type_rank = 0;
6148+
$found_status_rank = 0;
6149+
61476150
foreach ( (array) $pages as $page ) {
61486151
if ( $page->post_name === $revparts[0] ) {
61496152
$count = 0;
@@ -6166,9 +6169,22 @@ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
61666169
&& count( $revparts ) === $count + 1
61676170
&& $p->post_name === $revparts[ $count ]
61686171
) {
6169-
$found_id = $page->ID;
6170-
if ( $page->post_type === $post_type ) {
6171-
break;
6172+
$is_type_match = is_array( $post_type )
6173+
? in_array( $page->post_type, $post_type, true )
6174+
: ( $page->post_type === $post_type );
6175+
6176+
$type_rank = $is_type_match ? 2 : 1;
6177+
$status_rank = ( 'publish' === $page->post_status ) ? 2 : 1;
6178+
6179+
if ( $type_rank > $found_type_rank || ( $type_rank === $found_type_rank && $status_rank > $found_status_rank ) ) {
6180+
$found_id = $page->ID;
6181+
$found_type_rank = $type_rank;
6182+
$found_status_rank = $status_rank;
6183+
6184+
// Perfect match: correct type and viewable status. No need to scan further.
6185+
if ( 2 === $found_type_rank && 2 === $found_status_rank ) {
6186+
break;
6187+
}
61726188
}
61736189
}
61746190
}

tests/phpunit/tests/post/getPageByPath.php

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,158 @@ public function test_cache_should_be_invalidated_when_post_name_is_edited() {
377377
$this->assertSame( $num_queries, get_num_queries() );
378378
}
379379

380+
/**
381+
* @ticket 61996
382+
*
383+
* @covers ::get_page_by_path
384+
*/
385+
public function test_should_prefer_published_page_over_draft_with_same_slug() {
386+
global $wpdb;
387+
388+
// Create a published page first so we can use it as a target.
389+
$published = self::factory()->post->create(
390+
array(
391+
'post_type' => 'page',
392+
'post_name' => 'conflict-slug',
393+
'post_status' => 'publish',
394+
)
395+
);
396+
397+
// Force a draft to share the same post_name (bypass the uniqueness check).
398+
$draft = self::factory()->post->create(
399+
array(
400+
'post_type' => 'page',
401+
'post_name' => 'draft-slug',
402+
'post_status' => 'draft',
403+
)
404+
);
405+
$wpdb->update( $wpdb->posts, array( 'post_name' => 'conflict-slug' ), array( 'ID' => $draft ) );
406+
clean_post_cache( $draft );
407+
408+
$found = get_page_by_path( 'conflict-slug' );
409+
410+
$this->assertSame( $published, $found->ID, 'Published page should be preferred over a draft sharing the same slug.' );
411+
}
412+
413+
/**
414+
* @ticket 61996
415+
*
416+
* @covers ::get_page_by_path
417+
*/
418+
public function test_should_prefer_published_nested_page_over_draft_with_same_path() {
419+
global $wpdb;
420+
421+
// Create the published hierarchy: parent/child.
422+
$published_parent = self::factory()->post->create(
423+
array(
424+
'post_type' => 'page',
425+
'post_name' => 'shared-parent',
426+
'post_status' => 'publish',
427+
)
428+
);
429+
$published_child = self::factory()->post->create(
430+
array(
431+
'post_type' => 'page',
432+
'post_name' => 'shared-child',
433+
'post_status' => 'publish',
434+
'post_parent' => $published_parent,
435+
)
436+
);
437+
438+
// Create draft pages and force them to share the same post_names.
439+
$draft_parent = self::factory()->post->create(
440+
array(
441+
'post_type' => 'page',
442+
'post_name' => 'draft-parent',
443+
'post_status' => 'draft',
444+
)
445+
);
446+
$draft_child = self::factory()->post->create(
447+
array(
448+
'post_type' => 'page',
449+
'post_name' => 'draft-child',
450+
'post_status' => 'draft',
451+
'post_parent' => $draft_parent,
452+
)
453+
);
454+
455+
$wpdb->update( $wpdb->posts, array( 'post_name' => 'shared-parent' ), array( 'ID' => $draft_parent ) );
456+
$wpdb->update( $wpdb->posts, array( 'post_name' => 'shared-child' ), array( 'ID' => $draft_child ) );
457+
clean_post_cache( $draft_parent );
458+
clean_post_cache( $draft_child );
459+
460+
$found = get_page_by_path( 'shared-parent/shared-child' );
461+
462+
$this->assertSame( $published_child, $found->ID, 'Published nested page should be preferred over a draft sharing the same path.' );
463+
}
464+
465+
/**
466+
* @ticket 61996
467+
*
468+
* @covers ::get_page_by_path
469+
*/
470+
public function test_should_return_draft_when_no_viewable_page_exists_with_same_slug() {
471+
global $wpdb;
472+
473+
$draft_a = self::factory()->post->create(
474+
array(
475+
'post_type' => 'page',
476+
'post_name' => 'only-draft-slug',
477+
'post_status' => 'draft',
478+
)
479+
);
480+
481+
// A second draft shares the slug; the first one (lower ID) should be returned
482+
// since neither is viewable and existing fallback behaviour is preserved.
483+
$draft_b = self::factory()->post->create(
484+
array(
485+
'post_type' => 'page',
486+
'post_name' => 'only-draft-slug-b',
487+
'post_status' => 'draft',
488+
)
489+
);
490+
$wpdb->update( $wpdb->posts, array( 'post_name' => 'only-draft-slug' ), array( 'ID' => $draft_b ) );
491+
clean_post_cache( $draft_b );
492+
493+
$found = get_page_by_path( 'only-draft-slug' );
494+
495+
$this->assertNotNull( $found, 'A draft page should still be returned when no viewable page exists.' );
496+
$this->assertContains( $found->ID, array( $draft_a, $draft_b ), 'The returned page should be one of the drafts sharing the slug.' );
497+
}
498+
499+
/**
500+
* @ticket 61996
501+
*
502+
* @covers ::get_page_by_path
503+
*/
504+
public function test_should_prefer_published_page_over_draft_when_post_type_is_array() {
505+
global $wpdb;
506+
507+
register_post_type( 'wptests_pt' );
508+
509+
$published = self::factory()->post->create(
510+
array(
511+
'post_type' => 'wptests_pt',
512+
'post_name' => 'array-type-conflict-slug',
513+
'post_status' => 'publish',
514+
)
515+
);
516+
517+
$draft = self::factory()->post->create(
518+
array(
519+
'post_type' => 'wptests_pt',
520+
'post_name' => 'array-type-draft-slug',
521+
'post_status' => 'draft',
522+
)
523+
);
524+
$wpdb->update( $wpdb->posts, array( 'post_name' => 'array-type-conflict-slug' ), array( 'ID' => $draft ) );
525+
clean_post_cache( $draft );
526+
527+
$found = get_page_by_path( 'array-type-conflict-slug', OBJECT, array( 'wptests_pt' ) );
528+
529+
$this->assertSame( $published, $found->ID, 'Published page should be preferred over a draft when $post_type is passed as an array.' );
530+
}
531+
380532
/**
381533
* @ticket 37611
382534
*/

0 commit comments

Comments
 (0)