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
13 changes: 10 additions & 3 deletions src/wp-includes/class-wp-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,10 @@ class WP_Query {
/**
* Signifies whether the current query is for random content.
*
* True for a blog home, archive or search request that includes the `random`
* query variable, for example `example.com/random/` or `example.com/?s=foo&random`.
* True for a blog home or archive request that includes the `random` query
* variable, for example `example.com/random/` or `example.com/random/category/news/`.
* Search, feed, trackback, robots.txt, favicon and sitemap requests are never for
* random content.
*
* @since 7.2.0
*/
Expand Down Expand Up @@ -1167,7 +1169,12 @@ public function parse_query( $query = '' ) {

$this->is_random = isset( $query_vars['random'] )
&& ! $this->is_feed
&& ( $this->is_home || $this->is_archive || $this->is_search );
&& ! $this->is_search
&& ! $this->is_robots
&& ! $this->is_favicon
&& ! $this->is_sitemap
&& ! $this->is_trackback
&& ( $this->is_home || $this->is_archive );

$this->query_vars_hash = md5( serialize( $this->query_vars ) );
$this->query_vars_changed = false;
Expand Down
5 changes: 3 additions & 2 deletions src/wp-includes/class-wp-rewrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -1532,8 +1532,9 @@ public function rewrite_rules() {
* the URL, see {@see WP_Rewrite::post_permalinks_share_random_base()}. The rules are flushed
* when such a page or post changes, see {@see wp_random_content_flush_rewrite_rules_for_post()}.
*
* The `random` query variable can be added to any other archive or search URL, for example
* `/2026/?random`, to request a random post from that archive.
* The `random` query variable can be added to any other archive URL, for example
* `/2026/?random`, to request a random post from that archive. Search requests
* including it are redirected to the search results, see {@see WP::handle_random()}.
*
* @since 7.2.0
*
Expand Down
158 changes: 158 additions & 0 deletions src/wp-includes/class-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,14 @@ public function query_posts() {
* redirected, the request continues with the results of the main query. The main
* query is not run again.
*
* Search requests are never random content requests. A search request that includes
* the `random` query variable is redirected to the search results without it, see
* {@see WP::redirect_random_search()}.
*
* Requests that are not for posts ignore the `random` query variable, they are neither
* random content requests nor redirected. These are requests for robots.txt, the
* favicon, sitemaps, feeds and trackbacks.
*
* @since 7.2.0
*
* @see wp_is_random_content_query()
Expand All @@ -738,6 +746,26 @@ public function handle_random(): bool {
return false;
}

/*
* Random content can only be requested from posts. Requests for robots.txt, the
* favicon, sitemaps, feeds and trackbacks ignore the `random` query variable.
*
* Embeds are not included as only singular requests are embeds, which are never
* random content requests.
*/
foreach ( array( 'robots', 'favicon', 'sitemap', 'feed', 'tb' ) as $query_var ) {
if ( ! empty( $this->query_vars[ $query_var ] ) ) {
return false;
}
}

if ( isset( $this->query_vars['s'] ) ) {
$this->redirect_random_search();

// The request was not redirected, run the search as if `random` was not requested.
return false;
}

add_action( 'pre_get_posts', 'wp_random_content_pre_get_posts', 20 );
add_filter( 'posts_orderby', 'wp_random_content_posts_orderby', 20, 2 );
add_filter( 'post_limits', 'wp_random_content_post_limits', 20, 2 );
Expand All @@ -759,6 +787,136 @@ public function handle_random(): bool {
return true;
}

/**
* Redirects search requests for random content to the search results.
*
* Random content can not be requested from search results. Instead, a search
* request that includes the `random` query variable is redirected to
* the same request without it.
*
* The location depends on how random content was requested:
*
* - By the `random` query string parameter: the requested path is retained and the
* parameter is removed. For example, `example.com/search/foo/?random` redirects to
* `example.com/search/foo/` and `example.com/?s=foo&random` to `example.com/?s=foo`.
* - By a rewrite rule, such as those using the random base: the path is replaced by the
* query variables of the request, so the location never includes the random base.
* For example, `example.com/random/category/news/?s=foo` redirects to
* `example.com/?s=foo&category_name=news`.
*
* Other query string parameters, including those that are not query variables, are
* retained. The redirect happens before the main query is run, so no search is
* performed for the redirected request.
*
* As with random content requests, only front end GET and HEAD requests are redirected.
* The request is not redirected when the location would be the requested URL. This is
* the case when random content is not requested by the URL, for example by a filter,
* or is requested by a rewrite rule for the home page.
*
* @since 7.2.0
*
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
*/
protected function redirect_random_search(): void {
global $wp_rewrite;

if ( is_admin() ) {
return;
}

$request_method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : '';
if ( 'GET' !== $request_method && 'HEAD' !== $request_method ) {
return;
}

$requested_path = '';

if ( is_string( $this->request ) && '' !== $this->request ) {
/*
* The requested path is URL-encoded unless it is from PATH_INFO. Encode any
* characters that are not safe in a redirect without decoding the path, as
* doing so would change its meaning. For example, a plus is a space in the
* path of a search.
*/
$requested_path = (string) preg_replace_callback(
'|[^a-z0-9\-._~+%/]|i',
static fn ( array $matches ): string => rawurlencode( $matches[0] ),
$this->request
);
$requested_path = user_trailingslashit( $requested_path );

// PATHINFO permalinks are prefixed by the index file, which PATH_INFO does not include.
if ( '' !== $wp_rewrite->root && ! str_starts_with( $requested_path, $wp_rewrite->root ) ) {
$requested_path = $wp_rewrite->root . $requested_path;
}
}

$path = $requested_path;
$query_args = $_GET;

wp_parse_str( $this->matched_query, $matched_query_vars );

if ( isset( $matched_query_vars['random'] ) ) {
/*
* The path requests random content, so can not be retained. Replace it with
* the query variables it was rewritten to.
*/
$path = '';
$query_args = array_merge( $query_args, $this->query_vars );
}

unset( $query_args['random'] );

$location = add_query_arg( $this->urlencode_query_args( $query_args ), home_url( '/' . $path ) );

/*
* The redirect removes the random request from the URL. If there is nothing to
* remove the location is the requested URL, which would redirect repeatedly. This
* is the case when random content is not requested by the URL, for example by a
* filter, or is requested by a rewrite rule for the home page.
*/
if ( add_query_arg( $this->urlencode_query_args( $_GET ), home_url( '/' . $requested_path ) ) === $location ) {
return;
}

// Only consider redirecting permitted locations so wp_safe_redirect doesn't redirect to the fallback.
if ( ! wp_validate_redirect( $location, '' ) ) {
return;
}

if ( wp_safe_redirect( $location, 301 ) ) {
exit;
}
}

/**
* URL-encodes the names and values of query string parameters.
*
* Unlike urlencode_deep(), the parameter names are encoded as well as the values. A
* name is from the request, so may include characters that separate parameters.
*
* @since 7.2.0
*
* @param array<mixed> $query_args Query string parameters, expected to be slashed.
* @return array<string, mixed> URL-encoded query string parameters. Values that are
* neither scalar nor an array are removed.
*/
private function urlencode_query_args( array $query_args ): array {
$encoded = array();

foreach ( $query_args as $name => $value ) {
$name = urlencode( wp_unslash( (string) $name ) );

if ( is_array( $value ) ) {
$encoded[ $name ] = $this->urlencode_query_args( $value );
} elseif ( is_scalar( $value ) ) {
$encoded[ $name ] = urlencode( wp_unslash( (string) $value ) );
}
}

return $encoded;
}

/**
* Set the Headers for 404, if nothing is found for requested URL.
*
Expand Down
23 changes: 14 additions & 9 deletions src/wp-includes/query.php
Original file line number Diff line number Diff line change
Expand Up @@ -703,8 +703,9 @@ function is_sitemap(): bool {
/**
* Determines whether the query is for random content.
*
* True for a blog home, archive or search request that includes the `random` query
* variable, for example `example.com/random/` or `example.com/random/category/news/`.
* True for a blog home or archive request that includes the `random` query variable,
* for example `example.com/random/` or `example.com/random/category/news/`. Search,
* feed, trackback, robots.txt, favicon and sitemap requests are never for random content.
*
* @since 7.2.0
*
Expand Down Expand Up @@ -1287,13 +1288,17 @@ function wp_is_random_content_redirect_enabled(): bool {
/**
* Determines whether a query is a request for random content.
*
* A random content request is a front end, main query for the blog home, an archive,
* or search results that includes the `random` query variable and at least one
* `randomable` post type. For example
* `example.com/random/`, `example.com/random/category/news/` or `example.com/?s=foo&random`.
*
* Singular (including embeds) and feed requests are never random content requests,
* nor are requests using an HTTP method other than GET or HEAD.
* A random content request is a front end, main query for the blog home or an archive
* that includes the `random` query variable and at least one `randomable` post type.
* For example `example.com/random/` or `example.com/random/category/news/`.
*
* Singular (including embeds), search, feed, trackback, robots.txt, favicon and sitemap
* requests are never random content requests, nor are requests using an HTTP method
* other than GET or HEAD. Search
* requests are excluded as selecting a random post from search results would run an
* uncacheable search query that orders every matching post randomly. Search requests
* that include the `random` query variable are redirected to the search results
* by {@see WP::handle_random()}.
*
* @since 7.2.0
*
Expand Down
Loading
Loading