Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );

Expand Down
116 changes: 116 additions & 0 deletions src/wp-includes/query.php
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,122 @@ 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() {
// The `random` parameter is a flag, present with or without a value.
if ( ! isset( $_GET['random'] ) ) {
return;
}

/**
* 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;
}

$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : '';

if ( ! in_array( $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'] ) );
}

$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,
'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'] ) ) {
$cat_id = (int) wp_unslash( $_GET['random_cat_id'] );

if ( $cat_id < 1 ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absint always returns positive? Should it be (int) instead maybe?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! I changed this and also on the same note changed the test that checked the negative value as it was generally failing (as it should) but mostly for the wrong reasons.
changed both on the last commit

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 ) {
$offset = wp_rand( 0, $post_count - 1 );

if ( $offset > 0 ) {
$args['offset'] = $offset;
$args['no_found_rows'] = true;
$args['cache_results'] = false;

$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 and intermediary caches do not cache the randomly picked target.
nocache_headers();
wp_safe_redirect( $link, 302 );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also call nocache_headers?

@mcsf mcsf Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the intent here is to not cache the redirection (hence 302), while allowing clients to cache the resolved page (i.e. the randomly selected post). No?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it cache based on the URL? I'm not a page cache expert, hence my comment being formatted as a question 🙂

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks both for the comments. I ended up adding a nocache_headers as we actually want both as much as possible:

  • the 302 makes the redirect non-permanent
  • stop intermediary caches (CDNs, full-page caches) from pinning the redirect, which would make ?random return the same post for everyone

Thanks! Hadn't considered this properly

exit;
}

/**
* Set up global post data.
*
Expand Down
237 changes: 237 additions & 0 deletions tests/phpunit/tests/rewrite/randomContentRedirect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<?php

/**
* @group rewrite
* @ticket 64498
* @covers wp_random_content_redirect
*/
class Tests_Rewrite_RandomContentRedirect extends WP_UnitTestCase {
protected $random_content_redirect_url;

public function set_up() {
parent::set_up();

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() {
self::factory()->post->create();

wp_random_content_redirect();
$this->assertNull( $this->random_content_redirect_url );
}
Comment thread
mlaetitia marked this conversation as resolved.

public function test_no_random_parameter_does_not_redirect() {
add_filter( 'enable_random_content_redirect', '__return_true' );

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() {
add_filter( 'enable_random_content_redirect', '__return_true' );

$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() {
add_filter( 'enable_random_content_redirect', '__return_true' );

$_SERVER['REQUEST_METHOD'] = 'POST';

self::factory()->post->create();

wp_random_content_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';
Comment thread
ellatrix marked this conversation as resolved.

$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() {
add_filter( 'enable_random_content_redirect', '__return_true' );

$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() {
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' ) );

wp_random_content_redirect();
$this->assertSame( get_permalink( $public_id ), $this->random_content_redirect_url );
}

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' ) );

$_GET['random_post_type'] = 'page';
Comment thread
ellatrix marked this conversation as resolved.

wp_random_content_redirect();
$this->assertSame( get_permalink( $page_id ), $this->random_content_redirect_url );
}

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';

wp_random_content_redirect();
$this->assertNull( $this->random_content_redirect_url );
}

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();
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() {
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 );

$_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() {
add_filter( 'enable_random_content_redirect', '__return_true' );

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 ) {
add_filter( 'enable_random_content_redirect', '__return_true' );

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' ),
'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(
'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() {
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 );
}
}
Loading