diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 7f5f9d0fb0eb8..3c1234f9cfa8e 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -496,7 +496,6 @@ add_action( 'attachment_updated', 'wp_check_for_changed_dates', 12, 3 ); // Random content redirects, see WP::handle_random(). -add_filter( 'split_the_query', 'wp_random_content_split_the_query', 10, 2 ); add_action( 'wp_after_insert_post', 'wp_random_content_flush_rewrite_rules_for_post', 10, 4 ); add_action( 'after_delete_post', 'wp_random_content_flush_rewrite_rules_for_post', 10, 2 ); diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index 212ac1619e87f..4cafca3d7c068 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -1461,27 +1461,6 @@ function wp_random_content_post_limits( $limits, WP_Query $query ): string { return (string) $limits; } -/** - * Prevents the random content query from being split into multiple database queries. - * - * Randomly ordered queries are not cached, so there is no benefit to querying the post - * IDs separately to the post objects. Disabling the split allows the random post to be - * retrieved in a single database query. - * - * @since 7.2.0 - * - * @param bool $split_the_query Whether or not to split the query. - * @param WP_Query $query The WP_Query instance. - * @return bool Whether or not to split the query. - */ -function wp_random_content_split_the_query( $split_the_query, WP_Query $query ): bool { - if ( wp_is_random_content_query( $query ) ) { - return false; - } - - return (bool) $split_the_query; -} - /** * Redirects random content requests to the randomly selected post. * diff --git a/tests/phpunit/tests/query/randomContentRedirect.php b/tests/phpunit/tests/query/randomContentRedirect.php index 89c6f8a96d928..e12e2796c9336 100644 --- a/tests/phpunit/tests/query/randomContentRedirect.php +++ b/tests/phpunit/tests/query/randomContentRedirect.php @@ -15,7 +15,6 @@ * @covers ::wp_is_random_content_query * @covers ::wp_get_random_content_post_types * @covers ::wp_random_content_flush_rewrite_rules_for_post - * @covers ::wp_random_content_split_the_query * @covers ::wp_is_random_content_redirect_enabled */ class Tests_Query_RandomContentRedirect extends WP_UnitTestCase { @@ -97,6 +96,11 @@ class Tests_Query_RandomContentRedirect extends WP_UnitTestCase { */ protected $recording_queries = false; + /** + * Creates shared user, category and post fixtures. + * + * @param WP_UnitTest_Factory $factory Test factory. + */ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void { self::$editor = $factory->user->create( array( @@ -132,6 +136,9 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void ); } + /** + * Sets up pretty permalinks and redirect capturing before each test. + */ public function set_up(): void { parent::set_up(); @@ -149,6 +156,9 @@ public function set_up(): void { add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 10, 2 ); } + /** + * Unregisters the test post types and taxonomy after each test. + */ public function tear_down(): void { unregister_post_type( 'wptests_book' ); unregister_post_type( 'wptests_hidden' ); @@ -165,7 +175,7 @@ public function tear_down(): void { * @param int $status Redirect status. * @return false */ - public function filter_wp_redirect( $location, $status ) { + public function filter_wp_redirect( string $location, int $status ) { $this->redirect_location = $location; $this->redirect_status = $status; $this->recording_queries = false; @@ -186,7 +196,7 @@ public function start_recording_queries(): void { * @param string $query Database query. * @return string Unmodified database query. */ - public function record_query( $query ) { + public function record_query( string $query ) { if ( $this->recording_queries ) { $this->queries[] = $query; } @@ -199,7 +209,7 @@ public function record_query( $query ) { * * @param string $url URL to request. */ - protected function go_to_and_record_queries( $url ): void { + protected function go_to_and_record_queries( string $url ): void { add_action( 'parse_request', array( $this, 'start_recording_queries' ), PHP_INT_MAX ); add_filter( 'query', array( $this, 'record_query' ) ); @@ -246,7 +256,6 @@ protected function create_post( array $args = array() ): int { * * @param int[] $post_ids Allowed post IDs. * @param string $message Message to display on failure. - * * @phpstan-assert !null $this->redirect_location */ @@ -282,20 +291,22 @@ protected function assertQueryIsNotRandomized( WP_Query $query, string $message */ /** + * Ensure random content URLs redirect to a published post. + * * @dataProvider data_random_urls * * @param string $path Random content request path. */ - public function test_random_url_redirects_to_a_published_post( $path ): void { + public function test_random_url_redirects_to_a_published_post( string $path ): void { $this->go_to( home_url( $path ) ); $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'The random URL should redirect to a published post.' ); } /** - * Data provider. + * Data provider for {@see self::test_random_url_redirects_to_a_published_post()}. * - * @return array + * @return array */ public static function data_random_urls(): array { return array( @@ -307,18 +318,27 @@ public static function data_random_urls(): array { ); } + /** + * Ensure redirect uses HTTP 302 Temporary Redirect status. + */ public function test_random_redirect_uses_a_temporary_redirect(): void { $this->go_to( home_url( '/random/' ) ); $this->assertSame( 302, $this->redirect_status, 'Random redirects must not be permanent.' ); } + /** + * Ensure redirect location header matches the permalink of the selected post. + */ public function test_random_redirect_location_is_the_permalink(): void { $this->go_to( home_url( '/random/category/arts/' ) ); $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'The redirect location should be the permalink of the selected post.' ); } + /** + * Ensure random redirects work with plain permalinks. + */ public function test_random_query_string_redirects_with_plain_permalinks(): void { $this->set_permalink_structure( '' ); @@ -328,6 +348,9 @@ public function test_random_query_string_redirects_with_plain_permalinks(): void $this->assertStringContainsString( '?p=', $this->redirect_location, 'The redirect should use the plain permalink.' ); } + /** + * Ensure random redirects work with pathinfo (/index.php/...) permalinks. + */ public function test_random_redirect_with_pathinfo_permalinks(): void { $this->set_permalink_structure( '/index.php/%postname%/' ); @@ -336,12 +359,18 @@ public function test_random_redirect_with_pathinfo_permalinks(): void { $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'The random URL should redirect with PATHINFO permalinks.' ); } + /** + * Ensure random redirects respect the selected category. + */ public function test_random_category_url_only_selects_posts_in_the_category(): void { $this->go_to( home_url( '/random/category/sport/' ) ); $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'The random category URL should only select posts in the category.' ); } + /** + * Ensure random redirects respect the selected nested category. + */ public function test_random_child_category_url_selects_posts_in_the_child_category(): void { $child = self::factory()->category->create( array( @@ -356,6 +385,9 @@ public function test_random_child_category_url_selects_posts_in_the_child_catego $this->assertSame( get_permalink( $post_id ), $this->redirect_location, 'The random child category URL should select posts in the child category.' ); } + /** + * Ensure random redirects include posts in the selected category's sub-categories. + */ public function test_random_category_url_includes_posts_in_child_categories(): void { $child = self::factory()->category->create( array( @@ -370,18 +402,27 @@ public function test_random_category_url_includes_posts_in_child_categories(): v $this->assertRedirectedToOneOf( array( self::$sport_post, $child_post ), 'Category archives include child categories.' ); } + /** + * Ensure random redirects respect the `?cat` URL parameter. + */ public function test_random_cat_query_string_only_selects_posts_in_the_category(): void { $this->go_to( add_query_arg( 'cat', self::$arts_category, home_url( '/?random' ) ) ); $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'The random cat query string should only select posts in the category.' ); } + /** + * Ensure random redirects respect the selected tag. + */ public function test_random_tag_url_only_selects_posts_with_the_tag(): void { $this->go_to( home_url( '/random/tag/impressionism/' ) ); $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'The random tag URL should only select posts with the tag.' ); } + /** + * Ensure random redirects respect URLs specifying terms from multiple taxonomies. + */ public function test_random_combined_taxonomy_query_selects_posts_matching_all_terms(): void { $impressionist_sport = $this->create_post( array( @@ -396,24 +437,36 @@ public function test_random_combined_taxonomy_query_selects_posts_matching_all_t $this->assertSame( get_permalink( $impressionist_sport ), $this->redirect_location, 'The combined taxonomy query should only select posts matching all terms.' ); } + /** + * Ensure random redirects respect the selected author. + */ public function test_random_author_url_only_selects_posts_by_the_author(): void { $this->go_to( home_url( '/random/author/random-editor/' ) ); $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'The random author URL should only select posts by the author.' ); } + /** + * Ensure random redirects respect the selected date. + */ public function test_random_date_archive_only_selects_posts_from_the_date(): void { $this->go_to( home_url( '/1844/?random' ) ); $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'The random date archive should only select posts from the date.' ); } + /** + * Ensure random redirects respect the selected search query. + */ public function test_random_search_only_selects_matching_posts(): void { $this->go_to( home_url( '/?s=Baseball&random' ) ); $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'The random search should only select matching posts.' ); } + /** + * Ensure random redirects respect the selected post type. + */ public function test_random_post_type_archive_url_only_selects_posts_of_the_type(): void { register_post_type( 'wptests_book', @@ -431,6 +484,9 @@ public function test_random_post_type_archive_url_only_selects_posts_of_the_type $this->assertSame( get_permalink( $book ), $this->redirect_location, 'The random post type archive URL should only select posts of the type.' ); } + /** + * Ensure random redirects respect the randomable post type parameter regardless of archive setting. + */ public function test_random_post_type_rewrite_slug_url_selects_posts_of_a_randomable_type_without_an_archive(): void { register_post_type( 'wptests_book', @@ -450,6 +506,9 @@ public function test_random_post_type_rewrite_slug_url_selects_posts_of_a_random $this->assertSame( get_permalink( $book ), $this->redirect_location, 'The random rewrite slug URL should select posts of a randomable type without an archive.' ); } + /** + * Ensure random redirects respect the selected post type using plain permalinks. + */ public function test_random_post_type_query_string_selects_posts_of_the_type(): void { register_post_type( 'wptests_book', @@ -470,6 +529,9 @@ public function test_random_post_type_query_string_selects_posts_of_the_type(): * Randomable post types. */ + /** + * Ensure pages are not randomable. + */ public function test_no_redirect_for_pages(): void { $this->create_post( array( 'post_type' => 'page' ) ); @@ -478,6 +540,9 @@ public function test_no_redirect_for_pages(): void { $this->assertNotRedirected( 'Pages should not be randomable by default.' ); } + /** + * Ensure main query for pages is not modified by random query string parameter. + */ public function test_main_query_is_unchanged_for_pages(): void { $this->create_post( array( 'post_type' => 'page' ) ); @@ -486,6 +551,9 @@ public function test_main_query_is_unchanged_for_pages(): void { $this->assertQueryIsNotRandomized( $this->get_main_query(), 'Requests for non-randomable post types should not be randomized.' ); } + /** + * Ensure pages are randomable if registration arguments enable the feature. + */ public function test_pages_redirect_when_made_randomable(): void { add_filter( 'register_post_type_args', array( $this, 'filter_make_pages_randomable' ), 10, 2 ); create_initial_post_types(); @@ -512,6 +580,9 @@ public function filter_make_pages_randomable( array $args, string $post_type ): return $args; } + /** + * Ensure post types registered as not randomable do not redirect. + */ public function test_no_redirect_for_post_type_that_is_not_randomable(): void { register_post_type( 'wptests_book', @@ -528,6 +599,9 @@ public function test_no_redirect_for_post_type_that_is_not_randomable(): void { $this->assertNotRedirected( 'Post types registered as not randomable should not redirect.' ); } + /** + * Ensure archives of post types registered as not randomable do not redirect. + */ public function test_no_redirect_for_post_type_archive_that_is_not_randomable(): void { register_post_type( 'wptests_book', @@ -547,6 +621,9 @@ public function test_no_redirect_for_post_type_archive_that_is_not_randomable(): $this->assertTrue( is_post_type_archive( 'wptests_book' ), 'The archive should be displayed.' ); } + /** + * Ensure only randomable post types are selected when multiple post types are requested. + */ public function test_random_redirect_with_multiple_post_types_only_selects_randomable_types(): void { register_post_type( 'wptests_book', @@ -564,6 +641,9 @@ public function test_random_redirect_with_multiple_post_types_only_selects_rando $this->assertSame( get_permalink( $book ), $this->redirect_location, 'Only randomable post types should be selected from multiple post types.' ); } + /** + * Ensure random searches do not select pages. + */ public function test_random_search_only_selects_randomable_post_types(): void { $this->create_post( array( @@ -577,6 +657,9 @@ public function test_random_search_only_selects_randomable_post_types(): void { $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'Pages should not be selected from search results.' ); } + /** + * Ensure random searches include randomable custom post types. + */ public function test_random_search_includes_randomable_custom_post_types(): void { register_post_type( 'wptests_book', @@ -598,6 +681,9 @@ public function test_random_search_includes_randomable_custom_post_types(): void $this->assertSame( get_permalink( $book ), $this->redirect_location, 'Randomable custom post types should be selected from search results.' ); } + /** + * Ensure random custom taxonomy archives only select randomable post types. + */ public function test_random_custom_taxonomy_archive_only_selects_randomable_post_types(): void { register_post_type( 'wptests_book', @@ -626,6 +712,9 @@ public function test_random_custom_taxonomy_archive_only_selects_randomable_post $this->assertSame( get_permalink( $book ), $this->redirect_location, 'Only randomable post types should be selected from a custom taxonomy archive.' ); } + /** + * Ensure archives of taxonomies used only by pages do not redirect. + */ public function test_no_redirect_for_taxonomy_used_only_by_pages(): void { register_taxonomy( 'wptests_genre', @@ -647,12 +736,14 @@ public function test_no_redirect_for_taxonomy_used_only_by_pages(): void { } /** + * Ensure the eligible post types match the request. + * * @dataProvider data_wp_get_random_content_post_types * * @param string $url URL to request. * @param string[] $expected Expected post types. */ - public function test_wp_get_random_content_post_types( $url, $expected ): void { + public function test_wp_get_random_content_post_types( string $url, array $expected ): void { register_post_type( 'wptests_book', array( @@ -681,9 +772,9 @@ public function test_wp_get_random_content_post_types( $url, $expected ): void { } /** - * Data provider. + * Data provider for {@see self::test_wp_get_random_content_post_types()}. * - * @return array + * @return array }> */ public static function data_wp_get_random_content_post_types(): array { return array( @@ -700,6 +791,9 @@ public static function data_wp_get_random_content_post_types(): array { ); } + /** + * Ensure the random URL only selects posts by default. + */ public function test_random_url_defaults_to_posts(): void { $this->create_post( array( 'post_type' => 'page' ) ); @@ -708,6 +802,9 @@ public function test_random_url_defaults_to_posts(): void { $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'Pages should not be selected without requesting them.' ); } + /** + * Ensure the random URL redirects to a post when a static front page is set. + */ public function test_random_url_with_static_front_page_redirects_to_a_post(): void { $front = $this->create_post( array( 'post_type' => 'page' ) ); update_option( 'show_on_front', 'page' ); @@ -718,6 +815,9 @@ public function test_random_url_with_static_front_page_redirects_to_a_post(): vo $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'The random URL should redirect to a post when a static front page is set.' ); } + /** + * Ensure pagination does not affect the random selection. + */ public function test_random_redirect_ignores_pagination(): void { $this->go_to( home_url( '/random/category/arts/?paged=5' ) ); @@ -729,11 +829,13 @@ public function test_random_redirect_ignores_pagination(): void { */ /** + * Ensure unpublished and password protected posts are not selected, even for administrators. + * * @dataProvider data_ineligible_posts * * @param array $args Post arguments. */ - public function test_random_redirect_excludes_ineligible_posts( $args ): void { + public function test_random_redirect_excludes_ineligible_posts( array $args ): void { $category = self::factory()->category->create(); $this->create_post( array_merge( array( 'post_category' => array( $category ) ), $args ) ); @@ -745,9 +847,9 @@ public function test_random_redirect_excludes_ineligible_posts( $args ): void { } /** - * Data provider. + * Data provider for {@see self::test_random_redirect_excludes_ineligible_posts()}. * - * @return array}> + * @return array }> */ public static function data_ineligible_posts(): array { return array( @@ -766,6 +868,8 @@ public static function data_ineligible_posts(): array { } /** + * Ensure sticky posts are not prepended to the random selection. + * * @global wpdb $wpdb WordPress database abstraction object. */ public function test_random_redirect_ignores_sticky_posts(): void { @@ -805,6 +909,9 @@ static function ( array $clauses, WP_Query $query ) use ( $sticky_orderby ): arr * Requests that are not random content requests. */ + /** + * Ensure the home page does not redirect without the random query variable. + */ public function test_no_redirect_without_random_query_var(): void { $this->go_to( home_url( '/' ) ); @@ -812,6 +919,9 @@ public function test_no_redirect_without_random_query_var(): void { $this->assertQueryTrue( 'is_home', 'is_front_page' ); } + /** + * Ensure archives are not randomized without the random query variable. + */ public function test_archives_are_unchanged_without_random_query_var(): void { $this->go_to( home_url( '/category/arts/' ) ); @@ -819,6 +929,9 @@ public function test_archives_are_unchanged_without_random_query_var(): void { $this->assertQueryIsNotRandomized( $this->get_main_query(), 'The archive order should not be modified.' ); } + /** + * Ensure singular posts do not redirect with the random query variable. + */ public function test_no_redirect_for_singular_post_with_random_query_var(): void { $this->go_to( add_query_arg( 'random', '', get_permalink( self::$arts_post ) ) ); @@ -827,6 +940,9 @@ public function test_no_redirect_for_singular_post_with_random_query_var(): void $this->assertSame( self::$arts_post, get_queried_object_id(), 'The singular post should be queried despite the random query variable.' ); } + /** + * Ensure requests for a post ID do not redirect with the random query variable. + */ public function test_no_redirect_for_post_id_with_random_query_var(): void { $this->go_to( home_url( '/?random&p=' . self::$sport_post ) ); @@ -834,6 +950,9 @@ public function test_no_redirect_for_post_id_with_random_query_var(): void { $this->assertSame( self::$sport_post, get_queried_object_id(), 'The post ID should be queried despite the random query variable.' ); } + /** + * Ensure pages do not redirect with the random query variable. + */ public function test_no_redirect_for_page_with_random_query_var(): void { $page = $this->create_post( array( @@ -848,6 +967,9 @@ public function test_no_redirect_for_page_with_random_query_var(): void { $this->assertSame( $page, get_queried_object_id(), 'The page should be queried despite the random query variable.' ); } + /** + * Ensure the static front page does not redirect with the random query variable. + */ public function test_no_redirect_for_static_front_page_with_random_query_var_from_extra_vars(): void { $front = $this->create_post( array( 'post_type' => 'page' ) ); update_option( 'show_on_front', 'page' ); @@ -860,11 +982,13 @@ 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 * * @param string $path Feed path. */ - public function test_no_redirect_for_feeds( $path ): void { + public function test_no_redirect_for_feeds( string $path ): void { $this->go_to( home_url( $path ) ); $this->assertNotRedirected( 'Feeds should not redirect.' ); @@ -872,9 +996,9 @@ public function test_no_redirect_for_feeds( $path ): void { } /** - * Data provider. + * Data provider for {@see self::test_no_redirect_for_feeds()}. * - * @return array + * @return array */ public static function data_feed_urls(): array { return array( @@ -884,6 +1008,9 @@ public static function data_feed_urls(): array { ); } + /** + * Ensure embeds do not redirect with the random query variable. + */ public function test_no_redirect_for_embeds(): void { $this->go_to( home_url( '/cassatt/embed/?random' ) ); @@ -891,6 +1018,9 @@ public function test_no_redirect_for_embeds(): void { $this->assertTrue( is_embed(), 'The embed should be displayed despite the random query variable.' ); } + /** + * Ensure random requests for a nonexistent category return a 404 rather than redirecting. + */ public function test_no_redirect_for_nonexistent_category(): void { $this->go_to( home_url( '/random/category/does-not-exist/' ) ); @@ -898,6 +1028,9 @@ public function test_no_redirect_for_nonexistent_category(): void { $this->assertTrue( is_404(), 'A random request for a nonexistent term should 404.' ); } + /** + * Ensure random requests for an empty category return a 404 rather than redirecting. + */ public function test_no_redirect_for_empty_category(): void { $this->go_to( home_url( '/random/category/empty/' ) ); @@ -905,6 +1038,9 @@ public function test_no_redirect_for_empty_category(): void { $this->assertQueryTrue( 'is_404' ); } + /** + * Ensure random requests without eligible posts return a 404 rather than redirecting. + */ public function test_no_redirect_when_no_posts_are_eligible(): void { add_filter( 'posts_where', @@ -922,11 +1058,13 @@ 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 * * @param string $method HTTP request method. */ - public function test_no_redirect_for_non_get_requests( $method ): void { + public function test_no_redirect_for_non_get_requests( string $method ): void { $_SERVER['REQUEST_METHOD'] = $method; $this->go_to( home_url( '/random/' ) ); @@ -935,9 +1073,9 @@ public function test_no_redirect_for_non_get_requests( $method ): void { } /** - * Data provider. + * Data provider for {@see self::test_no_redirect_for_non_get_requests()}. * - * @return array + * @return array */ public static function data_non_redirecting_http_methods(): array { return array( @@ -948,6 +1086,9 @@ public static function data_non_redirecting_http_methods(): array { ); } + /** + * Ensure requests without an HTTP method do not redirect. + */ public function test_no_redirect_without_request_method(): void { unset( $_SERVER['REQUEST_METHOD'] ); @@ -957,11 +1098,13 @@ public function test_no_redirect_without_request_method(): void { } /** + * Ensure GET and HEAD requests redirect, regardless of case. + * * @dataProvider data_redirecting_http_methods * * @param string $method HTTP request method. */ - public function test_redirect_for_get_and_head_requests( $method ): void { + public function test_redirect_for_get_and_head_requests( string $method ): void { $_SERVER['REQUEST_METHOD'] = $method; $this->go_to( home_url( '/random/' ) ); @@ -970,9 +1113,9 @@ public function test_redirect_for_get_and_head_requests( $method ): void { } /** - * Data provider. + * Data provider for {@see self::test_redirect_for_get_and_head_requests()}. * - * @return array + * @return array */ public static function data_redirecting_http_methods(): array { return array( @@ -996,7 +1139,7 @@ public static function data_redirecting_http_methods(): array { * * @param string $structure Permalink structure. */ - public function test_page_with_slug_beginning_with_random_base_does_not_conflict( $structure ): void { + public function test_page_with_slug_beginning_with_random_base_does_not_conflict( string $structure ): void { global $wp; $this->set_permalink_structure( $structure ); @@ -1022,13 +1165,15 @@ public function test_page_with_slug_beginning_with_random_base_does_not_conflict } /** + * Ensure the random URL redirects when a page slug begins with the random base. + * * @dataProvider data_permalink_structures * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * * @param string $structure Permalink structure. */ - public function test_random_url_redirects_when_a_page_slug_begins_with_random_base( $structure ): void { + public function test_random_url_redirects_when_a_page_slug_begins_with_random_base( string $structure ): void { global $wp_rewrite; $this->set_permalink_structure( $structure ); @@ -1056,7 +1201,7 @@ public function test_random_url_redirects_when_a_page_slug_begins_with_random_ba * * @param string $structure Permalink structure. */ - public function test_page_named_random_takes_precedence_over_random_content_redirects( $structure ): void { + public function test_page_named_random_takes_precedence_over_random_content_redirects( string $structure ): void { global $wp_rewrite; $this->set_permalink_structure( $structure ); @@ -1079,9 +1224,10 @@ public function test_page_named_random_takes_precedence_over_random_content_redi } /** - * Data provider. + * Data provider for {@see self::test_page_with_slug_beginning_with_random_base_does_not_conflict()}, {@see self::test_random_url_redirects_when_a_page_slug_begins_with_random_base()}, + * and {@see self::test_page_named_random_takes_precedence_over_random_content_redirects()}. * - * @return array + * @return array */ public static function data_permalink_structures(): array { return array( @@ -1092,6 +1238,9 @@ public static function data_permalink_structures(): array { ); } + /** + * Ensure random archives redirect when a page named "random" exists. + */ public function test_random_archives_redirect_when_a_page_named_random_exists(): void { $this->create_post( array( @@ -1105,6 +1254,9 @@ public function test_random_archives_redirect_when_a_page_named_random_exists(): $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'Random archives should redirect when a page named random exists.' ); } + /** + * Ensure the random query string redirects when a page named "random" exists. + */ public function test_random_query_string_redirects_when_a_page_named_random_exists(): void { $this->create_post( array( @@ -1118,6 +1270,9 @@ public function test_random_query_string_redirects_when_a_page_named_random_exis $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'The random query string should redirect when a page named random exists.' ); } + /** + * Ensure a draft page named "random" does not take precedence over random content redirects. + */ public function test_draft_page_named_random_does_not_take_precedence(): void { $this->create_post( array( @@ -1133,11 +1288,13 @@ 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 * * @param callable $change Callback to change the page so it no longer conflicts. */ - public function test_random_content_redirects_resume_when_the_page_no_longer_conflicts( $change ): void { + public function test_random_content_redirects_resume_when_the_page_no_longer_conflicts( callable $change ): void { $page = $this->create_post( array( 'post_type' => 'page', @@ -1155,9 +1312,9 @@ public function test_random_content_redirects_resume_when_the_page_no_longer_con } /** - * Data provider. + * Data provider for {@see self::test_random_content_redirects_resume_when_the_page_no_longer_conflicts()}. * - * @return array + * @return array */ public static function data_page_named_random_changes(): array { return array( @@ -1204,6 +1361,9 @@ static function ( int $page_id ): void { ); } + /** + * Ensure publishing a draft page named "random" takes precedence over random content redirects. + */ public function test_publishing_a_draft_page_named_random_takes_precedence(): void { $page = $this->create_post( array( @@ -1221,6 +1381,9 @@ public function test_publishing_a_draft_page_named_random_takes_precedence(): vo $this->assertSame( $page, get_queried_object_id(), 'A newly published page named random should be queried.' ); } + /** + * Ensure saving pages and posts not at the random base does not flush the rewrite rules. + */ public function test_saving_unrelated_pages_does_not_flush_rewrite_rules(): void { $flushes = new MockAction(); add_filter( 'pre_update_option_rewrite_rules', array( $flushes, 'filter' ) ); @@ -1247,6 +1410,9 @@ public function test_saving_unrelated_pages_does_not_flush_rewrite_rules(): void * Opting out. */ + /** + * Ensure random content requests do not redirect when the feature is disabled. + */ public function test_no_redirect_when_disabled(): void { add_filter( 'wp_enable_random_content_redirect', '__return_false' ); @@ -1256,6 +1422,9 @@ public function test_no_redirect_when_disabled(): void { $this->assertQueryTrue( 'is_home', 'is_front_page' ); } + /** + * Ensure the main query is not randomized or limited when the feature is disabled. + */ public function test_main_query_is_unchanged_when_disabled(): void { add_filter( 'wp_enable_random_content_redirect', '__return_false' ); @@ -1265,6 +1434,9 @@ public function test_main_query_is_unchanged_when_disabled(): void { $this->assertSame( 2, $this->get_main_query()->post_count, 'The main query should not be limited when disabled.' ); } + /** + * Ensure the random URL is not routed when the feature is disabled. + */ public function test_random_url_is_not_routed_when_disabled(): void { add_filter( 'wp_enable_random_content_redirect', '__return_false' ); flush_rewrite_rules(); @@ -1279,6 +1451,9 @@ public function test_random_url_is_not_routed_when_disabled(): void { * Extensibility. */ + /** + * Ensure the `pre_get_posts` action can change the eligible post type. + */ public function test_pre_get_posts_can_change_the_eligible_post_type(): void { register_post_type( 'wptests_book', @@ -1303,6 +1478,9 @@ static function ( WP_Query $query ): void { $this->assertSame( get_permalink( $book ), $this->redirect_location, 'The pre_get_posts action should be able to change the eligible post type.' ); } + /** + * Ensure the `pre_get_posts` action cannot add non-randomable post types. + */ public function test_pre_get_posts_cannot_add_non_randomable_post_types(): void { $this->create_post( array( 'post_type' => 'page' ) ); @@ -1320,6 +1498,9 @@ static function ( WP_Query $query ): void { $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'The pre_get_posts action should not be able to add non-randomable post types.' ); } + /** + * Ensure the `pre_get_posts` action can exclude posts from the random selection. + */ public function test_pre_get_posts_can_exclude_posts(): void { add_action( 'pre_get_posts', @@ -1335,6 +1516,9 @@ static function ( WP_Query $query ): void { $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'The pre_get_posts action should be able to exclude posts.' ); } + /** + * Ensure theme customizations of the archive order and limit do not override the random query. + */ public function test_pre_get_posts_archive_customizations_do_not_override_the_random_order(): void { // Typical theme customization of the main query for archives. add_action( @@ -1356,6 +1540,9 @@ static function ( WP_Query $query ): void { $this->assertStringContainsString( 'LIMIT 0, 1', $request, 'Only one post should be selected when archive customizations set the number of posts.' ); } + /** + * Ensure secondary queries are not random content queries. + */ public function test_secondary_queries_are_not_random_content_queries(): void { $this->go_to( home_url( '/random/' ) ); @@ -1365,6 +1552,9 @@ public function test_secondary_queries_are_not_random_content_queries(): void { $this->assertQueryIsNotRandomized( $query, 'Secondary queries should not be randomized.' ); } + /** + * Ensure the main query for the random URL is a random content query. + */ public function test_wp_is_random_content_query_for_main_query(): void { $this->go_to( home_url( '/random/' ) ); @@ -1372,6 +1562,8 @@ public function test_wp_is_random_content_query_for_main_query(): void { } /** + * Ensure the `is_random()` conditional tag matches the request. + * * @dataProvider data_is_random * * @covers ::is_random @@ -1380,7 +1572,7 @@ public function test_wp_is_random_content_query_for_main_query(): void { * @param string $path Path to request. * @param bool $expected Expected result. */ - public function test_is_random( $path, $expected ): void { + public function test_is_random( string $path, bool $expected ): void { $this->go_to( home_url( $path ) ); $this->assertSame( $expected, is_random(), 'The is_random() conditional tag returned an unexpected value.' ); @@ -1388,9 +1580,9 @@ public function test_is_random( $path, $expected ): void { } /** - * Data provider. + * Data provider for {@see self::test_is_random()}. * - * @return array + * @return array */ public static function data_is_random(): array { return array( @@ -1409,6 +1601,8 @@ public static function data_is_random(): array { } /** + * Ensure secondary queries with the random query variable are for random content. + * * @covers WP_Query::is_random */ public function test_secondary_query_is_random_with_the_random_query_var(): void { @@ -1419,6 +1613,8 @@ public function test_secondary_query_is_random_with_the_random_query_var(): void } /** + * Ensure the query is not for random content once it is set to a 404. + * * @covers WP_Query::is_random */ public function test_is_random_is_reset_when_the_query_is_a_404(): void { @@ -1430,6 +1626,8 @@ public function test_is_random_is_reset_when_the_query_is_a_404(): void { } /** + * Ensure the `is_random()` conditional tag returns false before the query is run. + * * @covers ::is_random * * @expectedIncorrectUsage is_random @@ -1440,6 +1638,9 @@ public function test_is_random_before_the_query_is_run(): void { $this->assertFalse( is_random(), 'The is_random() conditional tag should return false before the query is run.' ); } + /** + * Ensure the main query is not randomized in the admin. + */ public function test_main_query_is_not_randomized_in_the_admin(): void { set_current_screen( 'edit.php' ); @@ -1454,6 +1655,9 @@ public function test_main_query_is_not_randomized_in_the_admin(): void { $this->assertQueryIsNotRandomized( $this->get_main_query(), 'Admin queries should not be randomized.' ); } + /** + * Ensure the request redirects when the `the_posts` filter returns a non-list array. + */ public function test_redirect_when_the_posts_filter_returns_a_non_list_array(): void { add_filter( 'the_posts', @@ -1467,6 +1671,9 @@ static function ( array $posts ): array { $this->assertSame( get_permalink( self::$arts_post ), $this->redirect_location, 'A non-list array returned by the posts filter should still redirect.' ); } + /** + * Ensure the request does not redirect when the permalink is empty. + */ public function test_no_redirect_when_the_permalink_is_empty(): void { add_filter( 'post_link', '__return_empty_string' ); @@ -1475,6 +1682,9 @@ public function test_no_redirect_when_the_permalink_is_empty(): void { $this->assertNotRedirected( 'An empty permalink should not redirect.' ); } + /** + * Ensure the redirect happens before the `wp` action fires. + */ public function test_redirect_happens_before_the_wp_action(): void { $wp = new MockAction(); add_action( 'wp', array( $wp, 'action' ) ); @@ -1498,29 +1708,48 @@ static function ( string $location ) use ( $wp, &$redirected_before_wp ): string * Database queries. */ - public function test_random_post_is_selected_with_a_single_database_query(): void { + /** + * The random order is applied to post IDs only, so the database sorts IDs rather than + * full rows, including post content, of every eligible post. + * + * @global wpdb $wpdb WordPress database abstraction object. + */ + public function test_random_post_is_selected_by_a_single_random_id_query(): void { + global $wpdb; + $this->go_to_and_record_queries( home_url( '/random/' ) ); - $this->assertNotNull( $this->redirect_location, 'The single database query request did not redirect.' ); - $this->assertCount( 1, $this->queries, "Only one database query should be made to select the random post:\n" . implode( "\n", $this->queries ) ); - $this->assertStringContainsString( 'RAND()', $this->queries[0], 'The random query should be the only query made.' ); + $this->assertNotNull( $this->redirect_location, 'The request did not redirect.' ); + + $random_queries = array_values( preg_grep( '/RAND\(\)/', $this->queries ) ); + $this->assertCount( 1, $random_queries, "Exactly one random query should be made:\n" . implode( "\n", $this->queries ) ); + $this->assertMatchesRegularExpression( "/^\s*SELECT\s+{$wpdb->posts}\.ID\s+FROM/", $random_queries[0], 'The random query should select post IDs only.' ); + $this->assertMatchesRegularExpression( '/ORDER BY RAND\(\)/', $random_queries[0], 'The query should use a random order.' ); + $this->assertMatchesRegularExpression( '/LIMIT 0, 1\s*$/', $random_queries[0], 'The query should select a single post.' ); + $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $random_queries[0], 'The query should not count found rows.' ); } /** + * At most one more query is made, to load the selected post by its ID. + * * @global wpdb $wpdb WordPress database abstraction object. */ - public function test_random_query_selects_a_single_full_post_row(): void { + public function test_random_post_is_loaded_by_id_after_the_random_query(): void { global $wpdb; $this->go_to_and_record_queries( home_url( '/random/' ) ); - $this->assertMatchesRegularExpression( "/SELECT\s+{$wpdb->posts}\.\*/", $this->queries[0], 'The query should not be split into an ID query and a post query.' ); - $this->assertMatchesRegularExpression( '/ORDER BY RAND\(\)/', $this->queries[0], 'The query should use a random order.' ); - $this->assertMatchesRegularExpression( '/LIMIT 0, 1\s*$/', $this->queries[0], 'The query should select a single post.' ); - $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $this->queries[0], 'The query should not count found rows.' ); + $this->assertLessThanOrEqual( 2, count( $this->queries ), "No more than two database queries should be made to select the random post:\n" . implode( "\n", $this->queries ) ); + $this->assertStringContainsString( 'RAND()', $this->queries[0], 'The random query should be made first.' ); + + if ( isset( $this->queries[1] ) ) { + $this->assertMatchesRegularExpression( "/^\s*SELECT\s+{$wpdb->posts}\.\*\s+FROM\s+{$wpdb->posts}\s+WHERE\s+ID\s+IN\s+\(\d+\)/", $this->queries[1], 'The second query should load the selected post by its ID.' ); + } } /** + * Ensure the random query does not prime the post meta or term caches. + * * @global wpdb $wpdb WordPress database abstraction object. */ public function test_random_query_does_not_prime_meta_or_term_caches(): void { @@ -1535,21 +1764,9 @@ public function test_random_query_does_not_prime_meta_or_term_caches(): void { } } - public function test_random_query_is_not_split_with_persistent_object_cache(): void { - $using_ext_cache = wp_using_ext_object_cache( true ); - - $this->go_to_and_record_queries( home_url( '/random/' ) ); - - wp_using_ext_object_cache( $using_ext_cache ); - - $this->assertCount( 1, $this->queries, "Only one database query should be made with a persistent object cache:\n" . implode( "\n", $this->queries ) ); - } - - public function test_split_the_query_is_unchanged_for_other_queries(): void { - $this->assertTrue( wp_random_content_split_the_query( true, new WP_Query() ), 'Splitting should remain enabled for non-random queries.' ); - $this->assertFalse( wp_random_content_split_the_query( false, new WP_Query() ), 'Splitting should remain disabled for non-random queries.' ); - } - + /** + * Ensure different posts are selected across random requests. + */ public function test_random_selection_varies_between_requests(): void { for ( $i = 0; $i < 8; $i++ ) { $this->create_post(); @@ -1571,13 +1788,15 @@ 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 * * @param string $path Request path. * @param string $query_var Query variable set by a theme. * @param mixed $value Query variable value. */ - public function test_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit( $path, $query_var, $value ): void { + public function test_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit( string $path, string $query_var, $value ): void { add_action( 'pre_get_posts', static function ( WP_Query $query ) use ( $query_var, $value ): void { @@ -1596,11 +1815,11 @@ static function ( WP_Query $query ) use ( $query_var, $value ): void { } /** - * Data provider. + * Data provider for {@see self::test_random_query_is_limited_to_one_post_when_a_theme_overrides_the_limit()}. * * The limit is applied differently to the home page and archives, see WP_Query::get_posts(). * - * @return array + * @return array */ public static function data_query_vars_overriding_the_limit(): array { $overrides = array( @@ -1616,13 +1835,20 @@ public static function data_query_vars_overriding_the_limit(): array { 'archive' => '/random/category/arts/', ) as $context => $path ) { foreach ( $overrides as $name => $override ) { - $data[ "{$context}: {$name}" ] = array( $path, $override[0], $override[1] ); + $data[ "{$context}: {$name}" ] = array( + 'path' => $path, + 'query_var' => $override[0], + 'value' => $override[1], + ); } } return $data; } + /** + * Ensure an offset set by a theme does not prevent the redirect. + */ public function test_random_redirect_ignores_an_offset_set_by_a_theme(): void { add_action( 'pre_get_posts', @@ -1642,6 +1868,9 @@ static function ( WP_Query $query ): void { * Redirect locations. */ + /** + * Ensure the request does not redirect to a permalink on another site. + */ public function test_no_redirect_when_the_permalink_is_on_another_site(): void { add_filter( 'post_link', @@ -1655,6 +1884,9 @@ static function (): string { $this->assertNotRedirected( 'Off-site permalinks must not fall back to a redirect to the dashboard.' ); } + /** + * Ensure the request redirects to a permalink on an allowed redirect host. + */ public function test_redirect_to_a_permalink_on_an_allowed_host(): void { add_filter( 'allowed_redirect_hosts', @@ -1675,6 +1907,9 @@ static function (): string { $this->assertSame( 'https://cdn.example.org/post/', $this->redirect_location, 'Permalinks on an allowed host should be redirected to.' ); } + /** + * Ensure the request does not redirect to a global post when the main query has no post. + */ public function test_no_redirect_when_the_main_query_has_no_post(): void { add_filter( 'the_posts', '__return_empty_array' ); $this->go_to( home_url( '/random/' ) ); @@ -1695,6 +1930,9 @@ public function test_no_redirect_when_the_main_query_has_no_post(): void { * Randomable post types: edge cases. */ + /** + * Ensure random searches do not select randomable post types excluded from search. + */ public function test_random_search_excludes_randomable_post_types_excluded_from_search(): void { register_post_type( 'wptests_unsearchable', @@ -1716,6 +1954,9 @@ public function test_random_search_excludes_randomable_post_types_excluded_from_ $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'Random search results should match the search results.' ); } + /** + * Ensure randomable post types excluded from search can be requested directly. + */ public function test_randomable_post_type_excluded_from_search_can_be_requested_directly(): void { register_post_type( 'wptests_unsearchable', @@ -1732,6 +1973,9 @@ public function test_randomable_post_type_excluded_from_search_can_be_requested_ $this->assertSame( get_permalink( $post_id ), $this->redirect_location, 'Randomable post types excluded from search should be selectable directly.' ); } + /** + * Ensure randomable attachments are eligible for taxonomies registered for attachment MIME types. + */ public function test_random_post_types_include_attachments_for_attachment_mime_type_taxonomies(): void { $filter = static function ( array $args, string $post_type ): array { if ( 'attachment' === $post_type ) { @@ -1758,6 +2002,9 @@ public function test_random_post_types_include_attachments_for_attachment_mime_t $this->assertSame( array( 'attachment' ), $post_types, 'Attachments should be eligible for attachment MIME type taxonomies.' ); } + /** + * Ensure non-string post types set by plugins are ignored. + */ public function test_non_string_post_types_set_by_plugins_are_ignored(): void { add_action( 'pre_get_posts', @@ -1778,12 +2025,14 @@ static function ( WP_Query $query ): void { */ /** + * Ensure the enabled filter value is cast to a boolean. + * * @dataProvider data_enabled_filter_values * * @param mixed $value Value returned by the filter. * @param bool $expected Expected result. */ - public function test_wp_is_random_content_redirect_enabled_returns_a_boolean( $value, $expected ): void { + public function test_wp_is_random_content_redirect_enabled_returns_a_boolean( $value, bool $expected ): void { add_filter( 'wp_enable_random_content_redirect', static function () use ( $value ) { @@ -1795,9 +2044,9 @@ static function () use ( $value ) { } /** - * Data provider. + * Data provider for {@see self::test_wp_is_random_content_redirect_enabled_returns_a_boolean()}. * - * @return array + * @return array */ public static function data_enabled_filter_values(): array { return array( @@ -1810,6 +2059,9 @@ public static function data_enabled_filter_values(): array { ); } + /** + * Ensure stale rewrite rules do not redirect or randomize the query when the feature is disabled. + */ public function test_no_redirect_when_disabled_without_flushing_the_rewrite_rules(): void { add_filter( 'wp_enable_random_content_redirect', '__return_false' ); @@ -1823,6 +2075,9 @@ public function test_no_redirect_when_disabled_without_flushing_the_rewrite_rule * Conflicts with pages: lifecycle guards. */ + /** + * Ensure creating a page named "random" flushes the rewrite rules once. + */ public function test_creating_a_page_named_random_flushes_the_rewrite_rules(): void { $flushes = new MockAction(); add_filter( 'pre_update_option_rewrite_rules', array( $flushes, 'filter' ) ); @@ -1837,6 +2092,9 @@ public function test_creating_a_page_named_random_flushes_the_rewrite_rules(): v $this->assertSame( 1, $flushes->get_call_count(), 'Creating a page named random should flush the rewrite rules once.' ); } + /** + * Ensure a page named "random" does not flush the rewrite rules with plain permalinks. + */ public function test_page_named_random_does_not_flush_rewrite_rules_with_plain_permalinks(): void { $this->set_permalink_structure( '' ); $flushes = new MockAction(); @@ -1852,6 +2110,9 @@ public function test_page_named_random_does_not_flush_rewrite_rules_with_plain_p $this->assertSame( 0, $flushes->get_call_count(), 'Plain permalinks have no rewrite rules to update.' ); } + /** + * Ensure a page named "random" does not flush the rewrite rules when the feature is disabled. + */ public function test_page_named_random_does_not_flush_rewrite_rules_when_disabled(): void { add_filter( 'wp_enable_random_content_redirect', '__return_false' ); $flushes = new MockAction(); @@ -1868,6 +2129,8 @@ public function test_page_named_random_does_not_flush_rewrite_rules_when_disable } /** + * Ensure a page at a custom random base takes precedence over random content redirects. + * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ public function test_page_at_custom_random_base_takes_precedence(): void { @@ -1890,6 +2153,8 @@ public function test_page_at_custom_random_base_takes_precedence(): void { } /** + * Ensure a page named "random" does not conflict with a custom random base. + * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ public function test_page_named_random_does_not_conflict_with_a_custom_random_base(): void { @@ -1913,6 +2178,9 @@ public function test_page_named_random_does_not_conflict_with_a_custom_random_ba $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'A page named random should not conflict with a custom random base.' ); } + /** + * Ensure a private page named "random" does not take precedence over random content redirects. + */ public function test_private_page_named_random_does_not_take_precedence(): void { $this->create_post( array( @@ -1927,6 +2195,9 @@ public function test_private_page_named_random_does_not_take_precedence(): void $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'Pages visitors cannot view should not replace random redirects.' ); } + /** + * Ensure a password protected page named "random" takes precedence over random content redirects. + */ public function test_password_protected_page_named_random_takes_precedence(): void { $page = $this->create_post( array( @@ -1947,13 +2218,15 @@ 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 * * @global WP $wp Current WordPress environment instance. * * @param string $path Request path. */ - public function test_no_redirect_for_paths_without_random_rules( $path ): void { + public function test_no_redirect_for_paths_without_random_rules( string $path ): void { global $wp; $this->go_to( home_url( $path ) ); @@ -1963,9 +2236,9 @@ public function test_no_redirect_for_paths_without_random_rules( $path ): void { } /** - * Data provider. + * Data provider for {@see self::test_no_redirect_for_paths_without_random_rules()}. * - * @return array + * @return array */ public static function data_unsupported_random_paths(): array { return array( @@ -1974,6 +2247,9 @@ public static function data_unsupported_random_paths(): array { ); } + /** + * Ensure random content requests without a post send a 404 status. + */ public function test_random_requests_without_a_post_send_a_404_status(): void { $status = new MockAction(); add_filter( 'status_header', array( $status, 'filter' ), 10, 2 ); @@ -1991,6 +2267,9 @@ public function test_random_requests_without_a_post_send_a_404_status(): void { * WP::handle_random(). */ + /** + * Ensure `WP::handle_random()` ignores requests without the random query variable. + */ public function test_handle_random_ignores_requests_without_the_random_query_var(): void { $wp = new WP(); $wp->query_vars = array( 'category_name' => 'arts' ); @@ -1998,6 +2277,9 @@ public function test_handle_random_ignores_requests_without_the_random_query_var $this->assertFalse( $wp->handle_random(), 'Requests without the random query variable should not be handled.' ); } + /** + * Ensure `WP::handle_random()` ignores random content requests when the feature is disabled. + */ public function test_handle_random_ignores_random_requests_when_disabled(): void { add_filter( 'wp_enable_random_content_redirect', '__return_false' ); @@ -2007,6 +2289,9 @@ public function test_handle_random_ignores_random_requests_when_disabled(): void $this->assertFalse( $wp->handle_random(), 'Random content requests should not be handled when the feature is disabled.' ); } + /** + * Ensure `WP::handle_random()` runs the main query for random content requests. + */ public function test_handle_random_runs_the_main_query_for_random_requests(): void { $wp = new WP(); $wp->query_vars = array( 'random' => '' ); @@ -2018,6 +2303,8 @@ 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 * * @param string $path Path to request. @@ -2032,9 +2319,9 @@ public function test_main_query_is_run_once_for_random_requests( string $path ): } /** - * Data provider for test_main_query_is_run_once_for_random_requests(). + * Data provider for {@see self::test_main_query_is_run_once_for_random_requests()}. * - * @return array + * @return array */ public static function data_random_requests(): array { return array( @@ -2043,6 +2330,9 @@ public static function data_random_requests(): array { ); } + /** + * Ensure the random content query callbacks are removed after the main query. + */ public function test_random_content_callbacks_are_removed_after_the_main_query(): void { $this->go_to( home_url( '/random/' ) ); @@ -2051,6 +2341,9 @@ public function test_random_content_callbacks_are_removed_after_the_main_query() $this->assertFalse( has_filter( 'post_limits', 'wp_random_content_post_limits' ), 'The post_limits callback should be removed after the main query.' ); } + /** + * Ensure the `posts_orderby` and `post_limits` filters do not override the random order or limit. + */ public function test_sql_customizations_do_not_override_the_random_order_or_limit(): void { add_filter( 'posts_orderby', @@ -2073,6 +2366,9 @@ static function (): string { $this->assertStringContainsString( 'LIMIT 0, 1', $request, 'The post_limits filter should not override the random limit.' ); } + /** + * Ensure suppressing filters does not bypass the random order or limit. + */ public function test_suppressed_filters_do_not_bypass_the_random_order_or_limit(): void { add_action( 'pre_get_posts', @@ -2096,6 +2392,8 @@ 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 * * @global WP_Rewrite $wp_rewrite WordPress rewrite component. @@ -2120,9 +2418,9 @@ public function test_post_named_random_takes_precedence_over_random_content_redi } /** - * Data provider. + * Data provider for {@see self::test_post_named_random_takes_precedence_over_random_content_redirects()}. * - * @return array + * @return array */ public static function data_post_name_at_the_root_structures(): array { return array( @@ -2131,6 +2429,9 @@ public static function data_post_name_at_the_root_structures(): array { ); } + /** + * Ensure random archives redirect when a post named "random" exists. + */ public function test_random_archives_redirect_when_a_post_named_random_exists(): void { $this->create_post( array( 'post_name' => 'random' ) ); @@ -2139,6 +2440,9 @@ public function test_random_archives_redirect_when_a_post_named_random_exists(): $this->assertSame( get_permalink( self::$sport_post ), $this->redirect_location, 'Random archives should redirect when a post named random exists.' ); } + /** + * Ensure a post named "random" does not conflict when posts have a blog prefix. + */ public function test_post_named_random_does_not_conflict_with_a_blog_prefix(): void { $this->set_permalink_structure( '/blog/%postname%/' ); create_initial_taxonomies(); @@ -2151,6 +2455,9 @@ public function test_post_named_random_does_not_conflict_with_a_blog_prefix(): v $this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post, $post ), 'Random content requests should redirect when posts have a blog prefix.' ); } + /** + * Ensure creating a post named "random" flushes the rewrite rules once. + */ public function test_creating_a_post_named_random_flushes_the_rewrite_rules(): void { $flushes = new MockAction(); add_filter( 'pre_update_option_rewrite_rules', array( $flushes, 'filter' ) ); @@ -2160,6 +2467,9 @@ public function test_creating_a_post_named_random_flushes_the_rewrite_rules(): v $this->assertSame( 1, $flushes->get_call_count(), 'Creating a post named random should flush the rewrite rules once.' ); } + /** + * Ensure a post named "random" does not flush the rewrite rules when posts have a blog prefix. + */ public function test_post_named_random_does_not_flush_rewrite_rules_with_a_blog_prefix(): void { $this->set_permalink_structure( '/blog/%postname%/' ); $flushes = new MockAction(); @@ -2170,6 +2480,9 @@ public function test_post_named_random_does_not_flush_rewrite_rules_with_a_blog_ $this->assertSame( 0, $flushes->get_call_count(), 'A post named random should not flush the rewrite rules when posts have a blog prefix.' ); } + /** + * Ensure random content redirects resume when a post named "random" is unpublished. + */ public function test_unpublishing_a_post_named_random_resumes_random_content_redirects(): void { $post = $this->create_post( array( 'post_name' => 'random' ) ); diff --git a/tests/phpunit/tests/wp/handleRandom.php b/tests/phpunit/tests/wp/handleRandom.php new file mode 100644 index 0000000000000..0e669fd36b186 --- /dev/null +++ b/tests/phpunit/tests/wp/handleRandom.php @@ -0,0 +1,216 @@ +post->create_many( 5 ); + } + + /** + * Captures redirects before each test. + */ + public function set_up(): void { + parent::set_up(); + + $this->redirect_location = null; + + // Capture redirects and prevent `exit` from ending the test run. + add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ) ); + } + + /** + * Captures the redirect and prevents it from being sent. + * + * @param string $location Redirect location. + * @return false + */ + public function filter_wp_redirect( string $location ) { + $this->redirect_location = $location; + + return false; + } + + /** + * Handles a random content request with a new main query. + * + * Unlike WP_UnitTestCase_Base::go_to(), the request is handled without flushing the + * cache or running the rest of WP::main(). + * + * @param array $query_vars Query variables for the request. + * @return bool Whether the main query was run for a random content request. + */ + protected function handle_random( array $query_vars ): bool { + $GLOBALS['wp_the_query'] = new WP_Query(); + $GLOBALS['wp_query'] = $GLOBALS['wp_the_query']; + + $wp = new WP(); + $wp->query_vars = $query_vars; + + return $wp->handle_random(); + } + + /** + * Ensure requests without the random query variable are not handled. + * + * @ticket 64498 + * + * @dataProvider data_requests_without_the_random_query_var + * + * @param array $query_vars Query variables for the request. + */ + public function test_requests_without_the_random_query_var_are_not_handled( array $query_vars ): void { + $handled = $this->handle_random( $query_vars ); + + $this->assertFalse( $handled, 'Requests without the random query variable should not be handled.' ); + $this->assertNull( $GLOBALS['wp_the_query']->request, 'The main query should be left for WP::main() to run.' ); + $this->assertNull( $this->redirect_location, 'The request should not redirect.' ); + } + + /** + * Data provider for {@see self::test_requests_without_the_random_query_var_are_not_handled()}. + * + * @return array }> + */ + public function data_requests_without_the_random_query_var(): array { + return array( + 'no query vars' => array( array() ), + 'other query vars' => array( array( 'post_type' => 'post' ) ), + ); + } + + /** + * Ensure random content requests are not handled when the feature is disabled. + * + * @ticket 64498 + */ + public function test_random_content_requests_are_not_handled_when_disabled(): void { + add_filter( 'wp_enable_random_content_redirect', '__return_false' ); + + $handled = $this->handle_random( array( 'random' => '' ) ); + + $this->assertFalse( $handled, 'Random content requests should not be handled when the feature is disabled.' ); + $this->assertNull( $GLOBALS['wp_the_query']->request, 'The main query should be left for WP::main() to run.' ); + $this->assertNull( $this->redirect_location, 'The request should not redirect.' ); + } + + /** + * Ensure random content queries are not served from the query cache. + * + * If the ID query were cached, the same post would be selected until the cache is + * invalidated. + * + * @ticket 64498 + */ + public function test_random_content_queries_are_not_cached(): void { + $queries = new MockAction(); + add_filter( 'query', array( $queries, 'filter' ) ); + + $this->handle_random( array( 'random' => '' ) ); + $this->handle_random( array( 'random' => '' ) ); + + $random_queries = preg_grep( '/RAND\(\)/', array_column( $queries->get_args(), 0 ) ); + + $this->assertCount( 2, $random_queries, 'The random ID query should run for each request rather than reuse a cached result.' ); + } + + /** + * Ensure the main query runs unchanged for requests with the random query variable + * that are not random content requests. + * + * The main query must not be run again by WP::main() and any 404 is left to + * WP::handle_404(). + * + * @ticket 64498 + * + * @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. + */ + public function test_main_query_runs_unchanged_for_requests_that_are_not_random_content_requests( array $query_vars, string $request_method ): void { + $_SERVER['REQUEST_METHOD'] = $request_method; + + $handled = $this->handle_random( $query_vars ); + + $request = $GLOBALS['wp_the_query']->request; + + $this->assertTrue( $handled, 'The main query should be reported as run.' ); + $this->assertIsString( $request, 'The main query was not run.' ); + $this->assertStringNotContainsString( 'RAND()', $request, 'The main query should not be randomized.' ); + $this->assertNull( $this->redirect_location, 'The request should not redirect.' ); + $this->assertFalse( is_404(), 'The request should not be set to a 404.' ); + } + + /** + * Data provider for {@see self::test_main_query_runs_unchanged_for_requests_that_are_not_random_content_requests()}. + * + * @return array, 1: non-falsy-string }> + */ + public function data_requests_that_are_not_random_content_requests(): array { + return array( + 'post request' => array( array( 'random' => '' ), 'POST' ), + 'non-randomable without posts' => array( + array( + 'random' => '', + 'post_type' => 'page', + ), + 'GET', + ), + ); + } + + /** + * Ensure the request continues with the selected post when it cannot be redirected to. + * + * @ticket 64498 + * + * @dataProvider data_permalinks_that_cannot_be_redirected_to + * + * @param string $permalink Permalink of the selected post. + */ + public function test_request_continues_with_the_selected_post_when_it_is_not_redirected( string $permalink ): void { + add_filter( + 'post_link', + static function () use ( $permalink ): string { + return $permalink; + } + ); + + $handled = $this->handle_random( array( 'random' => '' ) ); + + $this->assertTrue( $handled, 'The main query should be reported as run.' ); + $this->assertNull( $this->redirect_location, 'The request should not redirect.' ); + $this->assertInstanceOf( WP_Post::class, $GLOBALS['wp_the_query']->post, 'The selected post should remain in the main query.' ); + $this->assertFalse( is_404(), 'The request should not be set to a 404.' ); + } + + /** + * Data provider for {@see self::test_request_continues_with_the_selected_post_when_it_is_not_redirected()}. + * + * @return array + */ + public function data_permalinks_that_cannot_be_redirected_to(): array { + return array( + 'empty permalink' => array( '' ), + 'off-site permalink' => array( 'https://elsewhere.example/post/' ), + ); + } +}