Make WordPress Core

Changeset 63915


Ignore:
Timestamp:
09/24/2026 12:26:10 PM (5 hours ago)
Author:
zieladam
Message:

Query: Prefer published pages in path lookups.

A draft and a published page can share the same path. With no SQL result order, get_page_by_path() could return the draft and cause a 404 for the published page.

Order candidates with publish first, other statuses next, and draft, pending, and auto-draft last. Use the lowest ID to break ties within each group.

When $post_type is an array, return the first full-path match instead of the last so the SQL order is respected. Keep the existing preference for the requested post type over attachments when $post_type is a string.

Add regression tests for string and array post type arguments.

Developed in: https://github.com/WordPress/wordpress-develop/pull/13655

Props brookedot, trepmal, SirLouen, sahilgidwani, rdelbem, vijendrajat, jonsurrell.
Fixes #61996.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/post.php

    r63909 r63915  
    63926392                WHERE post_name IN ($in_string)
    63936393                AND post_type IN ($post_type_in_string)
     6394                ORDER BY
     6395                        post_status = 'publish' DESC,
     6396                        post_status IN ('draft', 'pending', 'auto-draft') ASC, ID ASC
    63946397        ";
    63956398
     
    64236426                        ) {
    64246427                                $found_id = $page->ID;
    6425                                 if ( $page->post_type === $post_type ) {
     6428
     6429                                /*
     6430                                 * A string like 'page' also searches attachments: /about/photo/ could be
     6431                                 * a child page or an attachment page, and this lookup handles both.
     6432                                 * Keep an attachment as a fallback, but keep looking for the requested
     6433                                 * type so an attachment cannot hide a page with the same path.
     6434                                 *
     6435                                 * An array is the exact list of types to search; no extra types are added.
     6436                                 * SQL already checks that list, so stop at the first full-path match.
     6437                                 */
     6438                                if ( is_array( $post_type ) || $page->post_type === $post_type ) {
    64266439                                        break;
    64276440                                }
  • trunk/tests/phpunit/tests/post/getPageByPath.php

    r56549 r63915  
    5656
    5757                $this->assertSame( $page, $found->ID );
     58        }
     59
     60        /**
     61         * @ticket 61996
     62         * @covers ::get_page_by_path
     63         *
     64         * @dataProvider data_page_post_types
     65         *
     66         * @param string|string[] $post_type Post type argument.
     67         */
     68        public function test_should_prefer_published_page_then_lowest_id( $post_type ) {
     69                // Setting a pending page's slug requires publish permission.
     70                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
     71
     72                $draft     = self::factory()->post->create(
     73                        array(
     74                                'post_type'   => 'page',
     75                                'post_status' => 'draft',
     76                        )
     77                );
     78                $pending   = self::factory()->post->create(
     79                        array(
     80                                'post_type'   => 'page',
     81                                'post_status' => 'pending',
     82                        )
     83                );
     84                $published = self::factory()->post->create(
     85                        array(
     86                                'post_type'   => 'page',
     87                                'post_status' => 'publish',
     88                                'post_name'   => 'privacy-policy',
     89                        )
     90                );
     91
     92                // Draft and pending slug updates can reuse a published page's slug.
     93                foreach ( array( $draft, $pending ) as $post_id ) {
     94                        wp_update_post(
     95                                array(
     96                                        'ID'        => $post_id,
     97                                        'post_name' => 'privacy-policy',
     98                                )
     99                        );
     100                        $this->assertSame( 'privacy-policy', get_post( $post_id )->post_name );
     101                }
     102
     103                $this->assertSame( $published, get_page_by_path( 'privacy-policy', OBJECT, $post_type )->ID );
     104
     105                // Draft and pending pages have the same priority, so the lowest ID wins.
     106                wp_delete_post( $published, true );
     107                $this->assertSame( $draft, get_page_by_path( 'privacy-policy', OBJECT, $post_type )->ID );
     108
     109                wp_delete_post( $draft, true );
     110                $this->assertSame( $pending, get_page_by_path( 'privacy-policy', OBJECT, $post_type )->ID );
     111        }
     112
     113        /**
     114         * Data provider.
     115         *
     116         * @return array<string, array<string|string[]>> {
     117         */
     118        public function data_page_post_types(): array {
     119                return array(
     120                        'string'             => array( 'page' ),
     121                        'array of one type'  => array( array( 'page' ) ),
     122                        'array of two types' => array( array( 'page', 'post' ) ),
     123                );
    58124        }
    59125
Note: See TracChangeset for help on using the changeset viewer.