diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index b225d35c48b2a..fff14b7b4a0a4 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -6222,7 +6222,7 @@ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) { $post_types = esc_sql( $post_types ); $post_type_in_string = "'" . implode( "','", $post_types ) . "'"; $sql = " - SELECT ID, post_name, post_parent, post_type + SELECT ID, post_name, post_parent, post_type, post_status FROM $wpdb->posts WHERE post_name IN ($in_string) AND post_type IN ($post_type_in_string) @@ -6232,7 +6232,10 @@ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) { $revparts = array_reverse( $parts ); - $found_id = 0; + $found_id = 0; + $found_type_rank = 0; + $found_status_rank = 0; + foreach ( (array) $pages as $page ) { if ( $page->post_name === $revparts[0] ) { $count = 0; @@ -6255,9 +6258,22 @@ function get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) { && count( $revparts ) === $count + 1 && $p->post_name === $revparts[ $count ] ) { - $found_id = $page->ID; - if ( $page->post_type === $post_type ) { - break; + $is_type_match = is_array( $post_type ) + ? in_array( $page->post_type, $post_type, true ) + : ( $page->post_type === $post_type ); + + $type_rank = $is_type_match ? 2 : 1; + $status_rank = ( 'publish' === $page->post_status ) ? 2 : 1; + + if ( $type_rank > $found_type_rank || ( $type_rank === $found_type_rank && $status_rank > $found_status_rank ) ) { + $found_id = $page->ID; + $found_type_rank = $type_rank; + $found_status_rank = $status_rank; + + // Perfect match: correct type and viewable status. No need to scan further. + if ( 2 === $found_type_rank && 2 === $found_status_rank ) { + break; + } } } } diff --git a/tests/phpunit/tests/post/getPageByPath.php b/tests/phpunit/tests/post/getPageByPath.php index 8db0274249bed..5668e41905440 100644 --- a/tests/phpunit/tests/post/getPageByPath.php +++ b/tests/phpunit/tests/post/getPageByPath.php @@ -377,6 +377,158 @@ public function test_cache_should_be_invalidated_when_post_name_is_edited() { $this->assertSame( $num_queries, get_num_queries() ); } + /** + * @ticket 61996 + * + * @covers ::get_page_by_path + */ + public function test_should_prefer_published_page_over_draft_with_same_slug() { + global $wpdb; + + // Create a published page first so we can use it as a target. + $published = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'conflict-slug', + 'post_status' => 'publish', + ) + ); + + // Force a draft to share the same post_name (bypass the uniqueness check). + $draft = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'draft-slug', + 'post_status' => 'draft', + ) + ); + $wpdb->update( $wpdb->posts, array( 'post_name' => 'conflict-slug' ), array( 'ID' => $draft ) ); + clean_post_cache( $draft ); + + $found = get_page_by_path( 'conflict-slug' ); + + $this->assertSame( $published, $found->ID, 'Published page should be preferred over a draft sharing the same slug.' ); + } + + /** + * @ticket 61996 + * + * @covers ::get_page_by_path + */ + public function test_should_prefer_published_nested_page_over_draft_with_same_path() { + global $wpdb; + + // Create the published hierarchy: parent/child. + $published_parent = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'shared-parent', + 'post_status' => 'publish', + ) + ); + $published_child = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'shared-child', + 'post_status' => 'publish', + 'post_parent' => $published_parent, + ) + ); + + // Create draft pages and force them to share the same post_names. + $draft_parent = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'draft-parent', + 'post_status' => 'draft', + ) + ); + $draft_child = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'draft-child', + 'post_status' => 'draft', + 'post_parent' => $draft_parent, + ) + ); + + $wpdb->update( $wpdb->posts, array( 'post_name' => 'shared-parent' ), array( 'ID' => $draft_parent ) ); + $wpdb->update( $wpdb->posts, array( 'post_name' => 'shared-child' ), array( 'ID' => $draft_child ) ); + clean_post_cache( $draft_parent ); + clean_post_cache( $draft_child ); + + $found = get_page_by_path( 'shared-parent/shared-child' ); + + $this->assertSame( $published_child, $found->ID, 'Published nested page should be preferred over a draft sharing the same path.' ); + } + + /** + * @ticket 61996 + * + * @covers ::get_page_by_path + */ + public function test_should_return_draft_when_no_viewable_page_exists_with_same_slug() { + global $wpdb; + + $draft_a = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'only-draft-slug', + 'post_status' => 'draft', + ) + ); + + // A second draft shares the slug; the first one (lower ID) should be returned + // since neither is viewable and existing fallback behaviour is preserved. + $draft_b = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'only-draft-slug-b', + 'post_status' => 'draft', + ) + ); + $wpdb->update( $wpdb->posts, array( 'post_name' => 'only-draft-slug' ), array( 'ID' => $draft_b ) ); + clean_post_cache( $draft_b ); + + $found = get_page_by_path( 'only-draft-slug' ); + + $this->assertNotNull( $found, 'A draft page should still be returned when no viewable page exists.' ); + $this->assertContains( $found->ID, array( $draft_a, $draft_b ), 'The returned page should be one of the drafts sharing the slug.' ); + } + + /** + * @ticket 61996 + * + * @covers ::get_page_by_path + */ + public function test_should_prefer_published_page_over_draft_when_post_type_is_array() { + global $wpdb; + + register_post_type( 'wptests_pt' ); + + $published = self::factory()->post->create( + array( + 'post_type' => 'wptests_pt', + 'post_name' => 'array-type-conflict-slug', + 'post_status' => 'publish', + ) + ); + + $draft = self::factory()->post->create( + array( + 'post_type' => 'wptests_pt', + 'post_name' => 'array-type-draft-slug', + 'post_status' => 'draft', + ) + ); + $wpdb->update( $wpdb->posts, array( 'post_name' => 'array-type-conflict-slug' ), array( 'ID' => $draft ) ); + clean_post_cache( $draft ); + + $found = get_page_by_path( 'array-type-conflict-slug', OBJECT, array( 'wptests_pt' ) ); + + $this->assertSame( $published, $found->ID, 'Published page should be preferred over a draft when $post_type is passed as an array.' ); + } + /** * @ticket 37611 */