From b984c49c423130653df8090b228bb3cc0278e34e Mon Sep 17 00:00:00 2001 From: Lae <30905719+mlaetitia@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:24:00 +0100 Subject: [PATCH 1/3] Rewrite Rules: Add opt-in support for random content redirects. Adds wp_random_content_redirect(), hooked on template_redirect, which redirects ?random requests to a randomly selected published post. Disabled by default; enable with the enable_random_content_redirect filter. Picks via COUNT plus a random OFFSET instead of ORDER BY RAND(). See https://core.trac.wordpress.org/ticket/64498 Co-Authored-By: Claude Fable 5 --- src/wp-includes/default-filters.php | 3 + src/wp-includes/query.php | 109 +++++++++ .../tests/rewrite/randomContentRedirect.php | 207 ++++++++++++++++++ 3 files changed, 319 insertions(+) create mode 100644 tests/phpunit/tests/rewrite/randomContentRedirect.php diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 12ca0045b98b4..9347fd0de60c6 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -489,6 +489,9 @@ add_action( 'post_updated', 'wp_check_for_changed_dates', 12, 3 ); add_action( 'attachment_updated', 'wp_check_for_changed_dates', 12, 3 ); +// Redirect random content requests (disabled by default, see 'enable_random_content_redirect'). +add_action( 'template_redirect', 'wp_random_content_redirect' ); + // Nonce check for post previews. add_action( 'init', '_show_post_preview' ); diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index 60571c01cb880..6ecc9b5103102 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -1235,6 +1235,115 @@ function _find_post_by_old_date( $post_type ) { return $id; } +/** + * Redirects requests for random content to a randomly selected published post. + * + * Handles requests using the `random` query parameter, e.g. `example.com/?random`. + * The `random_post_type` parameter restricts the pick to a post type (default `post`), + * and the `random_cat_id` parameter restricts it to a category (for post types using + * the `category` taxonomy). Only published, non-password-protected content from + * viewable post types is considered, and only GET/HEAD requests are handled. + * + * Disabled by default. To enable: + * + * add_filter( 'enable_random_content_redirect', '__return_true' ); + * + * @since 7.2.0 + */ +function wp_random_content_redirect() { + /** + * Filters whether random content redirects are enabled. + * + * @since 7.2.0 + * + * @param bool $enabled Whether `?random` requests redirect to random content. Default false. + */ + if ( ! apply_filters( 'enable_random_content_redirect', false ) ) { + return; + } + + // The `random` parameter is a flag, present with or without a value. + if ( ! isset( $_GET['random'] ) ) { + return; + } + + if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ), true ) ) { + return; + } + + $post_type = 'post'; + + if ( isset( $_GET['random_post_type'] ) ) { + $post_type = sanitize_key( wp_unslash( $_GET['random_post_type'] ) ); + + if ( ! post_type_exists( $post_type ) || ! is_post_type_viewable( $post_type ) ) { + return; + } + } + + $args = array( + 'post_type' => $post_type, + 'post_status' => 'publish', + 'has_password' => false, + 'fields' => 'ids', + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'ASC', + 'ignore_sticky_posts' => true, + ); + + if ( isset( $_GET['random_cat_id'] ) ) { + $cat_id = absint( wp_unslash( $_GET['random_cat_id'] ) ); + + if ( $cat_id < 1 ) { + return; + } + + $args['cat'] = $cat_id; + } + + // Pick via COUNT plus a random OFFSET rather than `ORDER BY RAND()`, + // which randomizes and sorts every candidate row on each request. + $query = new WP_Query( $args ); + $post_count = (int) $query->found_posts; + + if ( $post_count < 1 ) { + return; + } + + if ( $post_count > 1 ) { + $args['offset'] = wp_rand( 0, $post_count - 1 ); + $args['no_found_rows'] = true; + + $query = new WP_Query( $args ); + } + + if ( empty( $query->posts ) ) { + return; + } + + $link = get_permalink( $query->posts[0] ); + + /** + * Filters the random content redirect URL. + * + * Returning a falsey value cancels the redirect. + * + * @since 7.2.0 + * + * @param string $link The redirect URL. + */ + $link = apply_filters( 'random_content_redirect_url', $link ); + + if ( ! $link ) { + return; + } + + // Temporary redirect, so clients do not cache the randomly picked target. + wp_safe_redirect( $link, 302 ); + exit; +} + /** * Set up global post data. * diff --git a/tests/phpunit/tests/rewrite/randomContentRedirect.php b/tests/phpunit/tests/rewrite/randomContentRedirect.php new file mode 100644 index 0000000000000..12c60563fc29d --- /dev/null +++ b/tests/phpunit/tests/rewrite/randomContentRedirect.php @@ -0,0 +1,207 @@ +original_request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : null; + + $_GET['random'] = ''; + $_SERVER['REQUEST_METHOD'] = 'GET'; + } + + public function tear_down() { + $this->random_content_redirect_url = null; + + unset( $_GET['random'], $_GET['random_post_type'], $_GET['random_cat_id'] ); + + if ( null === $this->original_request_method ) { + unset( $_SERVER['REQUEST_METHOD'] ); + } else { + $_SERVER['REQUEST_METHOD'] = $this->original_request_method; + } + + parent::tear_down(); + } + + public function test_hook_is_registered() { + $this->assertSame( 10, has_action( 'template_redirect', 'wp_random_content_redirect' ) ); + } + + public function test_disabled_by_default() { + remove_filter( 'enable_random_content_redirect', '__return_true' ); + + self::factory()->post->create(); + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function test_no_random_parameter_does_not_redirect() { + unset( $_GET['random'] ); + + self::factory()->post->create(); + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function test_redirects_to_a_published_post() { + $post_ids = self::factory()->post->create_many( 3 ); + + wp_random_content_redirect(); + $this->assertContains( $this->random_content_redirect_url, array_map( 'get_permalink', $post_ids ) ); + } + + public function test_post_request_does_not_redirect() { + $_SERVER['REQUEST_METHOD'] = 'POST'; + + self::factory()->post->create(); + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function test_head_request_redirects() { + $_SERVER['REQUEST_METHOD'] = 'HEAD'; + + $post_id = self::factory()->post->create(); + + wp_random_content_redirect(); + $this->assertSame( get_permalink( $post_id ), $this->random_content_redirect_url ); + } + + public function test_excludes_password_protected_posts() { + $public_id = self::factory()->post->create(); + self::factory()->post->create_many( 2, array( 'post_password' => 'secret' ) ); + + wp_random_content_redirect(); + $this->assertSame( get_permalink( $public_id ), $this->random_content_redirect_url ); + } + + public function test_excludes_unpublished_posts() { + $public_id = self::factory()->post->create(); + self::factory()->post->create( array( 'post_status' => 'draft' ) ); + self::factory()->post->create( array( 'post_status' => 'private' ) ); + + wp_random_content_redirect(); + $this->assertSame( get_permalink( $public_id ), $this->random_content_redirect_url ); + } + + public function test_random_post_type_parameter() { + self::factory()->post->create(); + $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); + + $_GET['random_post_type'] = 'page'; + + wp_random_content_redirect(); + $this->assertSame( get_permalink( $page_id ), $this->random_content_redirect_url ); + } + + public function test_invalid_post_type_does_not_redirect() { + self::factory()->post->create(); + + $_GET['random_post_type'] = 'nonexistent_type'; + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function test_non_viewable_post_type_does_not_redirect() { + register_post_type( 'wptests_hidden', array( 'public' => false ) ); + + self::factory()->post->create(); + self::factory()->post->create( array( 'post_type' => 'wptests_hidden' ) ); + + $_GET['random_post_type'] = 'wptests_hidden'; + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function test_random_cat_id_parameter() { + $cat_id = self::factory()->category->create(); + $in_cat_id = self::factory()->post->create( array( 'post_category' => array( $cat_id ) ) ); + self::factory()->post->create_many( 3 ); + + $_GET['random_cat_id'] = (string) $cat_id; + + wp_random_content_redirect(); + $this->assertSame( get_permalink( $in_cat_id ), $this->random_content_redirect_url ); + } + + public function test_nonexistent_category_does_not_redirect() { + self::factory()->post->create(); + + $_GET['random_cat_id'] = '99999'; + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + /** + * @dataProvider data_invalid_category_ids + * + * @param string $cat_id Invalid category ID value. + */ + public function test_invalid_category_id_does_not_redirect( $cat_id ) { + self::factory()->post->create(); + + $_GET['random_cat_id'] = $cat_id; + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function data_invalid_category_ids() { + return array( + 'zero' => array( '0' ), + 'negative' => array( '-5' ), + 'non-numeric' => array( 'foo' ), + ); + } + + public function test_redirect_url_is_filterable() { + self::factory()->post->create(); + + add_filter( + 'random_content_redirect_url', + static function () { + return home_url( '/custom-target/' ); + }, + 9 + ); + + wp_random_content_redirect(); + $this->assertSame( home_url( '/custom-target/' ), $this->random_content_redirect_url ); + } + + public function test_no_posts_does_not_redirect() { + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + + public function test_single_post_redirects_to_it() { + $post_id = self::factory()->post->create(); + + wp_random_content_redirect(); + $this->assertSame( get_permalink( $post_id ), $this->random_content_redirect_url ); + } + + public function filter_random_content_redirect_url( $url ) { + $this->random_content_redirect_url = $url; + return false; + } +} From 0b156e7d445d4040abe8bab7d296452883b43aed Mon Sep 17 00:00:00 2001 From: Lae <30905719+mlaetitia@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:11:24 +0100 Subject: [PATCH 2/3] Rewrite Rules: Simplify random content redirect after review. Check for the random parameter before evaluating the enable filter so disabled sites pay no filter lookup per request. Validate post type viewability unconditionally, prime the post cache for get_permalink(), skip the second query when the random offset is zero, and drop test teardown that the test framework already guarantees. See https://core.trac.wordpress.org/ticket/64498 Co-Authored-By: Claude Fable 5 --- src/wp-includes/query.php | 52 ++++++++++--------- .../tests/rewrite/randomContentRedirect.php | 23 +------- 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index 6ecc9b5103102..a2210b61b1069 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -1251,6 +1251,11 @@ function _find_post_by_old_date( $post_type ) { * @since 7.2.0 */ function wp_random_content_redirect() { + // The `random` parameter is a flag, present with or without a value. + if ( ! isset( $_GET['random'] ) ) { + return; + } + /** * Filters whether random content redirects are enabled. * @@ -1262,12 +1267,7 @@ function wp_random_content_redirect() { return; } - // The `random` parameter is a flag, present with or without a value. - if ( ! isset( $_GET['random'] ) ) { - return; - } - - if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ), true ) ) { + if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'HEAD' ), true ) ) { return; } @@ -1275,21 +1275,24 @@ function wp_random_content_redirect() { if ( isset( $_GET['random_post_type'] ) ) { $post_type = sanitize_key( wp_unslash( $_GET['random_post_type'] ) ); + } - if ( ! post_type_exists( $post_type ) || ! is_post_type_viewable( $post_type ) ) { - return; - } + $post_type_object = get_post_type_object( $post_type ); + + if ( ! $post_type_object || ! is_post_type_viewable( $post_type_object ) ) { + return; } $args = array( - 'post_type' => $post_type, - 'post_status' => 'publish', - 'has_password' => false, - 'fields' => 'ids', - 'posts_per_page' => 1, - 'orderby' => 'ID', - 'order' => 'ASC', - 'ignore_sticky_posts' => true, + 'post_type' => $post_type, + 'post_status' => 'publish', + 'has_password' => false, + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'ASC', + 'ignore_sticky_posts' => true, + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, ); if ( isset( $_GET['random_cat_id'] ) ) { @@ -1307,15 +1310,16 @@ function wp_random_content_redirect() { $query = new WP_Query( $args ); $post_count = (int) $query->found_posts; - if ( $post_count < 1 ) { - return; - } - if ( $post_count > 1 ) { - $args['offset'] = wp_rand( 0, $post_count - 1 ); - $args['no_found_rows'] = true; + $offset = wp_rand( 0, $post_count - 1 ); - $query = new WP_Query( $args ); + if ( $offset > 0 ) { + $args['offset'] = $offset; + $args['no_found_rows'] = true; + $args['cache_results'] = false; + + $query = new WP_Query( $args ); + } } if ( empty( $query->posts ) ) { diff --git a/tests/phpunit/tests/rewrite/randomContentRedirect.php b/tests/phpunit/tests/rewrite/randomContentRedirect.php index 12c60563fc29d..2fe94dd1bc3b2 100644 --- a/tests/phpunit/tests/rewrite/randomContentRedirect.php +++ b/tests/phpunit/tests/rewrite/randomContentRedirect.php @@ -8,32 +8,13 @@ class Tests_Rewrite_RandomContentRedirect extends WP_UnitTestCase { protected $random_content_redirect_url; - protected $original_request_method; - public function set_up() { parent::set_up(); add_filter( 'enable_random_content_redirect', '__return_true' ); - add_filter( 'random_content_redirect_url', array( $this, 'filter_random_content_redirect_url' ), 10, 1 ); - - $this->original_request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : null; - - $_GET['random'] = ''; - $_SERVER['REQUEST_METHOD'] = 'GET'; - } - - public function tear_down() { - $this->random_content_redirect_url = null; - - unset( $_GET['random'], $_GET['random_post_type'], $_GET['random_cat_id'] ); - - if ( null === $this->original_request_method ) { - unset( $_SERVER['REQUEST_METHOD'] ); - } else { - $_SERVER['REQUEST_METHOD'] = $this->original_request_method; - } + add_filter( 'random_content_redirect_url', array( $this, 'filter_random_content_redirect_url' ) ); - parent::tear_down(); + $_GET['random'] = ''; } public function test_hook_is_registered() { From 7540f96de1d618a478d3605575d01d7dbc9ca984 Mon Sep 17 00:00:00 2001 From: Lae <30905719+mlaetitia@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:27:09 +0100 Subject: [PATCH 3/3] Rewrite Rules: Address review feedback on the random content redirect --- src/wp-includes/query.php | 9 ++- .../tests/rewrite/randomContentRedirect.php | 67 ++++++++++++++++--- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index a2210b61b1069..cdd2ccef6e802 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -1267,7 +1267,9 @@ function wp_random_content_redirect() { return; } - if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'HEAD' ), true ) ) { + $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : ''; + + if ( ! in_array( $request_method, array( 'GET', 'HEAD' ), true ) ) { return; } @@ -1296,7 +1298,7 @@ function wp_random_content_redirect() { ); if ( isset( $_GET['random_cat_id'] ) ) { - $cat_id = absint( wp_unslash( $_GET['random_cat_id'] ) ); + $cat_id = (int) wp_unslash( $_GET['random_cat_id'] ); if ( $cat_id < 1 ) { return; @@ -1343,7 +1345,8 @@ function wp_random_content_redirect() { return; } - // Temporary redirect, so clients do not cache the randomly picked target. + // Temporary redirect, so clients and intermediary caches do not cache the randomly picked target. + nocache_headers(); wp_safe_redirect( $link, 302 ); exit; } diff --git a/tests/phpunit/tests/rewrite/randomContentRedirect.php b/tests/phpunit/tests/rewrite/randomContentRedirect.php index 2fe94dd1bc3b2..4653f3018304d 100644 --- a/tests/phpunit/tests/rewrite/randomContentRedirect.php +++ b/tests/phpunit/tests/rewrite/randomContentRedirect.php @@ -11,19 +11,21 @@ class Tests_Rewrite_RandomContentRedirect extends WP_UnitTestCase { public function set_up() { parent::set_up(); - add_filter( 'enable_random_content_redirect', '__return_true' ); add_filter( 'random_content_redirect_url', array( $this, 'filter_random_content_redirect_url' ) ); $_GET['random'] = ''; } + public function filter_random_content_redirect_url( $url ) { + $this->random_content_redirect_url = $url; + return false; + } + public function test_hook_is_registered() { $this->assertSame( 10, has_action( 'template_redirect', 'wp_random_content_redirect' ) ); } public function test_disabled_by_default() { - remove_filter( 'enable_random_content_redirect', '__return_true' ); - self::factory()->post->create(); wp_random_content_redirect(); @@ -31,6 +33,8 @@ public function test_disabled_by_default() { } public function test_no_random_parameter_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + unset( $_GET['random'] ); self::factory()->post->create(); @@ -40,6 +44,8 @@ public function test_no_random_parameter_does_not_redirect() { } public function test_redirects_to_a_published_post() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $post_ids = self::factory()->post->create_many( 3 ); wp_random_content_redirect(); @@ -47,6 +53,8 @@ public function test_redirects_to_a_published_post() { } public function test_post_request_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $_SERVER['REQUEST_METHOD'] = 'POST'; self::factory()->post->create(); @@ -55,7 +63,20 @@ public function test_post_request_does_not_redirect() { $this->assertNull( $this->random_content_redirect_url ); } + public function test_unset_request_method_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + + unset( $_SERVER['REQUEST_METHOD'] ); + + self::factory()->post->create(); + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + public function test_head_request_redirects() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $_SERVER['REQUEST_METHOD'] = 'HEAD'; $post_id = self::factory()->post->create(); @@ -65,6 +86,8 @@ public function test_head_request_redirects() { } public function test_excludes_password_protected_posts() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $public_id = self::factory()->post->create(); self::factory()->post->create_many( 2, array( 'post_password' => 'secret' ) ); @@ -73,6 +96,8 @@ public function test_excludes_password_protected_posts() { } public function test_excludes_unpublished_posts() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $public_id = self::factory()->post->create(); self::factory()->post->create( array( 'post_status' => 'draft' ) ); self::factory()->post->create( array( 'post_status' => 'private' ) ); @@ -82,6 +107,8 @@ public function test_excludes_unpublished_posts() { } public function test_random_post_type_parameter() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + self::factory()->post->create(); $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); @@ -92,6 +119,8 @@ public function test_random_post_type_parameter() { } public function test_invalid_post_type_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + self::factory()->post->create(); $_GET['random_post_type'] = 'nonexistent_type'; @@ -101,6 +130,8 @@ public function test_invalid_post_type_does_not_redirect() { } public function test_non_viewable_post_type_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + register_post_type( 'wptests_hidden', array( 'public' => false ) ); self::factory()->post->create(); @@ -113,6 +144,8 @@ public function test_non_viewable_post_type_does_not_redirect() { } public function test_random_cat_id_parameter() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $cat_id = self::factory()->category->create(); $in_cat_id = self::factory()->post->create( array( 'post_category' => array( $cat_id ) ) ); self::factory()->post->create_many( 3 ); @@ -124,6 +157,8 @@ public function test_random_cat_id_parameter() { } public function test_nonexistent_category_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + self::factory()->post->create(); $_GET['random_cat_id'] = '99999'; @@ -138,6 +173,8 @@ public function test_nonexistent_category_does_not_redirect() { * @param string $cat_id Invalid category ID value. */ public function test_invalid_category_id_does_not_redirect( $cat_id ) { + add_filter( 'enable_random_content_redirect', '__return_true' ); + self::factory()->post->create(); $_GET['random_cat_id'] = $cat_id; @@ -149,12 +186,25 @@ public function test_invalid_category_id_does_not_redirect( $cat_id ) { public function data_invalid_category_ids() { return array( 'zero' => array( '0' ), - 'negative' => array( '-5' ), 'non-numeric' => array( 'foo' ), ); } + public function test_negated_category_id_of_existing_category_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + + $cat_id = self::factory()->category->create(); + self::factory()->post->create( array( 'post_category' => array( $cat_id ) ) ); + + $_GET['random_cat_id'] = (string) ( -$cat_id ); + + wp_random_content_redirect(); + $this->assertNull( $this->random_content_redirect_url ); + } + public function test_redirect_url_is_filterable() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + self::factory()->post->create(); add_filter( @@ -170,19 +220,18 @@ static function () { } public function test_no_posts_does_not_redirect() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + wp_random_content_redirect(); $this->assertNull( $this->random_content_redirect_url ); } public function test_single_post_redirects_to_it() { + add_filter( 'enable_random_content_redirect', '__return_true' ); + $post_id = self::factory()->post->create(); wp_random_content_redirect(); $this->assertSame( get_permalink( $post_id ), $this->random_content_redirect_url ); } - - public function filter_random_content_redirect_url( $url ) { - $this->random_content_redirect_url = $url; - return false; - } }