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)
Change History (6)
#1
@
6 years ago
- Component General → Posts, Post Types
- Keywords has-patch needs-testing needs-unit-tests added
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_IncludesPostphp -d xdebug.mode=off vendor/bin/phpcbf src/wp-admin/includes/post.php src/wp-includes/link-template.php tests/phpunit/tests/admin/includesPost.phpphp -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
@
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
@
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
- Hooked
pre_post_link,post_linkandpage_linkfilters that record$post->post_statusas seen by the filter. - Created posts in
draft,pending,future,auto-draftandpublishstatus, plus a draft child page under a published parent page. - Called
get_sample_permalink()for each one via WP-CLI (repro-50002.phpbelow) and printed the status seen by the filters, the sample permalink and the editable slug. - Bug reproduced on clean trunk: every non-published post is reported to
pre_post_link/post_linkaspublish.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
- Applied PR #12577 on trunk.
- Ran the same WP-CLI script as in steps 1–3.
- Compared the sample permalink and editable slug for every case against the clean-trunk output.
- Reviewed
get_permalink(),get_post_permalink()and_get_page_link()for otherpost_status-dependent branches.
- 8a. Checked the other caller, the REST API (block editor path): requested a draft post with
context=editviarest_do_request()and comparedpermalink_template/generated_slugand the status seen bypost_link(repro-50002-rest.phpbelow).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
- Ran the related PHPUnit tests and PHPCS.
- ✅ 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, andget_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 iswp_force_plain_post_permalink().draft,pendingandfutureareprotectedstatuses, so they already get a pretty sample link without the hack;auto-draftis not, which is why the PR extends that function. Theauto-draftcase 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-permalinkAjax) andWP_REST_Posts_Controller::prepare_item_for_response()(permalink_template/generated_slugfor 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$samplehandling and does not depend on the faked status.
Screenshots/Screencast with results
- Before: terminal output in Steps taken.
- After: terminal output above.
Support Content
tests-local/repro-50002.php(run withnpm 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 );
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)


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