Make WordPress Core

Opened 6 years ago

Last modified 7 weeks ago

#50002 new defect (bug)

Hack in get_sample_permalink() can cause filters like post_link, and pre_post_link to have inaccurate post_status in admin

Reported by: aubreypwd Owned by:
Priority: normal Milestone: Awaiting Review
Component: Permalinks Version: 5.4
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses: administration

Description

For context: ​https://github.com/aubreypwd/WordPress/issues/5

When editing a post (that is a draft) in the admin, add a filter to post_link you'll notice that the pass for get_sample_permalink() shows that $post->post_status is set incorrectly (un-truthfully) to publish.

This is because of a 12 year old block of code in get_sample_permalink():

// Hack: get_permalink() would return ugly permalink for drafts, so we will fake that our post is published.
if ( in_array( $post->post_status, array( 'draft', 'pending', 'future' ) ) ) {
	$post->post_status = 'publish';
	$post->post_name   = sanitize_title( $post->post_name ? $post->post_name : $post->post_title, $post->ID );
}

In order for post_link and even pre_post_link filters to pass accurate info about a post, the patch I have attached is my first attempt at ensuring get_sample_permalink() still gets the same result, while not affecting post_status. Obviously I tested this, but I still feel some more testing will need to be done.

Attachments (1)

50002.patch​ (1.6 KB ) - added by aubreypwd 6 years ago.
First pass at fixing this that does not modify post_status but does not affect the desired result (please test!).

Download all attachments as: .zip

Change History (6)

@aubreypwd
6 years ago

First pass at fixing this that does not modify post_status but does not affect the desired result (please test!).

#1 @SergeyBiryukov
6 years ago

  • Component General → Posts, Post Types
  • Keywords has-patch needs-testing needs-unit-tests added

#2 @SirLouen
13 months ago

  • Component Posts, Post Types → Permalinks

This ticket was mentioned in ​PR #12577 on ​WordPress/wordpress-develop by ​@arkaprabhachowdhury.


3 months ago
#3

  • Keywords has-unit-tests added; needs-unit-tests removed

get_sample_permalink() currently changes draft, pending, future, and auto-draft posts to publish before generating the permalink. This causes the pre_post_link and post_link filters to receive an inaccurate post status.

This changes the function to use a separate published status only when determining a unique sample slug, while keeping the WP_Post status unchanged. It also preserves pretty sample permalinks for auto-drafts through wp_force_plain_post_permalink().

The regression test verifies that both permalink filters receive the original draft status and that the generated sample permalink remains unchanged.

## Testing

  • npm run test:php -- --filter=Tests_Admin_IncludesPost
  • php -d xdebug.mode=off vendor/bin/phpcbf src/wp-admin/includes/post.php src/wp-includes/link-template.php tests/phpunit/tests/admin/includesPost.php
  • php -d xdebug.mode=off vendor/bin/phpcs --warning-severity=0 src/wp-admin/includes/post.php src/wp-includes/link-template.php tests/phpunit/tests/admin/includesPost.php

## Use of AI Tools

AI assistance: Yes
Tool(s): Codex
Model(s): GPT-5
Used for: Investigating the current code path, implementing the fix and regression test, and running validation. The final changes were reviewed and verified against the focused test suite.

#4 @arkaprabhachowdhury
7 weeks ago

