Commit 6aaf57b341 for wordpress.org

commit 6aaf57b3419614b5a7a6d11183595d657ad3fca3
Author: jorbin <jorbin@git.wordpress.org>
Date:   Sat Nov 14 03:58:01 2026 +0000

    Rewrite Rules: Send the random content redirect on template_redirect.

    The random content redirect was sent from `WP::handle_random()` before the `template_redirect` action fired. Plugins that restrict access to a site on that action never saw the request, so logged-out visitors to a locked site were still redirected to post URLs. The redirect is now sent on `template_redirect` at priority 100, and `redirect_canonical()` leaves these requests alone so they are not redirected twice.

    Developed in: https://github.com/WordPress/wordpress-develop/pull/15912

    Follow-up to r64402, r65440.

    Props jorbin, renehermi, aryanjasala, rcwpdev.
    Fixes #66661. See #64498.

    Built from https://develop.svn.wordpress.org/trunk@65526


    git-svn-id: http://core.svn.wordpress.org/trunk@64599 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/canonical.php b/wp-includes/canonical.php
index 2fb3d74bb9..5ec43f99c4 100644
--- a/wp-includes/canonical.php
+++ b/wp-includes/canonical.php
@@ -28,11 +28,12 @@
  *
  * @since 2.3.0
  *
- * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
+ * @global WP_Rewrite $wp_rewrite   WordPress rewrite component.
  * @global bool       $is_IIS
- * @global WP_Query   $wp_query   WordPress Query object.
- * @global wpdb       $wpdb       WordPress database abstraction object.
- * @global WP         $wp         Current WordPress environment instance.
+ * @global WP_Query   $wp_query     WordPress Query object.
+ * @global WP_Query   $wp_the_query WordPress main Query object.
+ * @global wpdb       $wpdb         WordPress database abstraction object.
+ * @global WP         $wp           Current WordPress environment instance.
  *
  * @param string $requested_url Optional. The URL that was requested, used to
  *                              figure if redirect is needed.
@@ -41,12 +42,24 @@
  * @phpstan-return ( $do_redirect is true ? null : string|null )
  */
 function redirect_canonical( $requested_url = null, $do_redirect = true ) {
-	global $wp_rewrite, $is_IIS, $wp_query, $wpdb, $wp;
+	global $wp_rewrite, $is_IIS, $wp_query, $wp_the_query, $wpdb, $wp;

 	if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'HEAD' ), true ) ) {
 		return null;
 	}

+	/*
+	 * Random content requests with a selected post are redirected to that post by
+	 * wp_random_content_redirect() later on the `template_redirect` action. A canonical
+	 * redirect would redirect the request twice and may drop the `random` query variable.
+	 */
+	if ( $wp_the_query instanceof WP_Query
+		&& wp_is_random_content_query( $wp_the_query )
+		&& $wp_the_query->post instanceof WP_Post
+	) {
+		return null;
+	}
+
 	/*
 	 * If we're not in wp-admin and the post has been published and preview nonce
 	 * is non-existent or invalid then no need for preview in query.
diff --git a/wp-includes/class-wp.php b/wp-includes/class-wp.php
index d7c2add374..b9b86116d3 100644
--- a/wp-includes/class-wp.php
+++ b/wp-includes/class-wp.php
@@ -719,6 +719,10 @@ class WP {
 	 * - {@see wp_random_content_posts_orderby()} on the {@see 'posts_orderby'} filter.
 	 * - {@see wp_random_content_post_limits()} on the {@see 'post_limits'} filter.
 	 *
+	 * If a post is selected, the redirect is sent by {@see wp_random_content_redirect()}
+	 * on the {@see 'template_redirect'} action at priority 100, so the request is set up
+	 * and the {@see 'wp'} action has fired before the redirect.
+	 *
 	 * If no post is selected, for example on an empty archive, the request is a 404 as
 	 * there is no post to redirect to. If a post is selected, the request ends with
 	 * the redirect. Otherwise the request continues with the results of the main
@@ -748,9 +752,13 @@ class WP {
 		remove_filter( 'posts_orderby', 'wp_random_content_posts_orderby', 20 );
 		remove_filter( 'post_limits', 'wp_random_content_post_limits', 20 );

-		wp_random_content_redirect();
+		if ( ! wp_is_random_content_query( $wp_the_query ) ) {
+			return true;
+		}

-		if ( wp_is_random_content_query( $wp_the_query ) && ! $wp_the_query->post instanceof WP_Post ) {
+		if ( $wp_the_query->post instanceof WP_Post ) {
+			add_action( 'template_redirect', 'wp_random_content_redirect', 100 );
+		} else {
 			$wp_the_query->set_404();
 			status_header( 404 );
 			nocache_headers();
diff --git a/wp-includes/query.php b/wp-includes/query.php
index 7a21ba50df..a4ceff66f7 100644
--- a/wp-includes/query.php
+++ b/wp-includes/query.php
@@ -1464,10 +1464,14 @@ function wp_random_content_post_limits( $limits, WP_Query $query ): string {
 /**
  * Redirects random content requests to the randomly selected post.
  *
- * Called by {@see WP::handle_random()} immediately after the main query selects the
- * random post, to avoid unnecessary processing before the redirect. The headers
- * preventing the response being cached or indexed are sent as soon as a post is
- * selected, before {@see WP::send_headers()}, so they are sent with the redirect.
+ * Added to the {@see 'template_redirect'} action at priority 100 by
+ * {@see WP::handle_random()} once the main query has selected the random post, so the
+ * redirect is sent after the request is set up and the {@see 'wp'} action has fired.
+ * The headers preventing the redirect being cached or indexed are sent with it.
+ *
+ * The post is read from the main query rather than the current query, so the redirect
+ * is still sent when an earlier callback replaces the current query, for example by
+ * calling {@see query_posts()}.
  *
  * The request ends once a post is selected. The redirect is to the post's permalink
  * as returned by {@see get_permalink()}, including a permalink filtered to be on
@@ -1478,12 +1482,16 @@ function wp_random_content_post_limits( $limits, WP_Query $query ): string {
  *
  * @since 7.2.0
  *
- * @global WP_Query $wp_query WordPress Query object.
+ * @global WP_Query $wp_the_query WordPress main Query object.
  */
 function wp_random_content_redirect(): void {
-	global $wp_query;
+	global $wp_the_query;

-	if ( ! wp_is_random_content_query( $wp_query ) || ! $wp_query->post instanceof WP_Post ) {
+	if (
+		! $wp_the_query instanceof WP_Query
+		|| ! wp_is_random_content_query( $wp_the_query )
+		|| ! $wp_the_query->post instanceof WP_Post
+	) {
 		return;
 	}

@@ -1493,7 +1501,7 @@ function wp_random_content_redirect(): void {
 		header( 'X-Robots-Tag: noindex, follow' );
 	}

-	$location = get_permalink( $wp_query->post );
+	$location = get_permalink( $wp_the_query->post );

 	wp_redirect( $location, 302 );
 	exit;
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 38d2c0162a..aaf3efbc3b 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-beta5-65525';
+$wp_version = '7.2-beta5-65526';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.