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. * diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 4f1c06c8fcea6..2aa5e948254c6 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -96,6 +96,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. * @@ -140,8 +159,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(); @@ -160,6 +184,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' ); @@ -2703,4 +2732,277 @@ 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', + ), + ), + '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 + * + * @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. + */ + 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. + * + * 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. + */ + 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' ), + '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. + * + * 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.' ); + } } diff --git a/tests/phpunit/tests/rewrite/randomRewriteRules.php b/tests/phpunit/tests/rewrite/randomRewriteRules.php index 47e828489df0b..4d68afcb86b8c 100644 --- a/tests/phpunit/tests/rewrite/randomRewriteRules.php +++ b/tests/phpunit/tests/rewrite/randomRewriteRules.php @@ -11,9 +11,33 @@ */ class Tests_Rewrite_RandomRewriteRules extends WP_UnitTestCase { + /** + * 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(); + 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; + // Prevent random content redirects from exiting, only the rewrite rules are under test. add_filter( 'wp_redirect', '__return_false' ); @@ -23,6 +47,11 @@ public function set_up(): 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. @@ -639,7 +668,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. */ @@ -677,7 +706,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%/' ), @@ -692,10 +721,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', @@ -717,56 +753,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' ) ), ); } @@ -1027,4 +1065,194 @@ public static function data_posts_that_do_not_conflict(): array { ), ); } + + /** + * Ensure random rules using the same regex as a rule added to the top are omitted. + * + * 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. + * + * @dataProvider data_random_rules_using_the_regex_of_a_top_rule_are_omitted + * + * @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_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' ); + + $this->get_random_rewrite_rules(); + $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." ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_random_rules_using_the_regex_of_a_top_rule_are_omitted(): array { + return array( + 'root rule' => array( 'random/?$', 'random/category/(.+?)/?$' ), + 'category archive rule' => array( 'random/category/(.+?)/?$', 'random/?$' ), + ); + } + + /** + * Ensure random rules are kept when a rule added to the top uses a different regex for 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. + */ + public function test_random_root_rule_is_kept_when_a_top_rule_uses_a_different_regex_for_the_random_base(): void { + global $wp; + + $page = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_name' => 'wptests-taken', + ) + ); + + add_rewrite_rule( '^random/?$', 'index.php?pagename=wptests-taken', 'top' ); + + $this->get_random_rewrite_rules(); + + $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->go_to( home_url( '/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.' ); + } + + /** + * Ensure a post type archive at the random base takes precedence over the root rule. + * + * @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. + */ + 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 ); + + $this->get_random_rewrite_rules(); + + $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.' ); + } + + /** + * Data provider. + * + * @return 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', + 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 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, + ) + ); + + $this->get_random_rewrite_rules(); + + $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}." ); + } + + /** + * 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 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_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' ); + + $this->get_random_rewrite_rules(); + + $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 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_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' ); + + $this->get_random_rewrite_rules(); + + $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 omitted when a top rule uses its regex with PATHINFO permalinks. + */ + 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( 'index.php/random/?$', 'index.php?pagename=wptests-taken', 'top' ); + + $this->get_random_rewrite_rules(); + + $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.' ); + } }