Opened [PR #12577](​https://github.com/WordPress/wordpress-develop/pull/12577) to address this ticket. get_sample_permalink() currently changes draft, pending, future, and auto-draft posts to publish before generating the permalink. This causes the pre_post_link and post_link filters to receive an inaccurate post status. This changes the function to use a separate published status only when determining a unique sample slug, while keeping the WP_Post status unchanged. It also preserves pretty sample permalinks for auto-drafts through wp_force_plain_post_permalink(). The regression test The PR includes focused regression coverage, and its validation details are recorded in the PR description.

I picked this up because the ticket describes a concrete core behavior or edge case that remains relevant in current WordPress. The proposed change is intentionally scoped to the ticket and is submitted here for code review, with the technical discussion remaining on this ticket.

#5 @sajib1223
7 weeks ago

  • Keywords needs-testing removed

Patch Testing Report

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

Environment

  • WordPress: trunk (7.2-alpha-63166-src)
  • PHP: 8.2.29
  • Server: nginx 1.29.5
  • Database: MySQL 8.4.8
  • Browser: n/a (WP-CLI)
  • OS: Ubuntu (WSL2)
  • Theme: Twenty Twenty-Five 1.5
  • MU Plugins:
  • Plugins:
    • None activated
  • Permalink structure: /%year%/%monthnum%/%postname%/

Steps taken

Reproduction on clean trunk

  1. Hooked pre_post_link, post_link and page_link filters that record $post->post_status as seen by the filter.
  2. Created posts in draft, pending, future, auto-draft and publish status, plus a draft child page under a published parent page.
  3. Called get_sample_permalink() for each one via WP-CLI (repro-50002.php below) and printed the status seen by the filters, the sample permalink and the editable slug.
  4. Bug reproduced on clean trunk: every non-published post is reported to pre_post_link / post_link as publish.
    draft post        status seen by filters: {"pre_post_link":"publish","post_link":"publish"} sample: http://localhost:8889/2026/08/%postname%/ | a-draft-post-50002
    pending post      status seen by filters: {"pre_post_link":"publish","post_link":"publish"} sample: http://localhost:8889/2026/08/%postname%/ | a-pending-post-50002
    future post       status seen by filters: {"pre_post_link":"publish","post_link":"publish"} sample: http://localhost:8889/2026/08/%postname%/ | a-future-post-50002
    auto-draft post   status seen by filters: {"pre_post_link":"publish","post_link":"publish"} sample: http://localhost:8889/2026/08/%postname%/ | auto-draft-50002
    draft child page  status seen by filters: {"page_link":"draft"}                    sample: http://localhost:8889/parent-page/%pagename%/ | child-page-50002
    published post    status seen by filters: {"pre_post_link":"publish","post_link":"publish"} sample: http://localhost:8889/2026/08/%postname%/ | a-published-post-50002
    

With patch applied

  1. Applied PR #12577 on trunk.
  2. Ran the same WP-CLI script as in steps 1–3.
  3. Compared the sample permalink and editable slug for every case against the clean-trunk output.
  4. Reviewed get_permalink(), get_post_permalink() and _get_page_link() for other post_status-dependent branches.
  • 8a. Checked the other caller, the REST API (block editor path): requested a draft post with context=edit via rest_do_request() and compared permalink_template / generated_slug and the status seen by post_link (repro-50002-rest.php below).
    clean trunk: permalink_template: http://localhost:8889/2026/08/%postname%/ | generated_slug: rest-draft-50002 | post_link saw: draft,publish
    patched:     permalink_template: http://localhost:8889/2026/08/%postname%/ | generated_slug: rest-draft-50002 | post_link saw: draft
    
  1. Ran the related PHPUnit tests and PHPCS.
  2. ✅ Patch is solving the problem: the filters receive the real status and the generated sample permalinks are unchanged.
    draft post        status seen by filters: {"pre_post_link":"draft","post_link":"draft"} sample: http://localhost:8889/2026/08/%postname%/ | a-draft-post-50002
    pending post      status seen by filters: {"pre_post_link":"pending","post_link":"pending"} sample: http://localhost:8889/2026/08/%postname%/ | a-pending-post-50002
    future post       status seen by filters: {"pre_post_link":"future","post_link":"future"} sample: http://localhost:8889/2026/08/%postname%/ | a-future-post-50002
    auto-draft post   status seen by filters: {"pre_post_link":"auto-draft","post_link":"auto-draft"} sample: http://localhost:8889/2026/08/%postname%/ | auto-draft-50002
    draft child page  status seen by filters: {"page_link":"draft"}                    sample: http://localhost:8889/parent-page/%pagename%/ | child-page-50002
    published post    status seen by filters: {"pre_post_link":"publish","post_link":"publish"} sample: http://localhost:8889/2026/08/%postname%/ | a-published-post-50002
    

Expected result

  • Permalink filters receive the original post_status, and get_sample_permalink() returns exactly the same permalink template and slug as before for every status. Both confirmed.

Additional Notes

  • The only post_status-dependent branch on the permalink path is wp_force_plain_post_permalink(). draft, pending and future are protected statuses, so they already get a pretty sample link without the hack; auto-draft is not, which is why the PR extends that function. The auto-draft case above confirms it still gets a pretty sample permalink.
  • get_sample_permalink() has two callers in core, get_sample_permalink_html() (classic editor permalink box / sample-permalink Ajax) and WP_REST_Posts_Controller::prepare_item_for_response() (permalink_template / generated_slug for the block editor). Both only consume the returned array, which is unchanged for every status tested.
  • Tests:
    npm run test:php -- --filter 'Tests_Admin_IncludesPost|Tests_Link_WpForcePlainPostPermalink|Tests_Link_GetPermalink|Tests_Link_GetPostPermalink|Tests_Link_GetPageLink'
    Tests: 54, Assertions: 121, Warnings: 6.
    
    (The 6 warnings are PHPUnit's pre-existing "Expecting E_DEPRECATED ... is deprecated" notices, unrelated to this change.)
  • PHPCS on the changed files: no new issues.
  • Pages are not affected because _get_page_link() has its own $sample handling and does not depend on the faked status.

Screenshots/Screencast with results

  • Before: terminal output in Steps taken.

https://files.catbox.moe/gkesbn.png

  • After: terminal output above.

https://files.catbox.moe/bwq5rx.png

Support Content

  • tests-local/repro-50002.php (run with npm run env:cli -- eval-file /var/www/tests-local/repro-50002.php):
    <?php
    require_once ABSPATH . 'wp-admin/includes/post.php';
    wp_set_current_user( 1 );
    $seen = array();
    add_filter( 'pre_post_link', function ( $permalink, $post ) use ( &$seen ) { $seen['pre_post_link'] = $post->post_status; return $permalink; }, 10, 2 );
    add_filter( 'post_link',     function ( $permalink, $post ) use ( &$seen ) { $seen['post_link']     = $post->post_status; return $permalink; }, 10, 2 );
    add_filter( 'page_link',     function ( $permalink, $post_id ) use ( &$seen ) { $seen['page_link'] = get_post( $post_id )->post_status; return $permalink; }, 10, 2 );
    
    $parent = wp_insert_post( array( 'post_type' => 'page', 'post_title' => 'Parent Page', 'post_status' => 'publish' ) );
    $cases = array(
    'draft post' => wp_insert_post( array( 'post_title' => 'A Draft Post 50002', 'post_status' => 'draft' ) ),
    'pending post' => wp_insert_post( array( 'post_title' => 'A Pending Post 50002', 'post_status' => 'pending' ) ),
    'future post' => wp_insert_post( array( 'post_title' => 'A Future Post 50002', 'post_status' => 'future', 'post_date' => gmdate( 'Y-m-d H:i:s', time() + WEEK_IN_SECONDS ) ) ),
    'auto-draft post' => wp_insert_post( array( 'post_title' => 'Auto Draft 50002', 'post_status' => 'auto-draft' ) ),
    'draft child page' => wp_insert_post( array( 'post_type' => 'page', 'post_title' => 'Child Page 50002', 'post_status' => 'draft', 'post_parent' => $parent ) ),
    'published post' => wp_insert_post( array( 'post_title' => 'A Published Post 50002', 'post_status' => 'publish' ) ),
    );
    foreach ( $cases as $label => $id ) {
    $seen = array();
    $sample = get_sample_permalink( $id );
    printf( "%-17s status seen by filters: %-40s sample: %s | %s\n", $label, json_encode( $seen ), $sample[0], $sample[1] );
    }
    foreach ( $cases as $id ) wp_delete_post( $id, true );
    wp_delete_post( $parent, true );
    
  • repro-50002-rest.php:
    <?php
    wp_set_current_user( 1 );
    $seen = array();
    add_filter( 'post_link', function ( $l, $p ) use ( &$seen ) { $seen[] = $p->post_status; return $l; }, 10, 2 );
    $id  = wp_insert_post( array( 'post_title' => 'REST Draft 50002', 'post_status' => 'draft' ) );
    $req = new WP_REST_Request( 'GET', "/wp/v2/posts/$id" ); $req->set_param( 'context', 'edit' );
    $d   = rest_do_request( $req )->get_data();
    echo 'permalink_template: ', $d['permalink_template'], "\ngenerated_slug:     ", $d['generated_slug'], "\npost_link saw:      ", implode( ',', array_unique( $seen ) ), "\n";
    wp_delete_post( $id, true );
    
Note: See TracTickets for help on using tickets.