Make WordPress Core

Opened 6 months ago

Last modified 4 weeks ago

#64670 new defect (bug)

WP_REST_Posts_Controller::update_item() not passing parent to wp_unique_post_slug() for draft child

Reported by: esaner Owned by:
Priority: normal Milestone: Awaiting Review
Component: REST API Version: 6.9.1
Severity: normal Keywords: has-patch needs-testing
Cc: Focuses: ui, administration, rest-api

Description

Editing the slug of a draft child page (or any hierarchical post type) in the block editor excludes the parent slug when determining slug interference, despite the fact that the parent slug is shown in the child's permalink, just below the slug setting input.

I believe the slug check occurs with wp_unique_post_slug() in WP_REST_Posts_Controller::update_item() of wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php:967.

Environment

  • WordPress 6.9.1
  • Theme: Twenty Twenty-Five 1.4
  • Plugins: none
  • PHP 8.4.14
  • nginx 1.29.3
  • mysql 9.5.0
  • macOS 15.7.3

Steps to Reproduce

Permalink structure: Post name

Steps 1–3 below can be done in multiple ways (block editor, wp-cli, etc.).

  1. Publish a page (First Page), example slug: /slug
  2. Publish another page (Parent Page), example slug: /parent
  3. Create another draft page (Child Page), set its parent to Parent Page, and save the draft
  4. In the block editor, edit the Child Page slug to /slug and save the draft; the slug will be changed to slug-2, even though the permalink just below the slug input shows /parent/slug and /parent/slug does not interfere with First Page /slug

Additional Notes:

  • If Child Page is published, its slug can be edited to /parent/slug in the block editor.
  • In draft status, Child Page can have its slug edited to /parent/slug using quick edit in admin post list.

Expected Outcomes

  • Slug editing behavior for parent/child relationships should be the same in the block editor and admin post list quick edit.
  • If the block editor slug setting shows a parent slug incorporated into a child's permalink, the parent slug should also be used to determine the child slug's interference (regardless of the child's status).

Possible Solutions

Before wp_unique_post_slug() is called in class-wp-rest-posts-controller.php:967, the following defines $post and $post_parent.

$post        = $this->prepare_item_for_database( $request );
...
$post_parent = ! empty( $post->post_parent ) ? $post->post_parent : 0;

For a draft page, $post->post_parent comes through as null, so it's set to 0;

$post_before, defined a few lines earlier at class-wp-rest-posts-controller.php:947, does contain the parent and could possibly be used to define $post_parent similar to $post_status at line 954. For example:

if ( ! empty( $post->post_parent ) ) {
    $post_parent = $post->post_parent;
} else {
    $post_parent = $post_before->post_parent ? $post_before->post_parent : 0;
}

Related Tickets

In #47988 @paulbonneau noted how draft and pending statuses are changed to publish for wp_unique_post_slug() calls in WP_REST_Posts_Controller:create_item() and WP_REST_Posts_Controller::update_item(). That seems unusual and suggests room for improvement, though any changes to wp_unique_post_slug() would need careful consideration.

I share @paulbonneau's interest in wondering, "Why shouldn't we assign a post_name to a post with a draft post_status?".

#47988 also references several other tickets related to draft status and wp_unique_post_slug(), such as #47552 and #61996.

Attachments (1)

Slug editing behaviour.png (43.1 KB ) - added by sanayasir 4 weeks ago.

Download all attachments as: .zip

Change History (5)

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


6 months ago
#1

  • Keywords has-patch added

#2 @maulikmakwana2008
6 months ago

  • Component PermalinksREST API
  • Keywords needs-testing added

I’ve prepared a fix that preserves the existing parent ID when updating draft hierarchical posts via the REST API.

Currently, when post_parent is not included in the REST request (as in the block editor for draft child pages), wp_unique_post_slug() is called with a parent ID of 0, causing false slug conflicts with top-level posts.

The patch falls back to $post_before->post_parent when $post->post_parent is empty, ensuring slug uniqueness is evaluated using the correct parent. This aligns block editor behavior with Quick Edit and classic update flows.

#3 @ekamran
3 months ago

Test Report

Patch tested: https://github.com/WordPress/wordpress-develop/pull/10973

Environment

  • Reported version: 6.9.1
  • Tested version: 7.1-alpha-62161-src
  • PHP: 8.3.31
  • Environment: wordpress-develop local Docker environment
  • Test method: WP-CLI automated reproduction using rest_do_request()
  • OS: macOS

Reproduction

I was able to confirm that the reported issue is still reproducible on current trunk.

The test setup:

  • Set up a published top-level page with slug slug.
  • Set up a published parent page with slug parent.
  • Set up a draft child page under the parent.
  • Sent a REST API update request to the draft child page with slug=slug, without sending a parent value.

Clean trunk result:

{
    "first_id": 12,
    "parent_id": 13,
    "child_id": 14,
    "response_status": 200,
    "response_slug": "slug-2",
    "stored_child_slug": "slug-2",
    "stored_child_parent": 13,
    "child_permalink": "http://localhost:8889/?page_id=14",
    "top_slug_permalink": "http://localhost:8889/slug/"
}

This confirms the bug: the draft child page remains under parent 13, but the REST update changes the requested slug from slug to slug-2, because the uniqueness check behaves as if the parent were 0.

Patch Result

After applying PR #10973 locally and running the same automated reproduction, the draft child page keeps the requested slug slug while preserving its parent:

{
    "first_id": 16,
    "parent_id": 17,
    "child_id": 18,
    "response_status": 200,
    "response_slug": "slug",
    "stored_child_slug": "slug",
    "stored_child_parent": 17,
    "child_permalink": "http://localhost:8889/?page_id=18",
    "top_slug_permalink": "http://localhost:8889/slug/"
}

Tests

I also ran the related REST pages controller PHPUnit tests with the patch applied:

npm run test:php -- --filter WP_Test_REST_Pages_Controller

OK (34 tests, 142 assertions)

Notes

The patch fixes the REST API draft child page slug behavior by falling back to the existing post parent when the REST update request does not include parent.

#4 @sanayasir
4 weeks ago

Tested successfully and was able to reproduce the reported issue.

Environment:

  • WordPress trunk
  • Twenty Twenty-Five theme
  • No active plugins
  • Permalink structure: Post name
  • PHP 8.4.x

Results:
Following the reproduction steps, I created a published page with the slug slug, a published parent page with the slug parent, and a draft child page assigned to the parent. When editing the draft child page's slug to slug in the Block Editor and saving the draft, WordPress automatically changed the slug to slug-2, even though the displayed permalink (/parent/slug) does not conflict with the existing page (/slug).

I also verified that the same slug can be saved successfully using Quick Edit for the draft page, and that the Block Editor allows the slug after the child page is published. This confirms the inconsistency between the Block Editor and Quick Edit when editing the slug of a draft child page.

Note: See TracTickets for help on using tickets.