From 3e20e731c35fe08d026ac5062cd65ca9f674dc53 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 13:45:28 +1100 Subject: [PATCH 01/22] Tests for additional random rewrites. --- .../tests/query/randomContentRedirect.php | 220 +++++++ .../tests/rewrite/randomRewriteRules.php | 571 ++++++++++++++++++ 2 files changed, 791 insertions(+) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 825c1d15667e6..3532d8b52ce89 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -103,6 +103,25 @@ class Tests_Query_RandomContentRedirect extends WP_UnitTestCase { */ protected $recording_queries = false; + /** + * Rewrite rules added to the top of the rules before each test. + * + * WP_Rewrite::init() does not reset these, so they are restored after each test. + * + * @var array + */ + protected $original_extra_rules_top = array(); + + /** + * Extra permalink structures registered before each test. + * + * Unregistering post types between tests does not remove their permalink structures, + * so they are restored after each test. + * + * @var array> + */ + protected $original_extra_permastructs = array(); + /** * Creates shared user, category and post fixtures. * @@ -147,8 +166,13 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void * Sets up pretty permalinks and redirect capturing before each test. */ public function set_up(): void { + global $wp_rewrite; + parent::set_up(); + $this->original_extra_rules_top = $wp_rewrite->extra_rules_top; + $this->original_extra_permastructs = $wp_rewrite->extra_permastructs; + $this->set_permalink_structure( '/%postname%/' ); // Register the taxonomy rewrite rules for the permalink structure. create_initial_taxonomies(); @@ -167,6 +191,11 @@ public function set_up(): void { * Unregisters the test post types and taxonomy after each test. */ public function tear_down(): void { + global $wp_rewrite; + + $wp_rewrite->extra_rules_top = $this->original_extra_rules_top; + $wp_rewrite->extra_permastructs = $this->original_extra_permastructs; + unregister_post_type( 'wptests_book' ); unregister_post_type( 'wptests_hidden' ); unregister_post_type( 'wptests_unsearchable' ); @@ -2697,4 +2726,195 @@ public function test_unpublishing_a_post_named_random_resumes_random_content_red $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'Random content redirects should resume when the post named random is unpublished.' ); } + + /** + * Ensure a post type archive at the random base is not redirected to random content. + * + * @dataProvider data_post_type_archive_at_the_random_base_is_not_redirected + * + * @param string $post_type Post type name. + * @param array $args Post type registration arguments. + */ + public function test_post_type_archive_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { + register_post_type( $post_type, $args ); + flush_rewrite_rules(); + + $this->create_post( array( 'post_type' => $post_type ) ); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertNotRedirected( 'The post type archive at the random base should not redirect to random content.' ); + $this->assertQueryTrue( 'is_archive', 'is_post_type_archive' ); + $this->assertSame( $post_type, get_query_var( 'post_type' ), 'The post type archive at the random base should be queried.' ); + } + + /** + * Data provider. + * + * @return array}> + */ + public static function data_post_type_archive_at_the_random_base_is_not_redirected(): array { + return array( + 'post type named random' => array( + 'random', + array( + 'public' => true, + 'has_archive' => true, + ), + ), + 'archive slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => 'random', + ), + ), + ); + } + + /** + * Ensure random archives redirect when a post type archive is at the random base. + */ + public function test_random_archives_redirect_when_a_post_type_archive_is_at_the_random_base(): void { + register_post_type( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => 'random', + ) + ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/category/arts/' ) ); + + $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'Random archives should redirect when a post type archive is at the random base.' ); + } + + /** + * Ensure the random base is not redirected to random content when an existing rule takes it. + * + * @dataProvider data_random_base_is_not_redirected_when_an_existing_rule_takes_it + * + * @param string $regex Rewrite rule regex. + */ + public function test_random_base_is_not_redirected_when_an_existing_rule_takes_it( string $regex ): void { + $page = $this->create_post( + array( + 'post_type' => 'page', + 'post_name' => 'wptests-taken', + ) + ); + + add_rewrite_rule( $regex, 'index.php?pagename=wptests-taken', 'top' ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertNotRedirected( "The random base should not redirect to random content when the rule {$regex} takes it." ); + $this->assertSame( $page, get_queried_object_id(), "The page served by the rule {$regex} should be queried." ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_base_is_not_redirected_when_an_existing_rule_takes_it(): array { + return array( + 'exact match' => array( 'random/?$' ), + 'anchored exact match' => array( '^random/?$' ), + 'alternation' => array( '(random|pens|pencils)/?$' ), + 'partial group' => array( '(rand)om/?$' ), + 'character class' => array( '[r]andom/?$' ), + 'optional suffix' => array( 'random(?:-ful)?/?$' ), + ); + } + + /** + * Ensure random archives redirect when an existing rule takes the random base. + */ + public function test_random_archives_redirect_when_an_existing_rule_takes_the_random_base(): void { + add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/category/arts/' ) ); + + $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'Random archives should redirect when an existing rule takes the random base.' ); + } + + /** + * Ensure the random base redirects to random content when existing rules do not take it. + * + * @dataProvider data_random_base_redirects_when_existing_rules_do_not_take_it + * + * @param string $regex Rewrite rule regex. + * @param string $position Position of the rewrite rule, either 'top' or 'bottom'. + */ + public function test_random_base_redirects_when_existing_rules_do_not_take_it( string $regex, string $position ): void { + add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', $position ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), "The random base should redirect to random content when the rule {$regex} does not take it." ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_base_redirects_when_existing_rules_do_not_take_it(): array { + return array( + 'suffixed slug' => array( 'randomful/?$', 'top' ), + 'hyphenated suffixed slug' => array( '^random-ful/?$', 'top' ), + 'similar alternation' => array( '(randomful|random-ful)/?$', 'top' ), + 'sub path' => array( '^random/wptests/?$', 'top' ), + 'requires a sub path' => array( '^random/(.*)?', 'top' ), + 'catch-all' => array( '(.+?)/?$', 'bottom' ), + 'single segment catch-all' => array( '([^/]+)/?$', 'bottom' ), + ); + } + + /** + * Ensure a category at the random base with the category base removed is not redirected to random content. + * + * This replicates plugins that remove the category base from category archive URLs. + */ + public function test_category_at_the_random_base_without_a_category_base_is_not_redirected(): void { + $category = self::factory()->category->create( array( 'slug' => 'random' ) ); + + add_rewrite_rule( '(random)/?$', 'index.php?category_name=$matches[1]', 'top' ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertNotRedirected( 'A category at the random base should not redirect to random content.' ); + $this->assertQueryTrue( 'is_archive', 'is_category' ); + $this->assertSame( $category, get_queried_object_id(), 'The category at the random base should be queried.' ); + } + + /** + * Ensure the random base is not redirected to random content when filtered as taken. + */ + public function test_random_base_is_not_redirected_when_filtered_as_taken(): void { + add_filter( 'wp_rewrite_random_base_is_taken', '__return_true' ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertNotRedirected( 'The random base should not redirect to random content when filtered as taken.' ); + } + + /** + * Ensure random archives redirect when the random base is filtered as taken. + */ + public function test_random_archives_redirect_when_the_random_base_is_filtered_as_taken(): void { + add_filter( 'wp_rewrite_random_base_is_taken', '__return_true' ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/category/arts/' ) ); + + $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'Random archives should redirect when the random base is filtered as taken.' ); + } } diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index f2bc7ffb59fb6..fec84a1719e33 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -18,6 +18,25 @@ class Tests_Rewrite_RandomRewriteRules extends WP_UnitTestCase { */ const REDIRECT_EXCEPTION_MESSAGE = 'Random content redirect.'; + /** + * Rewrite rules added to the top of the rules before each test. + * + * WP_Rewrite::init() does not reset these, so they are restored after each test. + * + * @var array + */ + private $original_extra_rules_top = array(); + + /** + * Extra permalink structures registered before each test. + * + * Unregistering post types between tests does not remove their permalink structures, + * so they are restored after each test. + * + * @var array> + */ + private $original_extra_permastructs = array(); + /** * Requests a URL, stopping where a random content redirect ends the request. * @@ -34,8 +53,13 @@ public function go_to( $url ) { } public function set_up(): void { + global $wp_rewrite; + parent::set_up(); + $this->original_extra_rules_top = $wp_rewrite->extra_rules_top; + $this->original_extra_permastructs = $wp_rewrite->extra_permastructs; + // End the request before random content redirects exit, only the rewrite rules are under test. add_filter( 'wp_redirect', @@ -50,6 +74,11 @@ static function (): void { } public function tear_down(): void { + global $wp_rewrite; + + $wp_rewrite->extra_rules_top = $this->original_extra_rules_top; + $wp_rewrite->extra_permastructs = $this->original_extra_permastructs; + /* * The parent class re-initializes the rewrite base from this option before the * database transaction is rolled back, so it needs to be removed first. @@ -1054,4 +1083,546 @@ public static function data_posts_that_do_not_conflict(): array { ), ); } + + /** + * Ensure the root rule is kept when the rules are regenerated. + * + * Once generated, the root rule is in the existing rules and must not be treated + * as taking the random base itself. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + */ + public function test_random_root_rule_is_kept_when_the_rules_are_regenerated(): void { + global $wp_rewrite; + + $this->get_random_rewrite_rules(); + $this->assertArrayHasKey( 'random/?$', $wp_rewrite->rules, 'The root rule should be in the generated rules.' ); + + $rules = $wp_rewrite->random_rewrite_rules(); + + $this->assertArrayHasKey( 'random/?$', $rules, 'The root rule should be kept when the rules are regenerated.' ); + } + + /** + * Ensure the root rule is omitted when an existing rule takes the random base. + * + * @dataProvider data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base + * + * @param string $regex Rewrite rule regex. + */ + public function test_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base( string $regex ): void { + add_rewrite_rule( $regex, 'index.php?pagename=wptests-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayNotHasKey( 'random/?$', $rules, "The root rule should be omitted when the rule {$regex} takes the random base." ); + $this->assertArrayHasKey( 'random/category/(.+?)/?$', $rules, 'Random archive rules should be kept when the random base is taken.' ); + $this->assertArrayNotHasKey( 'random/?$', $this->filter_random_rules( $this->get_stored_rewrite_rules() ), 'The stored rules should not include the root rule.' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base(): array { + return array( + 'exact match' => array( 'random/?$' ), + 'anchored exact match' => array( '^random/?$' ), + 'unterminated' => array( 'random' ), + 'optional suffix' => array( 'random(ful)?/?$' ), + 'optional non-capturing suffix' => array( 'random(?:-ful)?/?$' ), + 'anchored optional suffix' => array( '^random(?:-ful)?/?$' ), + 'alternation, first' => array( '(random|pens|pencils)/?$' ), + 'alternation, last' => array( '(pens|pencils|random)/?$' ), + 'anchored alternation' => array( '^(random|pens|pencils)/?$' ), + 'non-capturing alternation' => array( '(?:pens|random)/?$' ), + 'partial group' => array( '(rand)om/?$' ), + 'anchored partial group' => array( '^(rand)om/?$' ), + 'group in the middle' => array( 'r(and)om/?$' ), + 'character class' => array( '[r]andom/?$' ), + 'character class range' => array( '[a-z]andom/?$' ), + 'wildcard' => array( '.andom/?$' ), + 'optional final character' => array( 'random?/?$' ), + ); + } + + /** + * Ensure the root rule is kept when existing rules do not take the random base. + * + * @dataProvider data_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base + * + * @param string $regex Rewrite rule regex. + */ + public function test_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base( string $regex ): void { + add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayHasKey( 'random/?$', $rules, "The root rule should be kept when the rule {$regex} does not take the random base." ); + $this->assertSame( 'index.php?random=1', $rules['random/?$'], 'The root rule should set the random query variable.' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base(): array { + return array( + 'suffixed slug' => array( 'randomful/?$' ), + 'hyphenated suffixed slug' => array( 'random-ful/?$' ), + 'anchored suffixed slug' => array( '^randomful/?$' ), + 'anchored hyphenated slug' => array( '^random-ful/?$' ), + 'suffixed slug with sub path' => array( '^randomful/(.*)?' ), + 'hyphenated slug feed' => array( 'random-ful/feed/(feed|rdf|rss|rss2|atom)/?$' ), + 'required suffix' => array( 'random(ful|ness)/?$' ), + 'prefixed slug' => array( 'not-random/?$' ), + 'anchored prefixed slug' => array( '^not-random/?$' ), + 'sub path of the random base' => array( 'random/wptests/?$' ), + 'catch-all' => array( '(.+?)/?$' ), + 'anchored catch-all' => array( '^(.+?)/?$' ), + 'single segment catch-all' => array( '([^/]+)/?$' ), + 'letters only catch-all' => array( '([a-z]+)/?$' ), + 'page catch-all' => array( '(.?.+?)(?:/([0-9]+))?/?$' ), + 'alternation without random' => array( '(pens|pencils)/?$' ), + 'alternation of similar slugs' => array( '(randomful|random-ful)/?$' ), + 'partial group, suffixed' => array( '(rand)omful/?$' ), + 'character class, suffixed' => array( '[r]andomful/?$' ), + ); + } + + /** + * Ensure a request for the random base is a random content request when existing rules do not take the random base. + * + * @dataProvider data_random_base_request_matches_the_root_rule_when_existing_rules_do_not_take_the_random_base + * + * @global WP $wp Current WordPress environment instance. + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + * + * @param string $regex Rewrite rule regex. + * @param string $position Position of the rewrite rule, either 'top' or 'bottom'. + * @param string $structure Permalink structure. + * @param string $home_path Path of the site's home URL, for sites installed in a subdirectory. + */ + public function test_random_base_request_matches_the_root_rule_when_existing_rules_do_not_take_the_random_base( string $regex, string $position, string $structure, string $home_path ): void { + global $wp, $wp_rewrite; + + if ( '' !== $home_path ) { + update_option( 'home', untrailingslashit( home_url() ) . $home_path ); + } + + $this->set_permalink_structure( $structure ); + create_initial_taxonomies(); + + add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', $position ); + flush_rewrite_rules(); + + $this->go_to( home_url( user_trailingslashit( $wp_rewrite->root . 'random' ) ) ); + + $this->assertSame( $wp_rewrite->root . 'random/?$', $wp->matched_rule, "A request for the random base should match the root rule rather than {$wp->matched_rule}." ); + $this->assertSame( '1', $wp->query_vars['random'] ?? null, 'The random query variable should be set for a request for the random base.' ); + } + + /** + * Data provider. + * + * Each rule is tested with each permalink structure. For PATHINFO permalink structures, + * each rule is also tested prefixed with `index.php/`, as rules for such sites usually are. + * + * Catch-all rules are added to the bottom of the rules. A catch-all rule added to the top + * takes precedence over all of the rules WordPress generates, including the random content + * rules, so matches every request, as intended by the 'top' position. + * + * @return array + */ + public static function data_random_base_request_matches_the_root_rule_when_existing_rules_do_not_take_the_random_base(): array { + $structures = array( + 'post name' => array( '/%postname%/', '' ), + 'no trailing slash' => array( '/%postname%', '' ), + 'verbose page rules' => array( '/%category%/%postname%/', '' ), + 'day and name' => array( '/%year%/%monthnum%/%day%/%postname%/', '' ), + 'numeric' => array( '/archives/%post_id%', '' ), + 'subdirectory install' => array( '/%postname%/', '/wptests-subdirectory' ), + 'pathinfo post name' => array( '/index.php/%postname%/', '' ), + 'pathinfo day and name' => array( '/index.php/%year%/%monthnum%/%day%/%postname%/', '' ), + 'pathinfo no trailing' => array( '/index.php/%postname%', '' ), + ); + + $data = array(); + foreach ( self::data_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base() as $rule_name => $rule ) { + $position = str_contains( $rule_name, 'catch-all' ) ? 'bottom' : 'top'; + + foreach ( $structures as $structure_name => $structure ) { + $data[ "{$rule_name}, {$structure_name}" ] = array( $rule[0], $position, $structure[0], $structure[1] ); + + if ( str_starts_with( $structure[0], '/index.php/' ) ) { + $prefixed_rule = ( str_starts_with( $rule[0], '^' ) ? '^' : '' ) . 'index.php/' . ltrim( $rule[0], '^' ); + + $data[ "{$rule_name} prefixed with index.php, {$structure_name}" ] = array( $prefixed_rule, $position, $structure[0], $structure[1] ); + } + } + } + + return $data; + } + + /** + * Ensure a rule requiring a trailing slash does not take the random base. + * + * This applies regardless of whether the permalink structure uses a trailing slash. + * WP::parse_request() trims slashes from the requested path, so such a rule never + * matches a request for the random base. + * + * @dataProvider data_rules_requiring_a_trailing_slash_do_not_take_the_random_base + * + * @global WP $wp Current WordPress environment instance. + * + * @param string $structure Permalink structure. + * @param string $path Request path for the random base. + */ + public function test_rules_requiring_a_trailing_slash_do_not_take_the_random_base( string $structure, string $path ): void { + global $wp; + + $this->set_permalink_structure( $structure ); + create_initial_taxonomies(); + + add_rewrite_rule( '^random/(.*)?', 'index.php?pagename=wptests-not-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayHasKey( 'random/?$', $rules, 'The root rule should be kept for a rule requiring a trailing slash.' ); + + $this->go_to( home_url( $path ) ); + + $this->assertSame( 'random/?$', $wp->matched_rule, 'The random base should be matched by the root rule.' ); + $this->assertSame( '1', $wp->query_vars['random'] ?? null, 'The random query variable should be set.' ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_rules_requiring_a_trailing_slash_do_not_take_the_random_base(): array { + return array( + 'trailing slash' => array( '/%postname%/', '/random/' ), + 'no trailing slash' => array( '/%postname%', '/random' ), + ); + } + + /** + * Ensure a request for the random base matches the root rule. + * + * Random content rules must be matched ahead of the generic post and page rules. + * + * @dataProvider data_permalink_structures + * + * @global WP $wp Current WordPress environment instance. + * + * @param string $structure Permalink structure. + */ + public function test_random_base_request_matches_the_root_rule( string $structure ): void { + global $wp; + + $this->set_permalink_structure( $structure ); + create_initial_taxonomies(); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertSame( 'random/?$', $wp->matched_rule, 'The random base should be matched by the root rule.' ); + } + + /** + * Ensure a post type archive at the random base takes precedence over the root rule. + * + * @dataProvider data_post_types_using_the_random_base + * + * @param string $post_type Post type name. + * @param array $args Post type registration arguments. + */ + public function test_random_root_rule_is_omitted_for_a_post_type_archive_at_the_random_base( string $post_type, array $args ): void { + register_post_type( $post_type, $args ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayNotHasKey( 'random/?$', $rules, 'The root rule should be omitted for a post type archive at the random base.' ); + $this->assertSame( "index.php?post_type={$post_type}", $this->get_stored_rewrite_rules()['random/?$'] ?? null, 'The post type archive should be reachable at the random base.' ); + } + + /** + * Ensure a post type archive at the random base is not redirected to random content. + * + * @dataProvider data_post_types_using_the_random_base + * + * @param string $post_type Post type name. + * @param array $args Post type registration arguments. + */ + public function test_post_type_archive_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { + register_post_type( $post_type, $args ); + $this->get_random_rewrite_rules(); + + self::factory()->post->create_many( 2, array( 'post_type' => $post_type ) ); + self::factory()->post->create(); + + $redirect = new MockAction(); + add_filter( 'wp_redirect', array( $redirect, 'filter' ), 5 ); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertSame( 0, $redirect->get_call_count(), 'The post type archive should not redirect to random content.' ); + $this->assertQueryTrue( 'is_archive', 'is_post_type_archive' ); + $this->assertSame( $post_type, get_query_var( 'post_type' ), 'The post type archive should be queried.' ); + } + + /** + * Ensure a single post of a post type at the random base is not redirected to random content. + * + * Registering a post type named `random` also registers `random` as its query variable, + * which is the query variable used by random content requests. + * + * @dataProvider data_post_types_using_the_random_base + * + * @param string $post_type Post type name. + * @param array $args Post type registration arguments. + */ + public function test_single_post_of_a_post_type_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { + register_post_type( $post_type, $args ); + $this->get_random_rewrite_rules(); + + $post_id = self::factory()->post->create( array( 'post_type' => $post_type ) ); + self::factory()->post->create(); + + $permalink = get_permalink( $post_id ); + $this->assertIsString( $permalink, 'The post permalink could not be retrieved.' ); + $this->assertStringStartsWith( home_url( '/random/' ), $permalink, 'The post permalink should use the random base.' ); + + $redirect = new MockAction(); + add_filter( 'wp_redirect', array( $redirect, 'filter' ), 5 ); + + $this->go_to( $permalink ); + + $this->assertSame( 0, $redirect->get_call_count(), 'The single post should not redirect to random content.' ); + $this->assertQueryTrue( 'is_single', 'is_singular' ); + $this->assertSame( $post_id, get_queried_object_id(), 'The single post should be queried.' ); + } + + /** + * Data provider. + * + * @return array}> + */ + public static function data_post_types_using_the_random_base(): array { + return array( + 'post type named random' => array( + 'random', + array( + 'public' => true, + 'has_archive' => true, + ), + ), + 'archive slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => 'random', + 'rewrite' => array( 'slug' => 'random' ), + ), + ), + 'rewrite slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => true, + 'rewrite' => array( 'slug' => 'random' ), + ), + ), + ); + } + + /** + * Ensure a post type archive with a slug similar to the random base does not take it. + * + * @dataProvider data_random_root_rule_is_kept_for_a_post_type_archive_with_a_similar_slug + * + * @param string $slug Post type archive slug. + */ + public function test_random_root_rule_is_kept_for_a_post_type_archive_with_a_similar_slug( string $slug ): void { + register_post_type( + 'wptests_similar', + array( + 'public' => true, + 'has_archive' => $slug, + ) + ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayHasKey( 'random/?$', $rules, "The root rule should be kept for a post type archive at {$slug}." ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_root_rule_is_kept_for_a_post_type_archive_with_a_similar_slug(): array { + return array( + 'suffixed' => array( 'randomful' ), + 'hyphenated' => array( 'random-ful' ), + 'prefixed' => array( 'not-random' ), + ); + } + + /** + * Ensure the root rule is omitted when an existing rule takes a custom random base. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + */ + public function test_random_root_rule_is_omitted_when_an_existing_rule_takes_a_custom_random_base(): void { + global $wp_rewrite; + + $wp_rewrite->random_base = 'surprise-me'; + add_rewrite_rule( '^surprise-me/?$', 'index.php?pagename=wptests-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayNotHasKey( 'surprise-me/?$', $rules, 'The root rule should be omitted when a rule takes the custom random base.' ); + } + + /** + * Ensure a rule at the default random base does not take a custom random base. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + */ + public function test_random_root_rule_is_kept_when_an_existing_rule_uses_the_default_base_with_a_custom_random_base(): void { + global $wp_rewrite; + + $wp_rewrite->random_base = 'surprise-me'; + add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayHasKey( 'surprise-me/?$', $rules, 'The root rule should be kept when a rule only uses the default random base.' ); + } + + /** + * Ensure the root rule is omitted when an existing rule takes the random base with PATHINFO permalinks. + * + * @dataProvider data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base_with_pathinfo_permalinks + * + * @param string $regex Rewrite rule regex. + */ + public function test_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base_with_pathinfo_permalinks( string $regex ): void { + $this->set_permalink_structure( '/index.php/%postname%/' ); + create_initial_taxonomies(); + + add_rewrite_rule( $regex, 'index.php?pagename=wptests-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayNotHasKey( 'index.php/random/?$', $rules, "The root rule should be omitted when the rule {$regex} takes the random base." ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base_with_pathinfo_permalinks(): array { + return array( + 'exact match' => array( 'index.php/random/?$' ), + 'anchored exact match' => array( '^index.php/random/?$' ), + ); + } + + /** + * Ensure the root rule is kept when an existing catch-all rule matches the random base with PATHINFO permalinks. + * + * @dataProvider data_random_root_rule_is_kept_when_an_existing_catch_all_rule_matches_the_random_base_with_pathinfo_permalinks + * + * @param string $regex Rewrite rule regex. + */ + public function test_random_root_rule_is_kept_when_an_existing_catch_all_rule_matches_the_random_base_with_pathinfo_permalinks( string $regex ): void { + $this->set_permalink_structure( '/index.php/%postname%/' ); + create_initial_taxonomies(); + + add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', 'top' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayHasKey( 'index.php/random/?$', $rules, "The root rule should be kept when the catch-all rule {$regex} matches the random base." ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_root_rule_is_kept_when_an_existing_catch_all_rule_matches_the_random_base_with_pathinfo_permalinks(): array { + return array( + 'catch-all' => array( 'index.php/(.+?)/?$' ), + 'anchored catch-all' => array( '^index.php/(.+?)/?$' ), + 'single segment catch-all' => array( 'index.php/([^/]+)/?$' ), + 'page catch-all' => array( 'index.php/(.?.+?)(?:/([0-9]+))?/?$' ), + ); + } + + /** + * Ensure the random base is taken filter can omit the root rule. + */ + public function test_random_base_is_taken_filter_can_omit_the_root_rule(): void { + add_filter( 'wp_rewrite_random_base_is_taken', '__return_true' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayNotHasKey( 'random/?$', $rules, 'The filter should be able to omit the root rule.' ); + $this->assertArrayHasKey( 'random/category/(.+?)/?$', $rules, 'Random archive rules should be kept when the filter omits the root rule.' ); + } + + /** + * Ensure the random base is taken filter can keep the root rule when a rule takes the random base. + */ + public function test_random_base_is_taken_filter_can_keep_the_root_rule(): void { + add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + add_filter( 'wp_rewrite_random_base_is_taken', '__return_false' ); + + $rules = $this->get_random_rewrite_rules(); + + $this->assertArrayHasKey( 'random/?$', $rules, 'The filter should be able to keep the root rule when a rule takes the random base.' ); + } + + /** + * Ensure the random base is taken filter receives the detected value and the random base. + */ + public function test_random_base_is_taken_filter_receives_the_detected_value_and_random_base(): void { + add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + + $filter = new MockAction(); + add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 2 ); + + $this->get_random_rewrite_rules(); + + $args = $filter->get_args(); + $this->assertNotEmpty( $args, 'The filter was not called.' ); + $this->assertSame( array( true, 'random' ), end( $args ), 'The filter should receive whether the base is taken and the random base.' ); + } + + /** + * Filters rewrite rules to those setting the random query variable. + * + * @param array $rules Rewrite rules, keyed by their regex pattern. + * @return array Random content rewrite rules, keyed by their regex pattern. + */ + private function filter_random_rules( array $rules ): array { + return array_filter( + $rules, + static function ( $query ): bool { + parse_str( (string) wp_parse_url( (string) $query, PHP_URL_QUERY ), $query_vars ); + + return isset( $query_vars['random'] ); + } + ); + } } From b2b11d6579ca08c26abaf7233cf0368605872540 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 14:18:37 +1100 Subject: [PATCH 02/22] Add test for rewrite rule defined as `random`. --- tests/phpunit/tests/query/randomContentRedirect.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 3532d8b52ce89..fbe98901b6f10 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2769,6 +2769,14 @@ public static function data_post_type_archive_at_the_random_base_is_not_redirect 'has_archive' => 'random', ), ), + 'rewrite slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => true, + 'rewrite' => array( 'slug' => 'random' ), + ), + ), ); } From b0bdfc289581238a1c8e7c084ae3ee612bef9615 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 13:46:16 +1100 Subject: [PATCH 03/22] Use existing rewrite rules to test if /random is taken. --- src/wp-includes/class-wp-rewrite.php | 118 ++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 20967bafa0206..584a267e3400b 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1423,22 +1423,6 @@ public function rewrite_rules() { */ $page_rewrite = apply_filters( 'page_rewrite_rules', $page_rewrite ); - // Random content rewrite rules. - $random_rewrite = $this->random_rewrite_rules(); - - /** - * Filters rewrite rules used for random content redirects. - * - * Likely random content rules would include `/random/`, as well as - * term, author and post type archives prefixed with the random base, - * for example `/random/category/news/`. - * - * @since 7.2.0 - * - * @param array $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern. - */ - $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite ); - // Extra permastructs. foreach ( $this->extra_permastructs as $permastructname => $struct ) { if ( is_array( $struct ) ) { @@ -1485,7 +1469,35 @@ public function rewrite_rules() { $this->extra_rules_top = array_merge( $this->extra_rules_top, $rules ); } - // Put them together. + /* + * Random content rewrite rules. + * + * The random content rules check whether an existing rule already uses the + * random base. The page and post rules are generic catch-all rules matching + * any path, so are not checked. Pages and posts at the random base are + * accounted for separately by WP_Rewrite::random_rewrite_rules(). + */ + $random_rewrite = $this->random_rewrite_rules( + array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $this->extra_rules ) + ); + + /** + * Filters rewrite rules used for random content redirects. + * + * Likely random content rules would include `/random/`, as well as + * term, author and post type archives prefixed with the random base, + * for example `/random/category/news/`. + * + * @since 7.2.0 + * + * @param array $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern. + */ + $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite ); + + /* + * Put them together, with the random content rules ahead of the generic rules + * such as the page and post rules which would otherwise match them first. + */ if ( $this->use_verbose_page_rules ) { $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $random_rewrite, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules ); } else { @@ -1539,17 +1551,23 @@ public function rewrite_rules() { * * @see wp_is_random_content_redirect_enabled() * + * @param array|null $existing_rules Optional. Existing rewrite rules to check for rules using the + * random base, keyed by their regex pattern. Generic catch-all + * rules, such as the page and post rules, should not be included + * as they match any path. Default null, the extra rules added + * via WP_Rewrite::add_rule() and WP_Rewrite::add_permastruct(). * @return array Array of rewrite rules for random content redirects, keyed by their regex pattern. * * @phpstan-return array */ - public function random_rewrite_rules(): array { + public function random_rewrite_rules( ?array $existing_rules = null ): array { if ( ! wp_is_random_content_redirect_enabled() ) { return array(); } - $random_slug = $this->get_random_base(); - $random_base = $this->root . $random_slug; + $random_slug = $this->get_random_base(); + $random_base = $this->root . $random_slug; + $catch_all_test_base = "{$this->root}catchall{$random_slug}"; // No hyphens to account for [A-z0-9] rules. $rules = array(); @@ -1564,6 +1582,66 @@ public function random_rewrite_rules(): array { fn( $random_base_post ) => $random_base_post instanceof WP_Post && is_post_publicly_viewable( $random_base_post ) ); + if ( ! $random_base_is_taken ) { + /* + * WP::parse_request() trims slashes from the requested path before matching + * it against the rules, so the path is matched without a trailing slash + * regardless of the permalink structure. + */ + $random_base_path = untrailingslashit( $random_base ); + + /* + * The generic catch-all page and post rules match any path, including the + * random base, so are excluded from the existing rules. Pages and posts at + * the random base, which these rules serve, are accounted for above. + */ + $existing_rules ??= array_merge( $this->extra_rules_top, $this->extra_rules ); + + foreach ( $existing_rules as $regex => $query ) { + /* + * Match the same way WP::parse_request() does, so the regex is anchored to + * the start of the path whether or not it includes a leading caret. + * + * Matching the regex rather than comparing strings accounts for rules + * written in any form, such as `random/?$`, `^random/?$`, `(random|pens)/?$`, + * `(rand)om/?$` or `[r]andom/?$`, while rules for similarly named paths, + * such as `randomful/?$` or `random-ful/?$`, do not match. + * + * A rule requiring a trailing slash, such as `^random/(.*)?`, never matches + * a request for the random base, so it does not take it. + */ + $pattern = "#^{$regex}#"; + if ( ! preg_match( $pattern, $random_base_path ) ) { + continue; + } + + /* + * Generic catch-all rules, such as `(.+?)/?$` or `([^/]+)/?$`, match any + * path including the random base. A rule also matching a path other than + * the random base, for example `notrandom`, is a catch-all rule so does + * not take the random base. + */ + if ( preg_match( $pattern, $catch_all_test_base ) ) { + continue; + } + + $random_base_is_taken = true; + break; + } + } + + /** + * Filter whether the Random Redirect base is taken. + * + * Determine whether the random base redirect is taken. The random base may be + * modified depending on the site's language or permalink settings. Always determin + * the value from the second parameter, `$random_base`. + * + * @param bool $random_base_is_taken Whether the random base is known to be taken. + * @param string $random_base The random base slug for the site. + */ + $random_base_is_taken = apply_filters( 'wp_rewrite_random_base_is_taken', $random_base_is_taken, $random_base ); + if ( ! $random_base_is_taken ) { $rules[ $random_base . '/?$' ] = $this->index . '?random=1'; } From 8414b623be032434af14ddf8887390ef2349ff53 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 14:50:35 +1100 Subject: [PATCH 04/22] Tidy up new filter: typos, docs and passing the slug alone. --- src/wp-includes/class-wp-rewrite.php | 18 ++++++++++----- .../tests/rewrite/randomRewriteRules.php | 23 ++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 584a267e3400b..c8a250ee0941a 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1631,16 +1631,22 @@ public function random_rewrite_rules( ?array $existing_rules = null ): array { } /** - * Filter whether the Random Redirect base is taken. + * Filters whether the random content redirect base is taken. * - * Determine whether the random base redirect is taken. The random base may be - * modified depending on the site's language or permalink settings. Always determin - * the value from the second parameter, `$random_base`. + * When the random base is taken, the `/random/` rewrite rule is omitted. The random + * base may vary depending on the site's language or permalink settings, so always + * determine the value from the `$random_base` or `$random_slug` parameters. + * + * @since 7.2.0 * * @param bool $random_base_is_taken Whether the random base is known to be taken. - * @param string $random_base The random base slug for the site. + * @param string $random_base The random base for the site, including the PATHINFO + * prefix when using PATHINFO permalinks, for example + * `random` or `index.php/random`. + * @param string $random_slug The random base slug for the site, without the PATHINFO + * prefix, for example `random`. */ - $random_base_is_taken = apply_filters( 'wp_rewrite_random_base_is_taken', $random_base_is_taken, $random_base ); + $random_base_is_taken = apply_filters( 'wp_rewrite_random_base_is_taken', $random_base_is_taken, $random_base, $random_slug ); if ( ! $random_base_is_taken ) { $rules[ $random_base . '/?$' ] = $this->index . '?random=1'; diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index fec84a1719e33..e5698b985976c 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -1594,19 +1594,36 @@ public function test_random_base_is_taken_filter_can_keep_the_root_rule(): void } /** - * Ensure the random base is taken filter receives the detected value and the random base. + * Ensure the random base is taken filter receives the detected value, the random base and the random slug. */ public function test_random_base_is_taken_filter_receives_the_detected_value_and_random_base(): void { add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); $filter = new MockAction(); - add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 2 ); + add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 3 ); $this->get_random_rewrite_rules(); $args = $filter->get_args(); $this->assertNotEmpty( $args, 'The filter was not called.' ); - $this->assertSame( array( true, 'random' ), end( $args ), 'The filter should receive whether the base is taken and the random base.' ); + $this->assertSame( array( true, 'random', 'random' ), end( $args ), 'The filter should receive whether the base is taken, the random base and the random slug.' ); + } + + /** + * Ensure the random base is taken filter receives the random base with the PATHINFO prefix and the random slug without it. + */ + public function test_random_base_is_taken_filter_receives_the_random_base_and_slug_with_pathinfo_permalinks(): void { + $this->set_permalink_structure( '/index.php/%postname%/' ); + create_initial_taxonomies(); + + $filter = new MockAction(); + add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 3 ); + + $this->get_random_rewrite_rules(); + + $args = $filter->get_args(); + $this->assertNotEmpty( $args, 'The filter was not called with PATHINFO permalinks.' ); + $this->assertSame( array( false, 'index.php/random', 'random' ), end( $args ), 'The filter should receive the random base with the PATHINFO prefix and the random slug without it.' ); } /** From b8dbe62a16528af1f79e4ba13c39d95d8f5af819 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 15:06:08 +1100 Subject: [PATCH 05/22] Remove duplicate test. --- .../tests/rewrite/randomRewriteRules.php | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index e5698b985976c..1257638f2a9da 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -1351,31 +1351,6 @@ public function test_random_root_rule_is_omitted_for_a_post_type_archive_at_the_ $this->assertSame( "index.php?post_type={$post_type}", $this->get_stored_rewrite_rules()['random/?$'] ?? null, 'The post type archive should be reachable at the random base.' ); } - /** - * Ensure a post type archive at the random base is not redirected to random content. - * - * @dataProvider data_post_types_using_the_random_base - * - * @param string $post_type Post type name. - * @param array $args Post type registration arguments. - */ - public function test_post_type_archive_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { - register_post_type( $post_type, $args ); - $this->get_random_rewrite_rules(); - - self::factory()->post->create_many( 2, array( 'post_type' => $post_type ) ); - self::factory()->post->create(); - - $redirect = new MockAction(); - add_filter( 'wp_redirect', array( $redirect, 'filter' ), 5 ); - - $this->go_to( home_url( '/random/' ) ); - - $this->assertSame( 0, $redirect->get_call_count(), 'The post type archive should not redirect to random content.' ); - $this->assertQueryTrue( 'is_archive', 'is_post_type_archive' ); - $this->assertSame( $post_type, get_query_var( 'post_type' ), 'The post type archive should be queried.' ); - } - /** * Ensure a single post of a post type at the random base is not redirected to random content. * From 991356bbd9a0e1e354fad24fa348eaf0b7512be6 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 15:08:30 +1100 Subject: [PATCH 06/22] Move `test_single_post_of_a_post_type_at_the_random_base_is_not_redirected` to content tests. --- .../tests/query/randomContentRedirect.php | 65 +++++++++++++++++++ .../tests/rewrite/randomRewriteRules.php | 32 --------- 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index fbe98901b6f10..b8195c366d29f 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2780,6 +2780,71 @@ public static function data_post_type_archive_at_the_random_base_is_not_redirect ); } + /** + * Ensure a single post of a post type at the random base is not redirected to random content. + * + * Registering a post type named `random` also registers `random` as its query variable, + * which is the query variable used by random content requests. + * + * @dataProvider data_single_post_of_a_post_type_at_the_random_base_is_not_redirected + * + * @param string $post_type Post type name. + * @param array $args Post type registration arguments. + */ + public function test_single_post_of_a_post_type_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { + register_post_type( $post_type, $args ); + flush_rewrite_rules(); + + $post_id = self::factory()->post->create( array( 'post_type' => $post_type ) ); + self::factory()->post->create(); + + $permalink = get_permalink( $post_id ); + $this->assertIsString( $permalink, 'The post permalink could not be retrieved.' ); + $this->assertStringStartsWith( home_url( '/random/' ), $permalink, 'The post permalink should use the random base.' ); + + $redirect = new MockAction(); + add_filter( 'wp_redirect', array( $redirect, 'filter' ), 5 ); + + $this->go_to( $permalink ); + + $this->assertSame( 0, $redirect->get_call_count(), 'The single post should not redirect to random content.' ); + $this->assertQueryTrue( 'is_single', 'is_singular' ); + $this->assertSame( $post_id, get_queried_object_id(), 'The single post should be queried.' ); + } + + /** + * Data provider. + * + * @return array}> + */ + public static function data_single_post_of_a_post_type_at_the_random_base_is_not_redirected(): array { + return array( + 'post type named random' => array( + 'random', + array( + 'public' => true, + 'has_archive' => true, + ), + ), + 'archive slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => 'random', + 'rewrite' => array( 'slug' => 'random' ), + ), + ), + 'rewrite slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => true, + 'rewrite' => array( 'slug' => 'random' ), + ), + ), + ); + } + /** * Ensure random archives redirect when a post type archive is at the random base. */ diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index 1257638f2a9da..a53c62ea5259f 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -1351,38 +1351,6 @@ public function test_random_root_rule_is_omitted_for_a_post_type_archive_at_the_ $this->assertSame( "index.php?post_type={$post_type}", $this->get_stored_rewrite_rules()['random/?$'] ?? null, 'The post type archive should be reachable at the random base.' ); } - /** - * Ensure a single post of a post type at the random base is not redirected to random content. - * - * Registering a post type named `random` also registers `random` as its query variable, - * which is the query variable used by random content requests. - * - * @dataProvider data_post_types_using_the_random_base - * - * @param string $post_type Post type name. - * @param array $args Post type registration arguments. - */ - public function test_single_post_of_a_post_type_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { - register_post_type( $post_type, $args ); - $this->get_random_rewrite_rules(); - - $post_id = self::factory()->post->create( array( 'post_type' => $post_type ) ); - self::factory()->post->create(); - - $permalink = get_permalink( $post_id ); - $this->assertIsString( $permalink, 'The post permalink could not be retrieved.' ); - $this->assertStringStartsWith( home_url( '/random/' ), $permalink, 'The post permalink should use the random base.' ); - - $redirect = new MockAction(); - add_filter( 'wp_redirect', array( $redirect, 'filter' ), 5 ); - - $this->go_to( $permalink ); - - $this->assertSame( 0, $redirect->get_call_count(), 'The single post should not redirect to random content.' ); - $this->assertQueryTrue( 'is_single', 'is_singular' ); - $this->assertSame( $post_id, get_queried_object_id(), 'The single post should be queried.' ); - } - /** * Data provider. * From 590fd7a90a5f448af61a79995e26718b1a3447be Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 15:11:44 +1100 Subject: [PATCH 07/22] Rename data provider for use with a single test. --- tests/phpunit/tests/rewrite/randomRewriteRules.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index a53c62ea5259f..e50652c172e92 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -1337,7 +1337,7 @@ public function test_random_base_request_matches_the_root_rule( string $structur /** * Ensure a post type archive at the random base takes precedence over the root rule. * - * @dataProvider data_post_types_using_the_random_base + * @dataProvider data_random_root_rule_is_omitted_for_a_post_type_archive_at_the_random_base * * @param string $post_type Post type name. * @param array $args Post type registration arguments. @@ -1356,7 +1356,7 @@ public function test_random_root_rule_is_omitted_for_a_post_type_archive_at_the_ * * @return array}> */ - public static function data_post_types_using_the_random_base(): array { + public static function data_random_root_rule_is_omitted_for_a_post_type_archive_at_the_random_base(): array { return array( 'post type named random' => array( 'random', From 69792f59d1b30f11e1df8312d91605195549b239 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 15:18:17 +1100 Subject: [PATCH 08/22] Merge mergable tests. --- .../tests/rewrite/randomRewriteRules.php | 60 +++++++------------ 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index e50652c172e92..00e2fd77eed93 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -695,7 +695,7 @@ static function ( array $rules ): array { * Random rules must be matched before any other rule matching the same URL, * such as the greedy page and post rules. * - * @dataProvider data_permalink_structures + * @dataProvider data_random_rewrite_rules_precede_other_matching_rules * * @param string $structure Permalink structure. */ @@ -733,7 +733,7 @@ public function test_random_rewrite_rules_precede_other_matching_rules( $structu * * @return array */ - public static function data_permalink_structures(): array { + public static function data_random_rewrite_rules_precede_other_matching_rules(): array { return array( 'post name' => array( '/%postname%/' ), 'verbose page rules' => array( '/%category%/%postname%/' ), @@ -748,10 +748,17 @@ public static function data_permalink_structures(): array { * * @param string $path Request path. * @param array $expected_query Expected query variables. + * @param string $structure Optional. Permalink structure. Default empty, the + * structure set up for each test. */ - public function test_random_urls_resolve_to_random_query_vars( $path, $expected_query ): void { + public function test_random_urls_resolve_to_random_query_vars( $path, $expected_query, string $structure = '' ): void { global $wp; + if ( '' !== $structure ) { + $this->set_permalink_structure( $structure ); + create_initial_taxonomies(); + } + register_taxonomy( 'wptests_genre', 'post', array( 'rewrite' => array( 'slug' => 'genre' ) ) ); register_post_type( 'wptests_book', @@ -773,56 +780,58 @@ public function test_random_urls_resolve_to_random_query_vars( $path, $expected_ /** * Data provider. * - * @return array}> + * @return array, 2?: string}> */ public static function data_random_urls(): array { return array( - 'random' => array( '/random/', array( 'random' => '1' ) ), - 'random without slash' => array( '/random', array( 'random' => '1' ) ), - 'random category' => array( + 'random' => array( '/random/', array( 'random' => '1' ) ), + 'random without slash' => array( '/random', array( 'random' => '1' ) ), + 'random, verbose page rules' => array( '/random/', array( 'random' => '1' ), '/%category%/%postname%/' ), + 'random, date and name' => array( '/random/', array( 'random' => '1' ), '/%year%/%monthnum%/%postname%/' ), + 'random category' => array( '/random/category/news/', array( 'random' => '1', 'category_name' => 'news', ), ), - 'random child category' => array( + 'random child category' => array( '/random/category/news/local/', array( 'random' => '1', 'category_name' => 'news/local', ), ), - 'random tag' => array( + 'random tag' => array( '/random/tag/featured/', array( 'random' => '1', 'tag' => 'featured', ), ), - 'random author' => array( + 'random author' => array( '/random/author/admin/', array( 'random' => '1', 'author_name' => 'admin', ), ), - 'random custom taxonomy' => array( + 'random custom taxonomy' => array( '/random/genre/sci-fi/', array( 'random' => '1', 'wptests_genre' => 'sci-fi', ), ), - 'random post type archive' => array( + 'random post type archive' => array( '/random/books/', array( 'random' => '1', 'post_type' => 'wptests_book', ), ), - 'query string random' => array( '/?random', array( 'random' => '' ) ), - 'query string random value' => array( '/?random=1', array( 'random' => '1' ) ), + 'query string random' => array( '/?random', array( 'random' => '' ) ), + 'query string random value' => array( '/?random=1', array( 'random' => '1' ) ), ); } @@ -1311,29 +1320,6 @@ public static function data_rules_requiring_a_trailing_slash_do_not_take_the_ran ); } - /** - * Ensure a request for the random base matches the root rule. - * - * Random content rules must be matched ahead of the generic post and page rules. - * - * @dataProvider data_permalink_structures - * - * @global WP $wp Current WordPress environment instance. - * - * @param string $structure Permalink structure. - */ - public function test_random_base_request_matches_the_root_rule( string $structure ): void { - global $wp; - - $this->set_permalink_structure( $structure ); - create_initial_taxonomies(); - flush_rewrite_rules(); - - $this->go_to( home_url( '/random/' ) ); - - $this->assertSame( 'random/?$', $wp->matched_rule, 'The random base should be matched by the root rule.' ); - } - /** * Ensure a post type archive at the random base takes precedence over the root rule. * From 359a868ca393b66dd32aea94bc782915746c9456 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 15:25:23 +1100 Subject: [PATCH 09/22] Document preceident of rewrite rules. --- src/wp-includes/class-wp-rewrite.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index c8a250ee0941a..3f41761340c16 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1544,6 +1544,20 @@ public function rewrite_rules() { * the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed * when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}. * + * The `/random/` rule is also omitted if an existing rewrite rule matches the random base, + * for example a post type archive at `/random/`. Generic catch-all rules matching any path, + * such as the page and post rules, are ignored. See the {@see 'wp_rewrite_random_base_is_taken'} + * filter. + * + * Rules added to the top of the rewrite rules, including those for post types and taxonomies, + * take precedence over the random content rules. If a post type or taxonomy uses the random + * base as its rewrite slug, its rules may match random archive URLs before the random content + * rules do. For example, the attachment rules of a post type with the rewrite slug `random` + * match `/random/category/news/`, and the rules of a hierarchical post type or taxonomy match + * every URL beginning with `/random/`. Such random archive URLs display the post type or + * taxonomy content instead. To use random archives on these sites, change the random base on + * the Permalinks screen. + * * The `random` query variable can be added to any other archive or search URL, for example * `/2026/?random`, to request a random post from that archive. * From 726f8b9736b6386e7c639b18b09b604f88b665db Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 9 Nov 2026 15:29:32 +1100 Subject: [PATCH 10/22] Rename single use data providers for their test. --- .../tests/query/randomContentRedirect.php | 48 +++++++++---------- .../tests/rewrite/randomRewriteRules.php | 20 ++++---- tests/phpunit/tests/wp/handleRandom.php | 12 ++--- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index b8195c366d29f..1a6a10da8164e 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -360,7 +360,7 @@ protected function assertQueryIsNotRandomized( WP_Query $query, string $message /** * Ensure random content URLs redirect to a published post. * - * @dataProvider data_random_urls + * @dataProvider data_random_url_redirects_to_a_published_post * * @param string $path Random content request path. */ @@ -375,7 +375,7 @@ public function test_random_url_redirects_to_a_published_post( string $path ): v * * @return array */ - public static function data_random_urls(): array { + public static function data_random_url_redirects_to_a_published_post(): array { return array( 'pretty permalink' => array( '/random/' ), 'pretty without slash' => array( '/random' ), @@ -898,7 +898,7 @@ public function test_random_redirect_ignores_pagination(): void { /** * Ensure unpublished and password protected posts are not selected, even for administrators. * - * @dataProvider data_ineligible_posts + * @dataProvider data_random_redirect_excludes_ineligible_posts * * @param array $args Post arguments. */ @@ -918,7 +918,7 @@ public function test_random_redirect_excludes_ineligible_posts( array $args ): v * * @return array }> */ - public static function data_ineligible_posts(): array { + public static function data_random_redirect_excludes_ineligible_posts(): array { return array( 'draft' => array( array( 'post_status' => 'draft' ) ), 'pending' => array( array( 'post_status' => 'pending' ) ), @@ -1051,7 +1051,7 @@ public function test_no_redirect_for_static_front_page_with_random_query_var_fro /** * Ensure feeds do not redirect with the random query variable. * - * @dataProvider data_feed_urls + * @dataProvider data_no_redirect_for_feeds * * @param string $path Feed path. */ @@ -1067,7 +1067,7 @@ public function test_no_redirect_for_feeds( string $path ): void { * * @return array */ - public static function data_feed_urls(): array { + public static function data_no_redirect_for_feeds(): array { return array( 'site feed' => array( '/feed/?random' ), 'category feed' => array( '/category/arts/feed/?random' ), @@ -1127,7 +1127,7 @@ static function ( string $where, WP_Query $query ): string { /** * Ensure requests using HTTP methods other than GET or HEAD do not redirect. * - * @dataProvider data_non_redirecting_http_methods + * @dataProvider data_no_redirect_for_non_get_requests * * @param string $method HTTP request method. */ @@ -1144,7 +1144,7 @@ public function test_no_redirect_for_non_get_requests( string $method ): void { * * @return array */ - public static function data_non_redirecting_http_methods(): array { + public static function data_no_redirect_for_non_get_requests(): array { return array( 'POST' => array( 'POST' ), 'PUT' => array( 'PUT' ), @@ -1167,7 +1167,7 @@ public function test_no_redirect_without_request_method(): void { /** * Ensure GET and HEAD requests redirect, regardless of case. * - * @dataProvider data_redirecting_http_methods + * @dataProvider data_redirect_for_get_and_head_requests * * @param string $method HTTP request method. */ @@ -1184,7 +1184,7 @@ public function test_redirect_for_get_and_head_requests( string $method ): void * * @return array */ - public static function data_redirecting_http_methods(): array { + public static function data_redirect_for_get_and_head_requests(): array { return array( 'GET' => array( 'GET' ), 'HEAD' => array( 'HEAD' ), @@ -1357,7 +1357,7 @@ public function test_draft_page_named_random_does_not_take_precedence(): void { /** * Ensure random content redirects resume once a page named "random" no longer conflicts. * - * @dataProvider data_page_named_random_changes + * @dataProvider data_random_content_redirects_resume_when_the_page_no_longer_conflicts * * @param callable $change Callback to change the page so it no longer conflicts. */ @@ -1383,7 +1383,7 @@ public function test_random_content_redirects_resume_when_the_page_no_longer_con * * @return array */ - public static function data_page_named_random_changes(): array { + public static function data_random_content_redirects_resume_when_the_page_no_longer_conflicts(): array { return array( 'renamed' => array( static function ( int $page_id ): void { @@ -1474,7 +1474,7 @@ public function test_saving_unrelated_pages_does_not_flush_rewrite_rules(): void } /** - * @dataProvider data_invalid_arguments + * @dataProvider data_invalid_arguments_do_not_flush_rewrite_rules * * @param array $args The arguments to call the function with. */ @@ -1508,7 +1508,7 @@ function ( $arg ) { * * @return array}> */ - public static function data_invalid_arguments(): array { + public static function data_invalid_arguments_do_not_flush_rewrite_rules(): array { return array( 'nulls' => array( array( null, null, false, null ) ), 'strings' => array( array( 'random', 'random', 'yes', 'random' ) ), @@ -2038,7 +2038,7 @@ public function test_random_selection_varies_between_requests(): void { /** * Ensure the random query selects a single post when a theme overrides the limit. * - * @dataProvider data_query_vars_overriding_the_limit + * @dataProvider data_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit * * @param string $path Request path. * @param string $query_var Query variable set by a theme. @@ -2069,7 +2069,7 @@ static function ( WP_Query $query ) use ( $query_var, $value ): void { * * @return array */ - public static function data_query_vars_overriding_the_limit(): array { + public static function data_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit(): array { $overrides = array( 'nopaging' => array( 'nopaging', true ), 'posts_per_archive_page' => array( 'posts_per_archive_page', 10 ), @@ -2252,7 +2252,7 @@ static function ( WP_Query $query ): void { /** * Ensure the enabled filter value is cast to a boolean. * - * @dataProvider data_enabled_filter_values + * @dataProvider data_wp_is_random_content_redirect_enabled_returns_a_boolean * * @param mixed $value Value returned by the filter. * @param bool $expected Expected result. @@ -2273,7 +2273,7 @@ static function () use ( $value ) { * * @return array */ - public static function data_enabled_filter_values(): array { + public static function data_wp_is_random_content_redirect_enabled_returns_a_boolean(): array { return array( 'default true' => array( true, true ), 'false' => array( false, false ), @@ -2445,7 +2445,7 @@ public function test_password_protected_page_named_random_takes_precedence(): vo /** * Ensure paths beneath the random base without random rules do not redirect. * - * @dataProvider data_unsupported_random_paths + * @dataProvider data_no_redirect_for_paths_without_random_rules * * @global WP $wp Current WordPress environment instance. * @@ -2465,7 +2465,7 @@ public function test_no_redirect_for_paths_without_random_rules( string $path ): * * @return array */ - public static function data_unsupported_random_paths(): array { + public static function data_no_redirect_for_paths_without_random_rules(): array { return array( 'pagination' => array( '/random/page/2/' ), 'feed' => array( '/random/feed/' ), @@ -2534,7 +2534,7 @@ public function test_handle_random_runs_the_main_query_for_random_requests(): vo /** * Ensure the main query is only run once for random content requests. * - * @dataProvider data_random_requests + * @dataProvider data_main_query_is_run_once_for_random_requests * * @param string $path Path to request. */ @@ -2552,7 +2552,7 @@ public function test_main_query_is_run_once_for_random_requests( string $path ): * * @return array */ - public static function data_random_requests(): array { + public static function data_main_query_is_run_once_for_random_requests(): array { return array( 'random post' => array( '/random/' ), 'random empty archive' => array( '/random/category/empty/' ), @@ -2623,7 +2623,7 @@ static function ( WP_Query $query ): void { /** * Ensure a post named "random" takes precedence over random content redirects. * - * @dataProvider data_post_name_at_the_root_structures + * @dataProvider data_post_named_random_takes_precedence_over_random_content_redirects * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * @@ -2651,7 +2651,7 @@ public function test_post_named_random_takes_precedence_over_random_content_redi * * @return array */ - public static function data_post_name_at_the_root_structures(): array { + public static function data_post_named_random_takes_precedence_over_random_content_redirects(): array { return array( 'post name' => array( '/%postname%/' ), 'pathinfo' => array( '/index.php/%postname%/' ), diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index 00e2fd77eed93..a30612f5a820b 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -116,7 +116,7 @@ private function get_stored_rewrite_rules(): array { } /** - * @dataProvider data_core_random_rewrite_rules + * @dataProvider data_core_random_rewrite_rules_are_registered * * @param string $regex Expected rewrite rule regex. * @param string $query Expected rewrite rule query. @@ -133,7 +133,7 @@ public function test_core_random_rewrite_rules_are_registered( $regex, $query ): * * @return array */ - public static function data_core_random_rewrite_rules(): array { + public static function data_core_random_rewrite_rules_are_registered(): array { return array( 'any post' => array( 'random/?$', 'index.php?random=1' ), 'category' => array( 'random/category/(.+?)/?$', 'index.php?category_name=$matches[1]&random=1' ), @@ -742,7 +742,7 @@ public static function data_random_rewrite_rules_precede_other_matching_rules(): } /** - * @dataProvider data_random_urls + * @dataProvider data_random_urls_resolve_to_random_query_vars * * @global WP $wp Current WordPress environment instance. * @@ -782,7 +782,7 @@ public function test_random_urls_resolve_to_random_query_vars( $path, $expected_ * * @return array, 2?: string}> */ - public static function data_random_urls(): array { + public static function data_random_urls_resolve_to_random_query_vars(): array { return array( 'random' => array( '/random/', array( 'random' => '1' ) ), 'random without slash' => array( '/random', array( 'random' => '1' ) ), @@ -854,7 +854,7 @@ public function test_random_root_rule_is_omitted_when_a_page_named_random_exists } /** - * @dataProvider data_pages_that_do_not_conflict + * @dataProvider data_random_root_rule_is_kept_for_pages_that_do_not_conflict * * @param array $args Page arguments. A `post_parent` is replaced with a parent page ID. */ @@ -873,7 +873,7 @@ public function test_random_root_rule_is_kept_for_pages_that_do_not_conflict( $a * * @return array}> */ - public static function data_pages_that_do_not_conflict(): array { + public static function data_random_root_rule_is_kept_for_pages_that_do_not_conflict(): array { return array( 'slug starting with random' => array( array( 'post_name' => 'random-is-just-patterns-we-cant-decipher' ) ), 'draft page named random' => array( @@ -1027,7 +1027,7 @@ public static function data_post_permalinks_share_random_base(): array { /** * @covers WP_Rewrite::post_permalinks_share_random_base * - * @dataProvider data_post_name_at_the_root_structures + * @dataProvider data_random_root_rule_is_omitted_when_a_post_named_random_exists * * @param string $structure Permalink structure. */ @@ -1049,7 +1049,7 @@ public function test_random_root_rule_is_omitted_when_a_post_named_random_exists * * @return array */ - public static function data_post_name_at_the_root_structures(): array { + public static function data_random_root_rule_is_omitted_when_a_post_named_random_exists(): array { return array( 'post name' => array( '/%postname%/' ), 'pathinfo' => array( '/index.php/%postname%/' ), @@ -1057,7 +1057,7 @@ public static function data_post_name_at_the_root_structures(): array { } /** - * @dataProvider data_posts_that_do_not_conflict + * @dataProvider data_random_root_rule_is_kept_for_posts_that_do_not_conflict * * @param string $structure Permalink structure. * @param array $args Post arguments. @@ -1076,7 +1076,7 @@ public function test_random_root_rule_is_kept_for_posts_that_do_not_conflict( st * * @return array}> */ - public static function data_posts_that_do_not_conflict(): array { + public static function data_random_root_rule_is_kept_for_posts_that_do_not_conflict(): array { return array( 'blog prefix' => array( '/blog/%postname%/', array() ), 'date and post name' => array( '/%year%/%monthnum%/%postname%/', array() ), diff --git a/tests/phpunit/tests/wp/handleRandom.php b/tests/phpunit/tests/wp/handleRandom.php index 714382d5cf53d..4cf52ff5f459e 100644 --- a/tests/phpunit/tests/wp/handleRandom.php +++ b/tests/phpunit/tests/wp/handleRandom.php @@ -90,7 +90,7 @@ protected function handle_random( array $query_vars ): ?bool { * * @ticket 64498 * - * @dataProvider data_requests_without_the_random_query_var + * @dataProvider data_requests_without_the_random_query_var_are_not_handled * * @param array $query_vars Query variables for the request. */ @@ -107,7 +107,7 @@ public function test_requests_without_the_random_query_var_are_not_handled( arra * * @return array }> */ - public static function data_requests_without_the_random_query_var(): array { + public static function data_requests_without_the_random_query_var_are_not_handled(): array { return array( 'no query vars' => array( array() ), 'other query vars' => array( array( 'post_type' => 'post' ) ), @@ -158,7 +158,7 @@ public function test_random_content_queries_are_not_cached(): void { * * @ticket 64498 * - * @dataProvider data_requests_that_are_not_random_content_requests + * @dataProvider data_main_query_runs_unchanged_for_requests_that_are_not_random_content_requests * * @param array $query_vars Query variables for the request. * @param string $request_method HTTP request method. @@ -182,7 +182,7 @@ public function test_main_query_runs_unchanged_for_requests_that_are_not_random_ * * @return array, 1: non-falsy-string }> */ - public static function data_requests_that_are_not_random_content_requests(): array { + public static function data_main_query_runs_unchanged_for_requests_that_are_not_random_content_requests(): array { return array( 'post request' => array( array( 'random' => '' ), 'POST' ), 'non-randomable without posts' => array( @@ -201,7 +201,7 @@ public static function data_requests_that_are_not_random_content_requests(): arr * @ticket 64498 * @ticket 66625 * - * @dataProvider data_permalinks + * @dataProvider data_request_is_redirected_to_the_permalink_of_the_selected_post * * @param string $permalink Permalink of the selected post. */ @@ -224,7 +224,7 @@ static function () use ( $permalink ): string { * * @return array */ - public static function data_permalinks(): array { + public static function data_request_is_redirected_to_the_permalink_of_the_selected_post(): array { return array( 'permalink on this site' => array( 'http://' . WP_TESTS_DOMAIN . '/post/' ), 'permalink on another site' => array( 'https://elsewhere.example/post/' ), From 0e85cc1dd89e022228d2712aefb97cea92d33f55 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 10 Nov 2026 08:57:25 +1100 Subject: [PATCH 11/22] Revert "Rename single use data providers for their test." This reverts commit a4c7f9b35d4ea40904491bb6bf7602b7d022b3c8. --- .../tests/query/randomContentRedirect.php | 48 +++++++++---------- .../tests/rewrite/randomRewriteRules.php | 20 ++++---- tests/phpunit/tests/wp/handleRandom.php | 12 ++--- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 1a6a10da8164e..b8195c366d29f 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -360,7 +360,7 @@ protected function assertQueryIsNotRandomized( WP_Query $query, string $message /** * Ensure random content URLs redirect to a published post. * - * @dataProvider data_random_url_redirects_to_a_published_post + * @dataProvider data_random_urls * * @param string $path Random content request path. */ @@ -375,7 +375,7 @@ public function test_random_url_redirects_to_a_published_post( string $path ): v * * @return array */ - public static function data_random_url_redirects_to_a_published_post(): array { + public static function data_random_urls(): array { return array( 'pretty permalink' => array( '/random/' ), 'pretty without slash' => array( '/random' ), @@ -898,7 +898,7 @@ public function test_random_redirect_ignores_pagination(): void { /** * Ensure unpublished and password protected posts are not selected, even for administrators. * - * @dataProvider data_random_redirect_excludes_ineligible_posts + * @dataProvider data_ineligible_posts * * @param array $args Post arguments. */ @@ -918,7 +918,7 @@ public function test_random_redirect_excludes_ineligible_posts( array $args ): v * * @return array }> */ - public static function data_random_redirect_excludes_ineligible_posts(): array { + public static function data_ineligible_posts(): array { return array( 'draft' => array( array( 'post_status' => 'draft' ) ), 'pending' => array( array( 'post_status' => 'pending' ) ), @@ -1051,7 +1051,7 @@ public function test_no_redirect_for_static_front_page_with_random_query_var_fro /** * Ensure feeds do not redirect with the random query variable. * - * @dataProvider data_no_redirect_for_feeds + * @dataProvider data_feed_urls * * @param string $path Feed path. */ @@ -1067,7 +1067,7 @@ public function test_no_redirect_for_feeds( string $path ): void { * * @return array */ - public static function data_no_redirect_for_feeds(): array { + public static function data_feed_urls(): array { return array( 'site feed' => array( '/feed/?random' ), 'category feed' => array( '/category/arts/feed/?random' ), @@ -1127,7 +1127,7 @@ static function ( string $where, WP_Query $query ): string { /** * Ensure requests using HTTP methods other than GET or HEAD do not redirect. * - * @dataProvider data_no_redirect_for_non_get_requests + * @dataProvider data_non_redirecting_http_methods * * @param string $method HTTP request method. */ @@ -1144,7 +1144,7 @@ public function test_no_redirect_for_non_get_requests( string $method ): void { * * @return array */ - public static function data_no_redirect_for_non_get_requests(): array { + public static function data_non_redirecting_http_methods(): array { return array( 'POST' => array( 'POST' ), 'PUT' => array( 'PUT' ), @@ -1167,7 +1167,7 @@ public function test_no_redirect_without_request_method(): void { /** * Ensure GET and HEAD requests redirect, regardless of case. * - * @dataProvider data_redirect_for_get_and_head_requests + * @dataProvider data_redirecting_http_methods * * @param string $method HTTP request method. */ @@ -1184,7 +1184,7 @@ public function test_redirect_for_get_and_head_requests( string $method ): void * * @return array */ - public static function data_redirect_for_get_and_head_requests(): array { + public static function data_redirecting_http_methods(): array { return array( 'GET' => array( 'GET' ), 'HEAD' => array( 'HEAD' ), @@ -1357,7 +1357,7 @@ public function test_draft_page_named_random_does_not_take_precedence(): void { /** * Ensure random content redirects resume once a page named "random" no longer conflicts. * - * @dataProvider data_random_content_redirects_resume_when_the_page_no_longer_conflicts + * @dataProvider data_page_named_random_changes * * @param callable $change Callback to change the page so it no longer conflicts. */ @@ -1383,7 +1383,7 @@ public function test_random_content_redirects_resume_when_the_page_no_longer_con * * @return array */ - public static function data_random_content_redirects_resume_when_the_page_no_longer_conflicts(): array { + public static function data_page_named_random_changes(): array { return array( 'renamed' => array( static function ( int $page_id ): void { @@ -1474,7 +1474,7 @@ public function test_saving_unrelated_pages_does_not_flush_rewrite_rules(): void } /** - * @dataProvider data_invalid_arguments_do_not_flush_rewrite_rules + * @dataProvider data_invalid_arguments * * @param array $args The arguments to call the function with. */ @@ -1508,7 +1508,7 @@ function ( $arg ) { * * @return array}> */ - public static function data_invalid_arguments_do_not_flush_rewrite_rules(): array { + public static function data_invalid_arguments(): array { return array( 'nulls' => array( array( null, null, false, null ) ), 'strings' => array( array( 'random', 'random', 'yes', 'random' ) ), @@ -2038,7 +2038,7 @@ public function test_random_selection_varies_between_requests(): void { /** * Ensure the random query selects a single post when a theme overrides the limit. * - * @dataProvider data_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit + * @dataProvider data_query_vars_overriding_the_limit * * @param string $path Request path. * @param string $query_var Query variable set by a theme. @@ -2069,7 +2069,7 @@ static function ( WP_Query $query ) use ( $query_var, $value ): void { * * @return array */ - public static function data_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit(): array { + public static function data_query_vars_overriding_the_limit(): array { $overrides = array( 'nopaging' => array( 'nopaging', true ), 'posts_per_archive_page' => array( 'posts_per_archive_page', 10 ), @@ -2252,7 +2252,7 @@ static function ( WP_Query $query ): void { /** * Ensure the enabled filter value is cast to a boolean. * - * @dataProvider data_wp_is_random_content_redirect_enabled_returns_a_boolean + * @dataProvider data_enabled_filter_values * * @param mixed $value Value returned by the filter. * @param bool $expected Expected result. @@ -2273,7 +2273,7 @@ static function () use ( $value ) { * * @return array */ - public static function data_wp_is_random_content_redirect_enabled_returns_a_boolean(): array { + public static function data_enabled_filter_values(): array { return array( 'default true' => array( true, true ), 'false' => array( false, false ), @@ -2445,7 +2445,7 @@ public function test_password_protected_page_named_random_takes_precedence(): vo /** * Ensure paths beneath the random base without random rules do not redirect. * - * @dataProvider data_no_redirect_for_paths_without_random_rules + * @dataProvider data_unsupported_random_paths * * @global WP $wp Current WordPress environment instance. * @@ -2465,7 +2465,7 @@ public function test_no_redirect_for_paths_without_random_rules( string $path ): * * @return array */ - public static function data_no_redirect_for_paths_without_random_rules(): array { + public static function data_unsupported_random_paths(): array { return array( 'pagination' => array( '/random/page/2/' ), 'feed' => array( '/random/feed/' ), @@ -2534,7 +2534,7 @@ public function test_handle_random_runs_the_main_query_for_random_requests(): vo /** * Ensure the main query is only run once for random content requests. * - * @dataProvider data_main_query_is_run_once_for_random_requests + * @dataProvider data_random_requests * * @param string $path Path to request. */ @@ -2552,7 +2552,7 @@ public function test_main_query_is_run_once_for_random_requests( string $path ): * * @return array */ - public static function data_main_query_is_run_once_for_random_requests(): array { + public static function data_random_requests(): array { return array( 'random post' => array( '/random/' ), 'random empty archive' => array( '/random/category/empty/' ), @@ -2623,7 +2623,7 @@ static function ( WP_Query $query ): void { /** * Ensure a post named "random" takes precedence over random content redirects. * - * @dataProvider data_post_named_random_takes_precedence_over_random_content_redirects + * @dataProvider data_post_name_at_the_root_structures * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * @@ -2651,7 +2651,7 @@ public function test_post_named_random_takes_precedence_over_random_content_redi * * @return array */ - public static function data_post_named_random_takes_precedence_over_random_content_redirects(): array { + public static function data_post_name_at_the_root_structures(): array { return array( 'post name' => array( '/%postname%/' ), 'pathinfo' => array( '/index.php/%postname%/' ), diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index a30612f5a820b..00e2fd77eed93 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -116,7 +116,7 @@ private function get_stored_rewrite_rules(): array { } /** - * @dataProvider data_core_random_rewrite_rules_are_registered + * @dataProvider data_core_random_rewrite_rules * * @param string $regex Expected rewrite rule regex. * @param string $query Expected rewrite rule query. @@ -133,7 +133,7 @@ public function test_core_random_rewrite_rules_are_registered( $regex, $query ): * * @return array */ - public static function data_core_random_rewrite_rules_are_registered(): array { + public static function data_core_random_rewrite_rules(): array { return array( 'any post' => array( 'random/?$', 'index.php?random=1' ), 'category' => array( 'random/category/(.+?)/?$', 'index.php?category_name=$matches[1]&random=1' ), @@ -742,7 +742,7 @@ public static function data_random_rewrite_rules_precede_other_matching_rules(): } /** - * @dataProvider data_random_urls_resolve_to_random_query_vars + * @dataProvider data_random_urls * * @global WP $wp Current WordPress environment instance. * @@ -782,7 +782,7 @@ public function test_random_urls_resolve_to_random_query_vars( $path, $expected_ * * @return array, 2?: string}> */ - public static function data_random_urls_resolve_to_random_query_vars(): array { + public static function data_random_urls(): array { return array( 'random' => array( '/random/', array( 'random' => '1' ) ), 'random without slash' => array( '/random', array( 'random' => '1' ) ), @@ -854,7 +854,7 @@ public function test_random_root_rule_is_omitted_when_a_page_named_random_exists } /** - * @dataProvider data_random_root_rule_is_kept_for_pages_that_do_not_conflict + * @dataProvider data_pages_that_do_not_conflict * * @param array $args Page arguments. A `post_parent` is replaced with a parent page ID. */ @@ -873,7 +873,7 @@ public function test_random_root_rule_is_kept_for_pages_that_do_not_conflict( $a * * @return array}> */ - public static function data_random_root_rule_is_kept_for_pages_that_do_not_conflict(): array { + public static function data_pages_that_do_not_conflict(): array { return array( 'slug starting with random' => array( array( 'post_name' => 'random-is-just-patterns-we-cant-decipher' ) ), 'draft page named random' => array( @@ -1027,7 +1027,7 @@ public static function data_post_permalinks_share_random_base(): array { /** * @covers WP_Rewrite::post_permalinks_share_random_base * - * @dataProvider data_random_root_rule_is_omitted_when_a_post_named_random_exists + * @dataProvider data_post_name_at_the_root_structures * * @param string $structure Permalink structure. */ @@ -1049,7 +1049,7 @@ public function test_random_root_rule_is_omitted_when_a_post_named_random_exists * * @return array */ - public static function data_random_root_rule_is_omitted_when_a_post_named_random_exists(): array { + public static function data_post_name_at_the_root_structures(): array { return array( 'post name' => array( '/%postname%/' ), 'pathinfo' => array( '/index.php/%postname%/' ), @@ -1057,7 +1057,7 @@ public static function data_random_root_rule_is_omitted_when_a_post_named_random } /** - * @dataProvider data_random_root_rule_is_kept_for_posts_that_do_not_conflict + * @dataProvider data_posts_that_do_not_conflict * * @param string $structure Permalink structure. * @param array $args Post arguments. @@ -1076,7 +1076,7 @@ public function test_random_root_rule_is_kept_for_posts_that_do_not_conflict( st * * @return array}> */ - public static function data_random_root_rule_is_kept_for_posts_that_do_not_conflict(): array { + public static function data_posts_that_do_not_conflict(): array { return array( 'blog prefix' => array( '/blog/%postname%/', array() ), 'date and post name' => array( '/%year%/%monthnum%/%postname%/', array() ), diff --git a/tests/phpunit/tests/wp/handleRandom.php b/tests/phpunit/tests/wp/handleRandom.php index 4cf52ff5f459e..714382d5cf53d 100644 --- a/tests/phpunit/tests/wp/handleRandom.php +++ b/tests/phpunit/tests/wp/handleRandom.php @@ -90,7 +90,7 @@ protected function handle_random( array $query_vars ): ?bool { * * @ticket 64498 * - * @dataProvider data_requests_without_the_random_query_var_are_not_handled + * @dataProvider data_requests_without_the_random_query_var * * @param array $query_vars Query variables for the request. */ @@ -107,7 +107,7 @@ public function test_requests_without_the_random_query_var_are_not_handled( arra * * @return array }> */ - public static function data_requests_without_the_random_query_var_are_not_handled(): array { + public static function data_requests_without_the_random_query_var(): array { return array( 'no query vars' => array( array() ), 'other query vars' => array( array( 'post_type' => 'post' ) ), @@ -158,7 +158,7 @@ public function test_random_content_queries_are_not_cached(): void { * * @ticket 64498 * - * @dataProvider data_main_query_runs_unchanged_for_requests_that_are_not_random_content_requests + * @dataProvider data_requests_that_are_not_random_content_requests * * @param array $query_vars Query variables for the request. * @param string $request_method HTTP request method. @@ -182,7 +182,7 @@ public function test_main_query_runs_unchanged_for_requests_that_are_not_random_ * * @return array, 1: non-falsy-string }> */ - public static function data_main_query_runs_unchanged_for_requests_that_are_not_random_content_requests(): array { + public static function data_requests_that_are_not_random_content_requests(): array { return array( 'post request' => array( array( 'random' => '' ), 'POST' ), 'non-randomable without posts' => array( @@ -201,7 +201,7 @@ public static function data_main_query_runs_unchanged_for_requests_that_are_not_ * @ticket 64498 * @ticket 66625 * - * @dataProvider data_request_is_redirected_to_the_permalink_of_the_selected_post + * @dataProvider data_permalinks * * @param string $permalink Permalink of the selected post. */ @@ -224,7 +224,7 @@ static function () use ( $permalink ): string { * * @return array */ - public static function data_request_is_redirected_to_the_permalink_of_the_selected_post(): array { + public static function data_permalinks(): array { return array( 'permalink on this site' => array( 'http://' . WP_TESTS_DOMAIN . '/post/' ), 'permalink on another site' => array( 'https://elsewhere.example/post/' ), From 1e67fe4b18e5426685aa68215f42d594b84e0167 Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:14:00 +1100 Subject: [PATCH 12/22] Expanded test description. Co-authored-by: Aaron Jorbin --- tests/phpunit/tests/query/randomContentRedirect.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index b8195c366d29f..f7b6a48cf5bff 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2866,6 +2866,10 @@ public function test_random_archives_redirect_when_a_post_type_archive_is_at_the /** * Ensure the random base is not redirected to random content when an existing rule takes it. * + * The dataProvider supplies a top-priority rewrite rule, each one a different regex spelling that + * matches the path `/random` and points to a specific `page`. This ensures that rewrite rule + * takes priority over the random ones. + * * @dataProvider data_random_base_is_not_redirected_when_an_existing_rule_takes_it * * @param string $regex Rewrite rule regex. From be1bfbc88e15ed80c7a16a38312a512feb8b103b Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:15:01 +1100 Subject: [PATCH 13/22] Fix docblock type. Co-authored-by: Aaron Jorbin --- src/wp-includes/class-wp-rewrite.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 3f41761340c16..9ed414def49b8 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1565,11 +1565,11 @@ public function rewrite_rules() { * * @see wp_is_random_content_redirect_enabled() * - * @param array|null $existing_rules Optional. Existing rewrite rules to check for rules using the - * random base, keyed by their regex pattern. Generic catch-all - * rules, such as the page and post rules, should not be included - * as they match any path. Default null, the extra rules added - * via WP_Rewrite::add_rule() and WP_Rewrite::add_permastruct(). + * @param string[]|null $existing_rules Optional. Existing rewrite rules to check for rules using the + * random base, keyed by their regex pattern. Generic catch-all + * rules, such as the page and post rules, should not be included + * as they match any path. Default null, the extra rules added + * via WP_Rewrite::add_rule() and WP_Rewrite::add_permastruct(). * @return array Array of rewrite rules for random content redirects, keyed by their regex pattern. * * @phpstan-return array From 583f0915f5891ecad8203abeb4c8e24eb422426e Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 10 Nov 2026 14:44:55 +1100 Subject: [PATCH 14/22] Is @aaronjorbin trolling? --- tests/phpunit/tests/query/randomContentRedirect.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index f7b6a48cf5bff..918f57d78ec38 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2866,8 +2866,8 @@ public function test_random_archives_redirect_when_a_post_type_archive_is_at_the /** * Ensure the random base is not redirected to random content when an existing rule takes it. * - * The dataProvider supplies a top-priority rewrite rule, each one a different regex spelling that - * matches the path `/random` and points to a specific `page`. This ensures that rewrite rule + * The dataProvider supplies a top-priority rewrite rule, each one a different regex spelling that + * matches the path `/random` and points to a specific `page`. This ensures that rewrite rule * takes priority over the random ones. * * @dataProvider data_random_base_is_not_redirected_when_an_existing_rule_takes_it From af4dea1637d435b6a9e8429c14ea7608bd70076b Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 10 Nov 2026 14:47:20 +1100 Subject: [PATCH 15/22] Exclude bottom rules. --- src/wp-includes/class-wp-rewrite.php | 17 ++++++++--- .../tests/query/randomContentRedirect.php | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 9ed414def49b8..3ccb9b31f61c6 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1476,9 +1476,14 @@ public function rewrite_rules() { * random base. The page and post rules are generic catch-all rules matching * any path, so are not checked. Pages and posts at the random base are * accounted for separately by WP_Rewrite::random_rewrite_rules(). + * + * Rules added to the bottom are not checked either. The page and post rules + * precede them, so they are never matched for the random base. A bottom rule + * using the same regex as the random root rule replaces its query instead, + * taking its position ahead of the page and post rules. */ $random_rewrite = $this->random_rewrite_rules( - array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $this->extra_rules ) + array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite ) ); /** @@ -1568,8 +1573,9 @@ public function rewrite_rules() { * @param string[]|null $existing_rules Optional. Existing rewrite rules to check for rules using the * random base, keyed by their regex pattern. Generic catch-all * rules, such as the page and post rules, should not be included - * as they match any path. Default null, the extra rules added - * via WP_Rewrite::add_rule() and WP_Rewrite::add_permastruct(). + * as they match any path. Nor should rules added to the bottom, + * as the page and post rules precede them. Default null, the + * rules added to the top via WP_Rewrite::add_rule(). * @return array Array of rewrite rules for random content redirects, keyed by their regex pattern. * * @phpstan-return array @@ -1608,8 +1614,11 @@ public function random_rewrite_rules( ?array $existing_rules = null ): array { * The generic catch-all page and post rules match any path, including the * random base, so are excluded from the existing rules. Pages and posts at * the random base, which these rules serve, are accounted for above. + * + * Rules added to the bottom are excluded as the page and post rules precede + * them, see WP_Rewrite::rewrite_rules(). */ - $existing_rules ??= array_merge( $this->extra_rules_top, $this->extra_rules ); + $existing_rules ??= $this->extra_rules_top; foreach ( $existing_rules as $regex => $query ) { /* diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 918f57d78ec38..49c0c8f10f34a 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2950,9 +2950,38 @@ public static function data_random_base_redirects_when_existing_rules_do_not_tak 'requires a sub path' => array( '^random/(.*)?', 'top' ), 'catch-all' => array( '(.+?)/?$', 'bottom' ), 'single segment catch-all' => array( '([^/]+)/?$', 'bottom' ), + 'bottom anchored match' => array( '^random/?$', 'bottom' ), + 'bottom alternation' => array( '(random|pens|pencils)/?$', 'bottom' ), + 'bottom unterminated' => array( 'random', 'bottom' ), ); } + /** + * Ensure a bottom rule using the random root rule's regex takes the random base. + * + * Rules added to the bottom are preceded by the generic page and post rules, so are + * never matched for the random base. A bottom rule using the same regex as the random + * root rule replaces the root rule's query while keeping its position ahead of the + * page and post rules, so takes the random base. + */ + public function test_bottom_rule_using_the_random_root_rule_regex_takes_the_random_base(): void { + $page = $this->create_post( + array( + 'post_type' => 'page', + 'post_name' => 'wptests-taken', + ) + ); + + add_rewrite_rule( 'random/?$', 'index.php?pagename=wptests-taken', 'bottom' ); + flush_rewrite_rules(); + + $this->go_to( home_url( '/random/' ) ); + + $this->assertNotRedirected( 'A bottom rule using the root rule regex should not redirect to random content.' ); + $this->assertQueryTrue( 'is_page', 'is_singular' ); + $this->assertSame( $page, get_queried_object_id(), 'The page served by the bottom rule using the root rule regex should be queried.' ); + } + /** * Ensure a category at the random base with the category base removed is not redirected to random content. * From b3e0593b7dcdb3c7913765788e8341db4575492a Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 10 Nov 2026 15:15:56 +1100 Subject: [PATCH 16/22] KISS. --- src/wp-includes/class-wp-rewrite.php | 110 ++---- .../tests/rewrite/randomRewriteRules.php | 344 ++++-------------- 2 files changed, 87 insertions(+), 367 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 3ccb9b31f61c6..ae0d20710ba6c 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1469,22 +1469,20 @@ public function rewrite_rules() { $this->extra_rules_top = array_merge( $this->extra_rules_top, $rules ); } + // Random content rewrite rules. + $random_rewrite = $this->random_rewrite_rules(); + /* - * Random content rewrite rules. - * - * The random content rules check whether an existing rule already uses the - * random base. The page and post rules are generic catch-all rules matching - * any path, so are not checked. Pages and posts at the random base are - * accounted for separately by WP_Rewrite::random_rewrite_rules(). + * Rules added to the top, including those for post types and taxonomies, take + * precedence over random content rules using the same regex, for example a post + * type archive at `/random/`. * - * Rules added to the bottom are not checked either. The page and post rules - * precede them, so they are never matched for the random base. A bottom rule - * using the same regex as the random root rule replaces its query instead, - * taking its position ahead of the page and post rules. + * Rules added to the top are merged ahead of the random content rules, so without + * this, the random content rule's query would replace the query of a rule using the + * same regex. Rules added to the top using a different regex for the same path, + * such as `^random/?$`, already take precedence as they are matched first. */ - $random_rewrite = $this->random_rewrite_rules( - array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite ) - ); + $random_rewrite = array_diff_key( $random_rewrite, $this->extra_rules_top ); /** * Filters rewrite rules used for random content redirects. @@ -1549,19 +1547,21 @@ public function rewrite_rules() { * the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed * when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}. * - * The `/random/` rule is also omitted if an existing rewrite rule matches the random base, - * for example a post type archive at `/random/`. Generic catch-all rules matching any path, - * such as the page and post rules, are ignored. See the {@see 'wp_rewrite_random_base_is_taken'} + * Whether the `/random/` rule is omitted can be changed with the {@see 'wp_rewrite_random_base_is_taken'} * filter. * * Rules added to the top of the rewrite rules, including those for post types and taxonomies, - * take precedence over the random content rules. If a post type or taxonomy uses the random - * base as its rewrite slug, its rules may match random archive URLs before the random content - * rules do. For example, the attachment rules of a post type with the rewrite slug `random` - * match `/random/category/news/`, and the rules of a hierarchical post type or taxonomy match - * every URL beginning with `/random/`. Such random archive URLs display the post type or - * taxonomy content instead. To use random archives on these sites, change the random base on - * the Permalinks screen. + * take precedence over the random content rules. When the rewrite rules are generated, random + * content rules using the same regex as a rule added to the top are omitted, see + * {@see WP_Rewrite::rewrite_rules()}. For example, a post type archive at `/random/` replaces + * the `/random/` rule. + * + * If a post type or taxonomy uses the random base as its rewrite slug, its rules may match + * random archive URLs before the random content rules do. For example, the attachment rules + * of a post type with the rewrite slug `random` match `/random/category/news/`, and the rules + * of a hierarchical post type or taxonomy match every URL beginning with `/random/`. Such + * random archive URLs display the post type or taxonomy content instead. To use random + * archives on these sites, change the random base on the Permalinks screen. * * The `random` query variable can be added to any other archive or search URL, for example * `/2026/?random`, to request a random post from that archive. @@ -1570,24 +1570,17 @@ public function rewrite_rules() { * * @see wp_is_random_content_redirect_enabled() * - * @param string[]|null $existing_rules Optional. Existing rewrite rules to check for rules using the - * random base, keyed by their regex pattern. Generic catch-all - * rules, such as the page and post rules, should not be included - * as they match any path. Nor should rules added to the bottom, - * as the page and post rules precede them. Default null, the - * rules added to the top via WP_Rewrite::add_rule(). * @return array Array of rewrite rules for random content redirects, keyed by their regex pattern. * * @phpstan-return array */ - public function random_rewrite_rules( ?array $existing_rules = null ): array { + public function random_rewrite_rules(): array { if ( ! wp_is_random_content_redirect_enabled() ) { return array(); } - $random_slug = $this->get_random_base(); - $random_base = $this->root . $random_slug; - $catch_all_test_base = "{$this->root}catchall{$random_slug}"; // No hyphens to account for [A-z0-9] rules. + $random_slug = $this->get_random_base(); + $random_base = $this->root . $random_slug; $rules = array(); @@ -1602,57 +1595,6 @@ public function random_rewrite_rules( ?array $existing_rules = null ): array { fn( $random_base_post ) => $random_base_post instanceof WP_Post && is_post_publicly_viewable( $random_base_post ) ); - if ( ! $random_base_is_taken ) { - /* - * WP::parse_request() trims slashes from the requested path before matching - * it against the rules, so the path is matched without a trailing slash - * regardless of the permalink structure. - */ - $random_base_path = untrailingslashit( $random_base ); - - /* - * The generic catch-all page and post rules match any path, including the - * random base, so are excluded from the existing rules. Pages and posts at - * the random base, which these rules serve, are accounted for above. - * - * Rules added to the bottom are excluded as the page and post rules precede - * them, see WP_Rewrite::rewrite_rules(). - */ - $existing_rules ??= $this->extra_rules_top; - - foreach ( $existing_rules as $regex => $query ) { - /* - * Match the same way WP::parse_request() does, so the regex is anchored to - * the start of the path whether or not it includes a leading caret. - * - * Matching the regex rather than comparing strings accounts for rules - * written in any form, such as `random/?$`, `^random/?$`, `(random|pens)/?$`, - * `(rand)om/?$` or `[r]andom/?$`, while rules for similarly named paths, - * such as `randomful/?$` or `random-ful/?$`, do not match. - * - * A rule requiring a trailing slash, such as `^random/(.*)?`, never matches - * a request for the random base, so it does not take it. - */ - $pattern = "#^{$regex}#"; - if ( ! preg_match( $pattern, $random_base_path ) ) { - continue; - } - - /* - * Generic catch-all rules, such as `(.+?)/?$` or `([^/]+)/?$`, match any - * path including the random base. A rule also matching a path other than - * the random base, for example `notrandom`, is a catch-all rule so does - * not take the random base. - */ - if ( preg_match( $pattern, $catch_all_test_base ) ) { - continue; - } - - $random_base_is_taken = true; - break; - } - } - /** * Filters whether the random content redirect base is taken. * diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index 00e2fd77eed93..b0e02ba11c976 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -1094,82 +1094,24 @@ public static function data_posts_that_do_not_conflict(): array { } /** - * Ensure the root rule is kept when the rules are regenerated. + * Ensure random rules using the same regex as a rule added to the top are omitted. * - * Once generated, the root rule is in the existing rules and must not be treated - * as taking the random base itself. + * Rules added to the top are merged ahead of the random rules, so a random rule using + * the same regex would otherwise replace the top rule's query. * - * @global WP_Rewrite $wp_rewrite WordPress rewrite component. - */ - public function test_random_root_rule_is_kept_when_the_rules_are_regenerated(): void { - global $wp_rewrite; - - $this->get_random_rewrite_rules(); - $this->assertArrayHasKey( 'random/?$', $wp_rewrite->rules, 'The root rule should be in the generated rules.' ); - - $rules = $wp_rewrite->random_rewrite_rules(); - - $this->assertArrayHasKey( 'random/?$', $rules, 'The root rule should be kept when the rules are regenerated.' ); - } - - /** - * Ensure the root rule is omitted when an existing rule takes the random base. + * @dataProvider data_random_rules_using_the_regex_of_a_top_rule_are_omitted * - * @dataProvider data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base - * - * @param string $regex Rewrite rule regex. + * @param string $regex Rewrite rule regex. + * @param string $random_rule A random rule using a different regex, expected to be kept. */ - public function test_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base( string $regex ): void { + public function test_random_rules_using_the_regex_of_a_top_rule_are_omitted( string $regex, string $random_rule ): void { add_rewrite_rule( $regex, 'index.php?pagename=wptests-taken', 'top' ); - $rules = $this->get_random_rewrite_rules(); - - $this->assertArrayNotHasKey( 'random/?$', $rules, "The root rule should be omitted when the rule {$regex} takes the random base." ); - $this->assertArrayHasKey( 'random/category/(.+?)/?$', $rules, 'Random archive rules should be kept when the random base is taken.' ); - $this->assertArrayNotHasKey( 'random/?$', $this->filter_random_rules( $this->get_stored_rewrite_rules() ), 'The stored rules should not include the root rule.' ); - } - - /** - * Data provider. - * - * @return array - */ - public static function data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base(): array { - return array( - 'exact match' => array( 'random/?$' ), - 'anchored exact match' => array( '^random/?$' ), - 'unterminated' => array( 'random' ), - 'optional suffix' => array( 'random(ful)?/?$' ), - 'optional non-capturing suffix' => array( 'random(?:-ful)?/?$' ), - 'anchored optional suffix' => array( '^random(?:-ful)?/?$' ), - 'alternation, first' => array( '(random|pens|pencils)/?$' ), - 'alternation, last' => array( '(pens|pencils|random)/?$' ), - 'anchored alternation' => array( '^(random|pens|pencils)/?$' ), - 'non-capturing alternation' => array( '(?:pens|random)/?$' ), - 'partial group' => array( '(rand)om/?$' ), - 'anchored partial group' => array( '^(rand)om/?$' ), - 'group in the middle' => array( 'r(and)om/?$' ), - 'character class' => array( '[r]andom/?$' ), - 'character class range' => array( '[a-z]andom/?$' ), - 'wildcard' => array( '.andom/?$' ), - 'optional final character' => array( 'random?/?$' ), - ); - } - - /** - * Ensure the root rule is kept when existing rules do not take the random base. - * - * @dataProvider data_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base - * - * @param string $regex Rewrite rule regex. - */ - public function test_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base( string $regex ): void { - add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', 'top' ); - - $rules = $this->get_random_rewrite_rules(); + $this->get_random_rewrite_rules(); + $rules = $this->get_stored_rewrite_rules(); - $this->assertArrayHasKey( 'random/?$', $rules, "The root rule should be kept when the rule {$regex} does not take the random base." ); - $this->assertSame( 'index.php?random=1', $rules['random/?$'], 'The root rule should set the random query variable.' ); + $this->assertSame( 'index.php?pagename=wptests-taken', $rules[ $regex ] ?? null, "The top rule {$regex} should take precedence over the random rule using the same regex." ); + $this->assertStringContainsString( 'random=1', $rules[ $random_rule ] ?? '', "The random rule {$random_rule} should be kept when the top rule {$regex} uses a different regex." ); } /** @@ -1177,147 +1119,40 @@ public function test_random_root_rule_is_kept_when_existing_rules_do_not_take_th * * @return array */ - public static function data_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base(): array { + public static function data_random_rules_using_the_regex_of_a_top_rule_are_omitted(): array { return array( - 'suffixed slug' => array( 'randomful/?$' ), - 'hyphenated suffixed slug' => array( 'random-ful/?$' ), - 'anchored suffixed slug' => array( '^randomful/?$' ), - 'anchored hyphenated slug' => array( '^random-ful/?$' ), - 'suffixed slug with sub path' => array( '^randomful/(.*)?' ), - 'hyphenated slug feed' => array( 'random-ful/feed/(feed|rdf|rss|rss2|atom)/?$' ), - 'required suffix' => array( 'random(ful|ness)/?$' ), - 'prefixed slug' => array( 'not-random/?$' ), - 'anchored prefixed slug' => array( '^not-random/?$' ), - 'sub path of the random base' => array( 'random/wptests/?$' ), - 'catch-all' => array( '(.+?)/?$' ), - 'anchored catch-all' => array( '^(.+?)/?$' ), - 'single segment catch-all' => array( '([^/]+)/?$' ), - 'letters only catch-all' => array( '([a-z]+)/?$' ), - 'page catch-all' => array( '(.?.+?)(?:/([0-9]+))?/?$' ), - 'alternation without random' => array( '(pens|pencils)/?$' ), - 'alternation of similar slugs' => array( '(randomful|random-ful)/?$' ), - 'partial group, suffixed' => array( '(rand)omful/?$' ), - 'character class, suffixed' => array( '[r]andomful/?$' ), - ); - } - - /** - * Ensure a request for the random base is a random content request when existing rules do not take the random base. - * - * @dataProvider data_random_base_request_matches_the_root_rule_when_existing_rules_do_not_take_the_random_base - * - * @global WP $wp Current WordPress environment instance. - * @global WP_Rewrite $wp_rewrite WordPress rewrite component. - * - * @param string $regex Rewrite rule regex. - * @param string $position Position of the rewrite rule, either 'top' or 'bottom'. - * @param string $structure Permalink structure. - * @param string $home_path Path of the site's home URL, for sites installed in a subdirectory. - */ - public function test_random_base_request_matches_the_root_rule_when_existing_rules_do_not_take_the_random_base( string $regex, string $position, string $structure, string $home_path ): void { - global $wp, $wp_rewrite; - - if ( '' !== $home_path ) { - update_option( 'home', untrailingslashit( home_url() ) . $home_path ); - } - - $this->set_permalink_structure( $structure ); - create_initial_taxonomies(); - - add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', $position ); - flush_rewrite_rules(); - - $this->go_to( home_url( user_trailingslashit( $wp_rewrite->root . 'random' ) ) ); - - $this->assertSame( $wp_rewrite->root . 'random/?$', $wp->matched_rule, "A request for the random base should match the root rule rather than {$wp->matched_rule}." ); - $this->assertSame( '1', $wp->query_vars['random'] ?? null, 'The random query variable should be set for a request for the random base.' ); - } - - /** - * Data provider. - * - * Each rule is tested with each permalink structure. For PATHINFO permalink structures, - * each rule is also tested prefixed with `index.php/`, as rules for such sites usually are. - * - * Catch-all rules are added to the bottom of the rules. A catch-all rule added to the top - * takes precedence over all of the rules WordPress generates, including the random content - * rules, so matches every request, as intended by the 'top' position. - * - * @return array - */ - public static function data_random_base_request_matches_the_root_rule_when_existing_rules_do_not_take_the_random_base(): array { - $structures = array( - 'post name' => array( '/%postname%/', '' ), - 'no trailing slash' => array( '/%postname%', '' ), - 'verbose page rules' => array( '/%category%/%postname%/', '' ), - 'day and name' => array( '/%year%/%monthnum%/%day%/%postname%/', '' ), - 'numeric' => array( '/archives/%post_id%', '' ), - 'subdirectory install' => array( '/%postname%/', '/wptests-subdirectory' ), - 'pathinfo post name' => array( '/index.php/%postname%/', '' ), - 'pathinfo day and name' => array( '/index.php/%year%/%monthnum%/%day%/%postname%/', '' ), - 'pathinfo no trailing' => array( '/index.php/%postname%', '' ), + 'root rule' => array( 'random/?$', 'random/category/(.+?)/?$' ), + 'category archive rule' => array( 'random/category/(.+?)/?$', 'random/?$' ), ); - - $data = array(); - foreach ( self::data_random_root_rule_is_kept_when_existing_rules_do_not_take_the_random_base() as $rule_name => $rule ) { - $position = str_contains( $rule_name, 'catch-all' ) ? 'bottom' : 'top'; - - foreach ( $structures as $structure_name => $structure ) { - $data[ "{$rule_name}, {$structure_name}" ] = array( $rule[0], $position, $structure[0], $structure[1] ); - - if ( str_starts_with( $structure[0], '/index.php/' ) ) { - $prefixed_rule = ( str_starts_with( $rule[0], '^' ) ? '^' : '' ) . 'index.php/' . ltrim( $rule[0], '^' ); - - $data[ "{$rule_name} prefixed with index.php, {$structure_name}" ] = array( $prefixed_rule, $position, $structure[0], $structure[1] ); - } - } - } - - return $data; } /** - * Ensure a rule requiring a trailing slash does not take the random base. - * - * This applies regardless of whether the permalink structure uses a trailing slash. - * WP::parse_request() trims slashes from the requested path, so such a rule never - * matches a request for the random base. + * Ensure random rules are kept when a rule added to the top uses a different regex for the random base. * - * @dataProvider data_rules_requiring_a_trailing_slash_do_not_take_the_random_base + * Such rules are matched before the random rules, so take precedence without the random rules being omitted. * * @global WP $wp Current WordPress environment instance. - * - * @param string $structure Permalink structure. - * @param string $path Request path for the random base. */ - public function test_rules_requiring_a_trailing_slash_do_not_take_the_random_base( string $structure, string $path ): void { + public function test_random_root_rule_is_kept_when_a_top_rule_uses_a_different_regex_for_the_random_base(): void { global $wp; - $this->set_permalink_structure( $structure ); - create_initial_taxonomies(); - - add_rewrite_rule( '^random/(.*)?', 'index.php?pagename=wptests-not-taken', 'top' ); + $page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'wptests-taken', + ) + ); - $rules = $this->get_random_rewrite_rules(); + add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); - $this->assertArrayHasKey( 'random/?$', $rules, 'The root rule should be kept for a rule requiring a trailing slash.' ); + $this->get_random_rewrite_rules(); - $this->go_to( home_url( $path ) ); + $this->assertSame( 'index.php?random=1', $this->get_stored_rewrite_rules()['random/?$'] ?? null, 'The root rule should be kept when a top rule uses a different regex.' ); - $this->assertSame( 'random/?$', $wp->matched_rule, 'The random base should be matched by the root rule.' ); - $this->assertSame( '1', $wp->query_vars['random'] ?? null, 'The random query variable should be set.' ); - } + $this->go_to( home_url( '/random/' ) ); - /** - * Data provider. - * - * @return array - */ - public static function data_rules_requiring_a_trailing_slash_do_not_take_the_random_base(): array { - return array( - 'trailing slash' => array( '/%postname%/', '/random/' ), - 'no trailing slash' => array( '/%postname%', '/random' ), - ); + $this->assertSame( '^random/?$', $wp->matched_rule, 'The top rule should be matched before the root rule.' ); + $this->assertSame( $page, get_queried_object_id(), 'The page served by the top rule should be queried.' ); } /** @@ -1331,9 +1166,8 @@ public static function data_rules_requiring_a_trailing_slash_do_not_take_the_ran public function test_random_root_rule_is_omitted_for_a_post_type_archive_at_the_random_base( string $post_type, array $args ): void { register_post_type( $post_type, $args ); - $rules = $this->get_random_rewrite_rules(); + $this->get_random_rewrite_rules(); - $this->assertArrayNotHasKey( 'random/?$', $rules, 'The root rule should be omitted for a post type archive at the random base.' ); $this->assertSame( "index.php?post_type={$post_type}", $this->get_stored_rewrite_rules()['random/?$'] ?? null, 'The post type archive should be reachable at the random base.' ); } @@ -1356,7 +1190,6 @@ public static function data_random_root_rule_is_omitted_for_a_post_type_archive_ array( 'public' => true, 'has_archive' => 'random', - 'rewrite' => array( 'slug' => 'random' ), ), ), 'rewrite slug random' => array( @@ -1386,9 +1219,9 @@ public function test_random_root_rule_is_kept_for_a_post_type_archive_with_a_sim ) ); - $rules = $this->get_random_rewrite_rules(); + $this->get_random_rewrite_rules(); - $this->assertArrayHasKey( 'random/?$', $rules, "The root rule should be kept for a post type archive at {$slug}." ); + $this->assertSame( 'index.php?random=1', $this->get_stored_rewrite_rules()['random/?$'] ?? null, "The root rule should be kept for a post type archive at {$slug}." ); } /** @@ -1405,97 +1238,49 @@ public static function data_random_root_rule_is_kept_for_a_post_type_archive_wit } /** - * Ensure the root rule is omitted when an existing rule takes a custom random base. + * Ensure the root rule is omitted when a top rule uses its regex with a custom random base. * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ - public function test_random_root_rule_is_omitted_when_an_existing_rule_takes_a_custom_random_base(): void { + public function test_random_root_rule_is_omitted_when_a_top_rule_uses_its_regex_with_a_custom_random_base(): void { global $wp_rewrite; $wp_rewrite->random_base = 'surprise-me'; - add_rewrite_rule( '^surprise-me/?$', 'index.php?pagename=wptests-taken', 'top' ); + add_rewrite_rule( 'surprise-me/?$', 'index.php?pagename=wptests-taken', 'top' ); - $rules = $this->get_random_rewrite_rules(); + $this->get_random_rewrite_rules(); - $this->assertArrayNotHasKey( 'surprise-me/?$', $rules, 'The root rule should be omitted when a rule takes the custom random base.' ); + $this->assertSame( 'index.php?pagename=wptests-taken', $this->get_stored_rewrite_rules()['surprise-me/?$'] ?? null, 'The top rule should take precedence over the root rule for the custom random base.' ); } /** - * Ensure a rule at the default random base does not take a custom random base. + * Ensure a top rule at the default random base does not replace the root rule for a custom random base. * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ - public function test_random_root_rule_is_kept_when_an_existing_rule_uses_the_default_base_with_a_custom_random_base(): void { + public function test_random_root_rule_is_kept_when_a_top_rule_uses_the_default_base_with_a_custom_random_base(): void { global $wp_rewrite; $wp_rewrite->random_base = 'surprise-me'; - add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); - - $rules = $this->get_random_rewrite_rules(); - - $this->assertArrayHasKey( 'surprise-me/?$', $rules, 'The root rule should be kept when a rule only uses the default random base.' ); - } + add_rewrite_rule( 'random/?$', 'index.php?pagename=wptests-taken', 'top' ); - /** - * Ensure the root rule is omitted when an existing rule takes the random base with PATHINFO permalinks. - * - * @dataProvider data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base_with_pathinfo_permalinks - * - * @param string $regex Rewrite rule regex. - */ - public function test_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base_with_pathinfo_permalinks( string $regex ): void { - $this->set_permalink_structure( '/index.php/%postname%/' ); - create_initial_taxonomies(); - - add_rewrite_rule( $regex, 'index.php?pagename=wptests-taken', 'top' ); - - $rules = $this->get_random_rewrite_rules(); - - $this->assertArrayNotHasKey( 'index.php/random/?$', $rules, "The root rule should be omitted when the rule {$regex} takes the random base." ); - } + $this->get_random_rewrite_rules(); - /** - * Data provider. - * - * @return array - */ - public static function data_random_root_rule_is_omitted_when_an_existing_rule_takes_the_random_base_with_pathinfo_permalinks(): array { - return array( - 'exact match' => array( 'index.php/random/?$' ), - 'anchored exact match' => array( '^index.php/random/?$' ), - ); + $this->assertSame( 'index.php?random=1', $this->get_stored_rewrite_rules()['surprise-me/?$'] ?? null, 'The root rule for the custom random base should be kept when a top rule only uses the default random base.' ); } /** - * Ensure the root rule is kept when an existing catch-all rule matches the random base with PATHINFO permalinks. - * - * @dataProvider data_random_root_rule_is_kept_when_an_existing_catch_all_rule_matches_the_random_base_with_pathinfo_permalinks - * - * @param string $regex Rewrite rule regex. + * Ensure the root rule is omitted when a top rule uses its regex with PATHINFO permalinks. */ - public function test_random_root_rule_is_kept_when_an_existing_catch_all_rule_matches_the_random_base_with_pathinfo_permalinks( string $regex ): void { + public function test_random_root_rule_is_omitted_when_a_top_rule_uses_its_regex_with_pathinfo_permalinks(): void { $this->set_permalink_structure( '/index.php/%postname%/' ); create_initial_taxonomies(); - add_rewrite_rule( $regex, 'index.php?pagename=wptests-not-taken', 'top' ); - - $rules = $this->get_random_rewrite_rules(); + add_rewrite_rule( 'index.php/random/?$', 'index.php?pagename=wptests-taken', 'top' ); - $this->assertArrayHasKey( 'index.php/random/?$', $rules, "The root rule should be kept when the catch-all rule {$regex} matches the random base." ); - } + $this->get_random_rewrite_rules(); - /** - * Data provider. - * - * @return array - */ - public static function data_random_root_rule_is_kept_when_an_existing_catch_all_rule_matches_the_random_base_with_pathinfo_permalinks(): array { - return array( - 'catch-all' => array( 'index.php/(.+?)/?$' ), - 'anchored catch-all' => array( '^index.php/(.+?)/?$' ), - 'single segment catch-all' => array( 'index.php/([^/]+)/?$' ), - 'page catch-all' => array( 'index.php/(.?.+?)(?:/([0-9]+))?/?$' ), - ); + $this->assertSame( 'index.php?pagename=wptests-taken', $this->get_stored_rewrite_rules()['index.php/random/?$'] ?? null, 'The top rule should take precedence over the root rule with PATHINFO permalinks.' ); } /** @@ -1511,22 +1296,32 @@ public function test_random_base_is_taken_filter_can_omit_the_root_rule(): void } /** - * Ensure the random base is taken filter can keep the root rule when a rule takes the random base. + * Ensure the random base is taken filter can keep the root rule when a page named "random" exists. */ public function test_random_base_is_taken_filter_can_keep_the_root_rule(): void { - add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'random', + ) + ); add_filter( 'wp_rewrite_random_base_is_taken', '__return_false' ); $rules = $this->get_random_rewrite_rules(); - $this->assertArrayHasKey( 'random/?$', $rules, 'The filter should be able to keep the root rule when a rule takes the random base.' ); + $this->assertArrayHasKey( 'random/?$', $rules, 'The filter should be able to keep the root rule when a page named random exists.' ); } /** * Ensure the random base is taken filter receives the detected value, the random base and the random slug. */ public function test_random_base_is_taken_filter_receives_the_detected_value_and_random_base(): void { - add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'random', + ) + ); $filter = new MockAction(); add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 3 ); @@ -1554,21 +1349,4 @@ public function test_random_base_is_taken_filter_receives_the_random_base_and_sl $this->assertNotEmpty( $args, 'The filter was not called with PATHINFO permalinks.' ); $this->assertSame( array( false, 'index.php/random', 'random' ), end( $args ), 'The filter should receive the random base with the PATHINFO prefix and the random slug without it.' ); } - - /** - * Filters rewrite rules to those setting the random query variable. - * - * @param array $rules Rewrite rules, keyed by their regex pattern. - * @return array Random content rewrite rules, keyed by their regex pattern. - */ - private function filter_random_rules( array $rules ): array { - return array_filter( - $rules, - static function ( $query ): bool { - parse_str( (string) wp_parse_url( (string) $query, PHP_URL_QUERY ), $query_vars ); - - return isset( $query_vars['random'] ); - } - ); - } } From c09bab2bef464876624cfbc2935e2e7ea3af4504 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 10 Nov 2026 15:25:56 +1100 Subject: [PATCH 17/22] Remove duplicate filter test. --- .../tests/query/randomContentRedirect.php | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 49c0c8f10f34a..7992bd4cb37db 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2999,28 +2999,4 @@ public function test_category_at_the_random_base_without_a_category_base_is_not_ $this->assertQueryTrue( 'is_archive', 'is_category' ); $this->assertSame( $category, get_queried_object_id(), 'The category at the random base should be queried.' ); } - - /** - * Ensure the random base is not redirected to random content when filtered as taken. - */ - public function test_random_base_is_not_redirected_when_filtered_as_taken(): void { - add_filter( 'wp_rewrite_random_base_is_taken', '__return_true' ); - flush_rewrite_rules(); - - $this->go_to( home_url( '/random/' ) ); - - $this->assertNotRedirected( 'The random base should not redirect to random content when filtered as taken.' ); - } - - /** - * Ensure random archives redirect when the random base is filtered as taken. - */ - public function test_random_archives_redirect_when_the_random_base_is_filtered_as_taken(): void { - add_filter( 'wp_rewrite_random_base_is_taken', '__return_true' ); - flush_rewrite_rules(); - - $this->go_to( home_url( '/random/category/arts/' ) ); - - $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'Random archives should redirect when the random base is filtered as taken.' ); - } } From f381dc145b5337c81d5b19dcf53e27d11722f362 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 11 Nov 2026 09:21:27 +1100 Subject: [PATCH 18/22] Remove new filter in favour of existing `random_rewrite_rules`. --- src/wp-includes/class-wp-rewrite.php | 21 ------ .../tests/rewrite/randomRewriteRules.php | 67 ------------------- 2 files changed, 88 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index ae0d20710ba6c..d9f7a61f2e1f7 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1547,9 +1547,6 @@ public function rewrite_rules() { * the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed * when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}. * - * Whether the `/random/` rule is omitted can be changed with the {@see 'wp_rewrite_random_base_is_taken'} - * filter. - * * Rules added to the top of the rewrite rules, including those for post types and taxonomies, * take precedence over the random content rules. When the rewrite rules are generated, random * content rules using the same regex as a rule added to the top are omitted, see @@ -1595,24 +1592,6 @@ public function random_rewrite_rules(): array { fn( $random_base_post ) => $random_base_post instanceof WP_Post && is_post_publicly_viewable( $random_base_post ) ); - /** - * Filters whether the random content redirect base is taken. - * - * When the random base is taken, the `/random/` rewrite rule is omitted. The random - * base may vary depending on the site's language or permalink settings, so always - * determine the value from the `$random_base` or `$random_slug` parameters. - * - * @since 7.2.0 - * - * @param bool $random_base_is_taken Whether the random base is known to be taken. - * @param string $random_base The random base for the site, including the PATHINFO - * prefix when using PATHINFO permalinks, for example - * `random` or `index.php/random`. - * @param string $random_slug The random base slug for the site, without the PATHINFO - * prefix, for example `random`. - */ - $random_base_is_taken = apply_filters( 'wp_rewrite_random_base_is_taken', $random_base_is_taken, $random_base, $random_slug ); - if ( ! $random_base_is_taken ) { $rules[ $random_base . '/?$' ] = $this->index . '?random=1'; } diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index b0e02ba11c976..4d3acd7b23717 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -1282,71 +1282,4 @@ public function test_random_root_rule_is_omitted_when_a_top_rule_uses_its_regex_ $this->assertSame( 'index.php?pagename=wptests-taken', $this->get_stored_rewrite_rules()['index.php/random/?$'] ?? null, 'The top rule should take precedence over the root rule with PATHINFO permalinks.' ); } - - /** - * Ensure the random base is taken filter can omit the root rule. - */ - public function test_random_base_is_taken_filter_can_omit_the_root_rule(): void { - add_filter( 'wp_rewrite_random_base_is_taken', '__return_true' ); - - $rules = $this->get_random_rewrite_rules(); - - $this->assertArrayNotHasKey( 'random/?$', $rules, 'The filter should be able to omit the root rule.' ); - $this->assertArrayHasKey( 'random/category/(.+?)/?$', $rules, 'Random archive rules should be kept when the filter omits the root rule.' ); - } - - /** - * Ensure the random base is taken filter can keep the root rule when a page named "random" exists. - */ - public function test_random_base_is_taken_filter_can_keep_the_root_rule(): void { - self::factory()->post->create( - array( - 'post_type' => 'page', - 'post_name' => 'random', - ) - ); - add_filter( 'wp_rewrite_random_base_is_taken', '__return_false' ); - - $rules = $this->get_random_rewrite_rules(); - - $this->assertArrayHasKey( 'random/?$', $rules, 'The filter should be able to keep the root rule when a page named random exists.' ); - } - - /** - * Ensure the random base is taken filter receives the detected value, the random base and the random slug. - */ - public function test_random_base_is_taken_filter_receives_the_detected_value_and_random_base(): void { - self::factory()->post->create( - array( - 'post_type' => 'page', - 'post_name' => 'random', - ) - ); - - $filter = new MockAction(); - add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 3 ); - - $this->get_random_rewrite_rules(); - - $args = $filter->get_args(); - $this->assertNotEmpty( $args, 'The filter was not called.' ); - $this->assertSame( array( true, 'random', 'random' ), end( $args ), 'The filter should receive whether the base is taken, the random base and the random slug.' ); - } - - /** - * Ensure the random base is taken filter receives the random base with the PATHINFO prefix and the random slug without it. - */ - public function test_random_base_is_taken_filter_receives_the_random_base_and_slug_with_pathinfo_permalinks(): void { - $this->set_permalink_structure( '/index.php/%postname%/' ); - create_initial_taxonomies(); - - $filter = new MockAction(); - add_filter( 'wp_rewrite_random_base_is_taken', array( $filter, 'filter' ), 10, 3 ); - - $this->get_random_rewrite_rules(); - - $args = $filter->get_args(); - $this->assertNotEmpty( $args, 'The filter was not called with PATHINFO permalinks.' ); - $this->assertSame( array( false, 'index.php/random', 'random' ), end( $args ), 'The filter should receive the random base with the PATHINFO prefix and the random slug without it.' ); - } } From d2ae068f9ddb016592be55e1fafa95041ba2d17a Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 11 Nov 2026 09:34:15 +1100 Subject: [PATCH 19/22] Revert source code changes. --- src/wp-includes/class-wp-rewrite.php | 62 ++++++++-------------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index d9f7a61f2e1f7..20967bafa0206 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1423,6 +1423,22 @@ public function rewrite_rules() { */ $page_rewrite = apply_filters( 'page_rewrite_rules', $page_rewrite ); + // Random content rewrite rules. + $random_rewrite = $this->random_rewrite_rules(); + + /** + * Filters rewrite rules used for random content redirects. + * + * Likely random content rules would include `/random/`, as well as + * term, author and post type archives prefixed with the random base, + * for example `/random/category/news/`. + * + * @since 7.2.0 + * + * @param array $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern. + */ + $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite ); + // Extra permastructs. foreach ( $this->extra_permastructs as $permastructname => $struct ) { if ( is_array( $struct ) ) { @@ -1469,38 +1485,7 @@ public function rewrite_rules() { $this->extra_rules_top = array_merge( $this->extra_rules_top, $rules ); } - // Random content rewrite rules. - $random_rewrite = $this->random_rewrite_rules(); - - /* - * Rules added to the top, including those for post types and taxonomies, take - * precedence over random content rules using the same regex, for example a post - * type archive at `/random/`. - * - * Rules added to the top are merged ahead of the random content rules, so without - * this, the random content rule's query would replace the query of a rule using the - * same regex. Rules added to the top using a different regex for the same path, - * such as `^random/?$`, already take precedence as they are matched first. - */ - $random_rewrite = array_diff_key( $random_rewrite, $this->extra_rules_top ); - - /** - * Filters rewrite rules used for random content redirects. - * - * Likely random content rules would include `/random/`, as well as - * term, author and post type archives prefixed with the random base, - * for example `/random/category/news/`. - * - * @since 7.2.0 - * - * @param array $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern. - */ - $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite ); - - /* - * Put them together, with the random content rules ahead of the generic rules - * such as the page and post rules which would otherwise match them first. - */ + // Put them together. if ( $this->use_verbose_page_rules ) { $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $random_rewrite, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules ); } else { @@ -1547,19 +1532,6 @@ public function rewrite_rules() { * the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed * when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}. * - * Rules added to the top of the rewrite rules, including those for post types and taxonomies, - * take precedence over the random content rules. When the rewrite rules are generated, random - * content rules using the same regex as a rule added to the top are omitted, see - * {@see WP_Rewrite::rewrite_rules()}. For example, a post type archive at `/random/` replaces - * the `/random/` rule. - * - * If a post type or taxonomy uses the random base as its rewrite slug, its rules may match - * random archive URLs before the random content rules do. For example, the attachment rules - * of a post type with the rewrite slug `random` match `/random/category/news/`, and the rules - * of a hierarchical post type or taxonomy match every URL beginning with `/random/`. Such - * random archive URLs display the post type or taxonomy content instead. To use random - * archives on these sites, change the random base on the Permalinks screen. - * * The `random` query variable can be added to any other archive or search URL, for example * `/2026/?random`, to request a random post from that archive. * From 19c3aa206d8c04dcb540d0c48404465378837924 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 11 Nov 2026 09:49:49 +1100 Subject: [PATCH 20/22] Revert "Revert source code changes." This reverts commit d23af89e59683a0382446f8d2804428c2155a56c. --- src/wp-includes/class-wp-rewrite.php | 62 ++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 20967bafa0206..d9f7a61f2e1f7 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -1423,22 +1423,6 @@ public function rewrite_rules() { */ $page_rewrite = apply_filters( 'page_rewrite_rules', $page_rewrite ); - // Random content rewrite rules. - $random_rewrite = $this->random_rewrite_rules(); - - /** - * Filters rewrite rules used for random content redirects. - * - * Likely random content rules would include `/random/`, as well as - * term, author and post type archives prefixed with the random base, - * for example `/random/category/news/`. - * - * @since 7.2.0 - * - * @param array $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern. - */ - $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite ); - // Extra permastructs. foreach ( $this->extra_permastructs as $permastructname => $struct ) { if ( is_array( $struct ) ) { @@ -1485,7 +1469,38 @@ public function rewrite_rules() { $this->extra_rules_top = array_merge( $this->extra_rules_top, $rules ); } - // Put them together. + // Random content rewrite rules. + $random_rewrite = $this->random_rewrite_rules(); + + /* + * Rules added to the top, including those for post types and taxonomies, take + * precedence over random content rules using the same regex, for example a post + * type archive at `/random/`. + * + * Rules added to the top are merged ahead of the random content rules, so without + * this, the random content rule's query would replace the query of a rule using the + * same regex. Rules added to the top using a different regex for the same path, + * such as `^random/?$`, already take precedence as they are matched first. + */ + $random_rewrite = array_diff_key( $random_rewrite, $this->extra_rules_top ); + + /** + * Filters rewrite rules used for random content redirects. + * + * Likely random content rules would include `/random/`, as well as + * term, author and post type archives prefixed with the random base, + * for example `/random/category/news/`. + * + * @since 7.2.0 + * + * @param array $random_rewrite Array of rewrite rules for random content redirects, keyed by their regex pattern. + */ + $random_rewrite = apply_filters( 'random_rewrite_rules', $random_rewrite ); + + /* + * Put them together, with the random content rules ahead of the generic rules + * such as the page and post rules which would otherwise match them first. + */ if ( $this->use_verbose_page_rules ) { $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $random_rewrite, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules ); } else { @@ -1532,6 +1547,19 @@ public function rewrite_rules() { * the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed * when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}. * + * Rules added to the top of the rewrite rules, including those for post types and taxonomies, + * take precedence over the random content rules. When the rewrite rules are generated, random + * content rules using the same regex as a rule added to the top are omitted, see + * {@see WP_Rewrite::rewrite_rules()}. For example, a post type archive at `/random/` replaces + * the `/random/` rule. + * + * If a post type or taxonomy uses the random base as its rewrite slug, its rules may match + * random archive URLs before the random content rules do. For example, the attachment rules + * of a post type with the rewrite slug `random` match `/random/category/news/`, and the rules + * of a hierarchical post type or taxonomy match every URL beginning with `/random/`. Such + * random archive URLs display the post type or taxonomy content instead. To use random + * archives on these sites, change the random base on the Permalinks screen. + * * The `random` query variable can be added to any other archive or search URL, for example * `/2026/?random`, to request a random post from that archive. * From 69591df2221afffbb7decf131055db6fcb279658 Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Thu, 12 Nov 2026 15:36:30 -0600 Subject: [PATCH 21/22] Improve documentation based on PHPStan Co-authored-by: Aaron Jorbin --- .../tests/query/randomContentRedirect.php | 14 +++++++++++-- .../tests/rewrite/randomRewriteRules.php | 21 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 7992bd4cb37db..ca4d86aef4058 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -108,7 +108,7 @@ class Tests_Query_RandomContentRedirect extends WP_UnitTestCase { * * WP_Rewrite::init() does not reset these, so they are restored after each test. * - * @var array + * @var string[] */ protected $original_extra_rules_top = array(); @@ -118,7 +118,7 @@ class Tests_Query_RandomContentRedirect extends WP_UnitTestCase { * Unregistering post types between tests does not remove their permalink structures, * so they are restored after each test. * - * @var array> + * @var array> */ protected $original_extra_permastructs = array(); @@ -164,6 +164,8 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void /** * Sets up pretty permalinks and redirect capturing before each test. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ public function set_up(): void { global $wp_rewrite; @@ -189,6 +191,8 @@ public function set_up(): void { /** * Unregisters the test post types and taxonomy after each test. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ public function tear_down(): void { global $wp_rewrite; @@ -2734,6 +2738,9 @@ public function test_unpublishing_a_post_named_random_resumes_random_content_red * * @param string $post_type Post type name. * @param array $args Post type registration arguments. + * + * @phpstan-param lowercase-string&non-falsy-string $post_type + * @phpstan-param array{public: bool, has_archive: bool|string, rewrite?: array{slug: string}} $args */ public function test_post_type_archive_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { register_post_type( $post_type, $args ); @@ -2790,6 +2797,9 @@ public static function data_post_type_archive_at_the_random_base_is_not_redirect * * @param string $post_type Post type name. * @param array $args Post type registration arguments. + * + * @phpstan-param lowercase-string&non-falsy-string $post_type + * @phpstan-param array{public: bool, has_archive: bool|string, rewrite?: array{slug: string}} $args */ public function test_single_post_of_a_post_type_at_the_random_base_is_not_redirected( string $post_type, array $args ): void { register_post_type( $post_type, $args ); diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index 4d3acd7b23717..eed927f53a39f 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -23,7 +23,7 @@ class Tests_Rewrite_RandomRewriteRules extends WP_UnitTestCase { * * WP_Rewrite::init() does not reset these, so they are restored after each test. * - * @var array + * @var string[] */ private $original_extra_rules_top = array(); @@ -33,7 +33,7 @@ class Tests_Rewrite_RandomRewriteRules extends WP_UnitTestCase { * Unregistering post types between tests does not remove their permalink structures, * so they are restored after each test. * - * @var array> + * @var array> */ private $original_extra_permastructs = array(); @@ -52,6 +52,11 @@ public function go_to( $url ) { } } + /** + * Sets up pretty permalinks before each test. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + */ public function set_up(): void { global $wp_rewrite; @@ -73,6 +78,11 @@ static function (): void { create_initial_taxonomies(); } + /** + * Restores the rewrite rules and random base after each test. + * + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. + */ public function tear_down(): void { global $wp_rewrite; @@ -1111,7 +1121,9 @@ public function test_random_rules_using_the_regex_of_a_top_rule_are_omitted( str $rules = $this->get_stored_rewrite_rules(); $this->assertSame( 'index.php?pagename=wptests-taken', $rules[ $regex ] ?? null, "The top rule {$regex} should take precedence over the random rule using the same regex." ); - $this->assertStringContainsString( 'random=1', $rules[ $random_rule ] ?? '', "The random rule {$random_rule} should be kept when the top rule {$regex} uses a different regex." ); + $random_query = $rules[ $random_rule ] ?? ''; + $this->assertIsString( $random_query, "The random rule {$random_rule} should be kept when the top rule {$regex} uses a different regex." ); + $this->assertStringContainsString( 'random=1', $random_query, "The random rule {$random_rule} should be kept when the top rule {$regex} uses a different regex." ); } /** @@ -1162,6 +1174,9 @@ public function test_random_root_rule_is_kept_when_a_top_rule_uses_a_different_r * * @param string $post_type Post type name. * @param array $args Post type registration arguments. + * + * @phpstan-param lowercase-string&non-falsy-string $post_type + * @phpstan-param array{public: bool, has_archive: bool|string, rewrite?: array{slug: string}} $args */ public function test_random_root_rule_is_omitted_for_a_post_type_archive_at_the_random_base( string $post_type, array $args ): void { register_post_type( $post_type, $args ); From 253b89db489e09eca4dde0da5d1f55c1844ec5e3 Mon Sep 17 00:00:00 2001 From: Aaron Jorbin <622599+aaronjorbin@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:56:59 -0600 Subject: [PATCH 22/22] Combine post type at the random base data providers. Use a single data provider for the post type archive and single post tests, including the cases from each. The single post test now checks the permalink against the post type's rewrite slug, as the archive only case does not place single posts at the random base. Co-Authored-By: Claude Fable 5.2 --- .../tests/query/randomContentRedirect.php | 55 +++++-------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index ca4d86aef4058..a5a04107bb789 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -2734,7 +2734,7 @@ public function test_unpublishing_a_post_named_random_resumes_random_content_red /** * Ensure a post type archive at the random base is not redirected to random content. * - * @dataProvider data_post_type_archive_at_the_random_base_is_not_redirected + * @dataProvider data_post_types_at_the_random_base * * @param string $post_type Post type name. * @param array $args Post type registration arguments. @@ -2755,45 +2755,13 @@ public function test_post_type_archive_at_the_random_base_is_not_redirected( str $this->assertSame( $post_type, get_query_var( 'post_type' ), 'The post type archive at the random base should be queried.' ); } - /** - * Data provider. - * - * @return array}> - */ - public static function data_post_type_archive_at_the_random_base_is_not_redirected(): array { - return array( - 'post type named random' => array( - 'random', - array( - 'public' => true, - 'has_archive' => true, - ), - ), - 'archive slug random' => array( - 'wptests_random', - array( - 'public' => true, - 'has_archive' => 'random', - ), - ), - 'rewrite slug random' => array( - 'wptests_random', - array( - 'public' => true, - 'has_archive' => true, - 'rewrite' => array( 'slug' => 'random' ), - ), - ), - ); - } - /** * Ensure a single post of a post type at the random base is not redirected to random content. * * Registering a post type named `random` also registers `random` as its query variable, * which is the query variable used by random content requests. * - * @dataProvider data_single_post_of_a_post_type_at_the_random_base_is_not_redirected + * @dataProvider data_post_types_at_the_random_base * * @param string $post_type Post type name. * @param array $args Post type registration arguments. @@ -2810,7 +2778,7 @@ public function test_single_post_of_a_post_type_at_the_random_base_is_not_redire $permalink = get_permalink( $post_id ); $this->assertIsString( $permalink, 'The post permalink could not be retrieved.' ); - $this->assertStringStartsWith( home_url( '/random/' ), $permalink, 'The post permalink should use the random base.' ); + $this->assertStringStartsWith( home_url( '/' . ( $args['rewrite']['slug'] ?? $post_type ) . '/' ), $permalink, 'The post permalink should use the post type rewrite slug.' ); $redirect = new MockAction(); add_filter( 'wp_redirect', array( $redirect, 'filter' ), 5 ); @@ -2823,20 +2791,27 @@ public function test_single_post_of_a_post_type_at_the_random_base_is_not_redire } /** - * Data provider. + * Data provider for post types registered with an archive or single posts at the random base. * * @return array}> */ - public static function data_single_post_of_a_post_type_at_the_random_base_is_not_redirected(): array { + public static function data_post_types_at_the_random_base(): array { return array( - 'post type named random' => array( + 'post type named random' => array( 'random', array( 'public' => true, 'has_archive' => true, ), ), - 'archive slug random' => array( + 'archive slug random' => array( + 'wptests_random', + array( + 'public' => true, + 'has_archive' => 'random', + ), + ), + 'archive and rewrite slug random' => array( 'wptests_random', array( 'public' => true, @@ -2844,7 +2819,7 @@ public static function data_single_post_of_a_post_type_at_the_random_base_is_not 'rewrite' => array( 'slug' => 'random' ), ), ), - 'rewrite slug random' => array( + 'rewrite slug random' => array( 'wptests_random', array( 'public' => true,