Make WordPress Core

Opened 15 months ago

Last modified 3 months ago

#61996 new defect (bug)

Slug Conflict When a Published and Draft Page Share post_name

Reported by: brookedot's profile brookedot Owned by:
Milestone: Future Release Priority: normal
Severity: normal Version:
Component: Posts, Post Types Keywords: good-first-bug has-test-info has-patch
Focuses: Cc:

Description

It is possible (although it takes some work) to have a published page and a draft page that shares the same post_name. When this occurs if the draft page has a lower ID then it is retrieved by get_page_by_path in place of the published page. This is due at least in part to get_page_by_path not checking the page status and .

  1. Ensure permalinks are anything other than Plain
  2. Publish three nested pages, the first page here is needed to avoid a slug conflict when you later create pages with the same name. For example, to prevent parent from becoming parent-2. The posts must be published so they can be selected as parent pages. Consider setting the page content to "this is draft" so it's easy to tell these apart later.
  • Placeholder (slug placeholder)
    • Parent (slug /placehoder/parent
      • Child ( slug placeholder/parent/child
  1. Using the Bulk Action, set all three newly created published posts to draft. There may be other ways to do this, as well.
  1. Publish two addition new pages with the same slugs as parent and child
  • Parent ( slug parent )
    • Child (slug parent/child)
  1. Using Quick Edit remove the parent from the draft page parent ( /placeholder/parent) by setting its parent page to `Main (no parent) keeping it as a draft.

You should now have parent and parent/child as drafts alongside your other two published pages.

  1. Ensure you don't have a -2 in either parent-2 or child-2
  1. If all goes well, when you visit parent/child you will now see a 404 instead of the pages you published in step 4.

There is a screen recording of these steps here:
https://cloudup.com/cxkwjscxDfB

They can be replicated in Multisite, Single site, and WordPress playground.

Some insight into what is happening is when you return the array used to get_page_by_path. For WordPress 6.6.1 that is https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/post.php#L5881

Using var_dump to return the array:

<?php
        $sql                 = "
                SELECT ID, post_name, post_parent, post_type
                FROM $wpdb->posts
                WHERE post_name IN ($in_string)
                AND post_type IN ($post_type_in_string)
        ";

        $pages = $wpdb->get_results( $sql, ARRAY_A );

        $revparts = array_reverse( $parts );
        var_dump( $pages );

This returns post ID 63 first which is our draft child page thus a 404 on the front end. ID 69 is the published child page which is never displayed on the front end as ID 63 is returned first.

Here is the output of the above var_dump()

<?php
array (size=4)
  0 => 
    array (size=4)
      'ID' => string '63' (length=2)
      'post_name' => string 'child' (length=5)
      'post_parent' => string '61' (length=2)
      'post_type' => string 'page' (length=4)
  1 => 
    array (size=4)
      'ID' => string '69' (length=2)
      'post_name' => string 'child' (length=5)
      'post_parent' => string '65' (length=2)
      'post_type' => string 'page' (length=4)
  2 => 
    array (size=4)
      'ID' => string '61' (length=2)
      'post_name' => string 'parent' (length=6)
      'post_parent' => string '0' (length=1)
      'post_type' => string 'page' (length=4)
  3 => 
    array (size=4)
      'ID' => string '65' (length=2)
      'post_name' => string 'parent' (length=6)
      'post_parent' => string '0' (length=1)
      'post_type' => string 'page' (length=4)

A few other related trac issues were found but nothing that matched this exactly. Please update the Component to match the closest, I almost set this as permalinks

Related to #13459 (but not a duplicate as this is the same post type)

Props @trepmal who was instrumental in debugging and reporting this bug.

Change History (2)

#1 @SirLouen
3 months ago

  • Keywords needs-patch good-first-bug has-test-info added
  • Milestone changed from Awaiting Review to Future Release

Reproduction Report

Description

✅ This report validates that the issue can be reproduced.

Environment

  • WordPress: 6.9-alpha-60093-src
  • PHP: 8.2.29
  • Server: nginx/1.29.1
  • Database: mysqli (Server: 8.4.6 / Client: mysqlnd 8.2.29)
  • Browser: Chrome 139.0.0.0
  • OS: Windows 10/11
  • Theme: Twenty Twenty-Five 1.3
  • MU Plugins: None activated
  • Plugins:
    • Test Reports 1.2.0

Test Instructions

  • Used the ones provided by the OP.
  • 🐞 Both child pages are linked as the same page following such procedure.

Actual Results

  1. ✅ Error condition occurs (reproduced).

Additional Notes

  • I'm not going to add a video because the one provided in the OP works as expected. I did a little twist to the instructions as follows with the same result:
  1. Create the Placeholder > First Parent > First Child pages
  2. Create the Second Parent > Second Child page
  3. Set to draft First Parent and Child Pages
  4. Set First parent's parent to Main instead of Placeholder
  5. We can see that both Second And First Child page point to the First Child Page
  • It seems that the problem comes the moment we set the First Parent to root, leaving the link to Placeholder. After this, both posts share the same level hierarchy and the same post_name which causes a collision. Same happens for the First Parent and Second parent as they also share the same post_name. When changing the hierarchy, the system is not checking for collisions in post_name hence adding the corresponding -2, -3 like when we create a new post with the exact same slug.

This ticket was mentioned in PR #9675 on WordPress/wordpress-develop by @sahilgidwani.


3 months ago
#2

  • Keywords has-patch added; needs-patch removed

This PR updates get_page_by_path() to exclude non-public post statuses (such as draft, pending, and trash) from path resolution. Previously, unpublished pages sharing a slug with a published one could incorrectly hijack URL resolution and return the wrong post.

### Changes Made

  • Added filtering by valid post_status using get_post_stati( array( 'public' => true, 'private' => true ) ).
  • Ensured only published and intentionally viewable posts are considered when resolving paths.
  • Prevented draft or pending posts from conflicting with live URLs.
Note: See TracTickets for help on using tickets.