diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index cf41630c4a8c5..c50cc31cc70fe 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -124,6 +124,29 @@ function wp_force_plain_post_permalink( $post = null, $sample = null ) { return true; } + /** + * Filters the post statuses that should use plain permalinks instead of pretty permalinks. + * + * By default, posts with 'draft', 'pending', 'auto-draft', and 'future' statuses + * will use plain permalinks (e.g., /?p=123) for security and consistency reasons. + * + * To allow scheduled posts to use pretty permalinks, you can remove 'future' from this array: + * + * add_filter( 'wp_force_plain_post_permalink_statuses', function( $statuses ) { + * return array_diff( $statuses, [ 'future' ] ); + * }); + * + * @since 6.9.0 + * + * @param string[] $statuses Array of post status names that should use plain permalinks. + * @param WP_Post $post The post object. + */ + $plain_permalink_statuses = apply_filters( + 'wp_force_plain_post_permalink_statuses', + array( 'draft', 'pending', 'auto-draft', 'future' ), + $post + ); + if ( // Publicly viewable links never have plain permalinks. is_post_status_viewable( $post_status_obj ) || @@ -133,7 +156,9 @@ function wp_force_plain_post_permalink( $post = null, $sample = null ) { current_user_can( 'read_post', $post->ID ) ) || // Protected posts don't have plain links if getting a sample URL. - ( $post_status_obj->protected && $sample ) + ( $post_status_obj->protected && $sample ) || + // Allow customization of which post statuses should use plain permalinks. + ! in_array( $post->post_status, $plain_permalink_statuses, true ) ) { return false; } diff --git a/tests/phpunit/tests/link.php b/tests/phpunit/tests/link.php index d05a6b210f636..b809bd15653af 100644 --- a/tests/phpunit/tests/link.php +++ b/tests/phpunit/tests/link.php @@ -187,4 +187,157 @@ public function test_attachment_attached_to_non_existent_post_type_has_a_pretty_ $this->go_to( get_permalink( $attachment_id ) ); $this->assertQueryTrue( 'is_attachment', 'is_single', 'is_singular' ); } + + /** + * @ticket 32322 + */ + public function test_wp_force_plain_post_permalink_statuses_filter_future_posts_default() { + $post_id = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'post_name' => 'future-post', + ) + ); + + $post = get_post( $post_id ); + + // Should return true (force plain permalink) for future posts by default. + $this->assertTrue( wp_force_plain_post_permalink( $post ) ); + } + + /** + * @ticket 32322 + */ + public function test_wp_force_plain_post_permalink_statuses_filter_allows_customization() { + // Add filter to allow future posts to use pretty permalinks. + add_filter( + 'wp_force_plain_post_permalink_statuses', + function ( $statuses ) { + return array_diff( $statuses, array( 'future' ) ); + } + ); + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'post_name' => 'future-post', + ) + ); + + $post = get_post( $post_id ); + + // Should return false (allow pretty permalink) when filter is applied. + $this->assertFalse( wp_force_plain_post_permalink( $post ) ); + + // Clean up. + remove_all_filters( 'wp_force_plain_post_permalink_statuses' ); + } + + /** + * @ticket 32322 + */ + public function test_wp_force_plain_post_permalink_statuses_filter_selective_by_status() { + // Add filter to allow only future posts to use pretty permalinks. + add_filter( + 'wp_force_plain_post_permalink_statuses', + function ( $statuses ) { + return array_diff( $statuses, array( 'future' ) ); + } + ); + + $future_post_id = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'post_name' => 'future-post', + ) + ); + + $draft_post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_name' => 'draft-post', + ) + ); + + $future_post = get_post( $future_post_id ); + $draft_post = get_post( $draft_post_id ); + + // Future post should use pretty permalink. + $this->assertFalse( wp_force_plain_post_permalink( $future_post ) ); + + // Draft post should still use plain permalink. + $this->assertTrue( wp_force_plain_post_permalink( $draft_post ) ); + + // Clean up. + remove_all_filters( 'wp_force_plain_post_permalink_statuses' ); + } + + /** + * @ticket 32322 + */ + public function test_wp_force_plain_post_permalink_statuses_filter_selective_by_post_type() { + // Add filter to allow future posts pretty permalinks only for 'post' type. + add_filter( + 'wp_force_plain_post_permalink_statuses', + function ( $statuses, $post ) { + if ( 'future' === $post->post_status && 'post' === $post->post_type ) { + return array_diff( $statuses, array( 'future' ) ); + } + return $statuses; + }, + 10, + 2 + ); + + // Create future post. + $post_id = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'post_name' => 'future-post', + ) + ); + + // Create future page. + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'future', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'post_name' => 'future-page', + ) + ); + + $post = get_post( $post_id ); + $page = get_post( $page_id ); + + // Future post should use pretty permalinks. + $this->assertFalse( wp_force_plain_post_permalink( $post ) ); + + // Future page should still use plain permalinks. + $this->assertTrue( wp_force_plain_post_permalink( $page ) ); + + // Clean up. + remove_all_filters( 'wp_force_plain_post_permalink_statuses' ); + } + + /** + * @ticket 32322 + */ + public function test_wp_force_plain_post_permalink_statuses_filter_published_posts_unaffected() { + $post_id = self::factory()->post->create( + array( + 'post_status' => 'publish', + 'post_name' => 'published-post', + ) + ); + + $post = get_post( $post_id ); + + // Should return false (allow pretty permalink) for published posts. + $this->assertFalse( wp_force_plain_post_permalink( $post ) ); + } }