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
6 changes: 3 additions & 3 deletions src/wp-includes/class-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -720,9 +720,9 @@ public function query_posts() {
* - {@see wp_random_content_post_limits()} on the {@see 'post_limits'} filter.
*
* 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 but the request is not
* redirected, the request continues with the results of the main query. The main
* query is not run again.
* 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
* query, which is not run again.
*
* @since 7.2.0
*
Expand Down
27 changes: 12 additions & 15 deletions src/wp-includes/query.php
Original file line number Diff line number Diff line change
Expand Up @@ -1465,13 +1465,16 @@ 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. As the redirect
* is sent before {@see WP::send_headers()}, the headers preventing the redirect being
* cached or indexed are sent with it.
* 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.
*
* 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
* another site.
*
* If no post is selected, for example on an empty archive, {@see WP::handle_random()}
* handles the request as a 404. If the post's permalink is on another site, the
* request is handled as a regular archive request.
* handles the request as a 404.
*
* @since 7.2.0
*
Expand All @@ -1484,22 +1487,16 @@ function wp_random_content_redirect(): void {
return;
}

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

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

nocache_headers();
Comment thread
aaronjorbin marked this conversation as resolved.
Resolved

if ( ! headers_sent() ) {
header( 'X-Robots-Tag: noindex, follow' );
}

if ( wp_safe_redirect( $location, 302 ) ) {
exit;
}
$location = get_permalink( $wp_query->post );

wp_redirect( $location, 302 );
exit;
}

/**
Expand Down
80 changes: 37 additions & 43 deletions tests/phpunit/tests/query/randomContentRedirect.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ class Tests_Query_RandomContentRedirect extends WP_UnitTestCase {
*/
protected static $admin;

/**
* Message of the exception thrown in place of the `exit` following a redirect.
*
* @var string
*/
const REDIRECT_EXCEPTION_MESSAGE = 'Random content redirect.';

/**
* Redirect location captured from the `wp_redirect` filter.
*
Expand Down Expand Up @@ -152,7 +159,7 @@ public function set_up(): void {
$this->queries = array();
$this->recording_queries = false;

// Capture redirects and prevent `exit` from ending the test run.
// Capture redirects and end the request before `exit` ends the test run.
add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 10, 2 );
}

Expand All @@ -169,18 +176,35 @@ public function tear_down(): void {
}

/**
* Captures the redirect and prevents it from being sent.
* Captures the redirect and ends the request in place of the `exit` that follows it.
*
* @throws WP_Tests_Exception Always, caught by {@see self::go_to()}.
*
* @param string $location Redirect location.
* @param int $status Redirect status.
* @return false
* @return never
*/
public function filter_wp_redirect( string $location, int $status ) {
$this->redirect_location = $location;
$this->redirect_status = $status;
$this->recording_queries = false;

return false;
throw new WP_Tests_Exception( self::REDIRECT_EXCEPTION_MESSAGE );
}

/**
* Requests a URL, stopping where a random content redirect ends the request.
*
* @param string $url The URL for the request.
*/
public function go_to( $url ) {
try {
parent::go_to( $url );
} catch ( WP_Tests_Exception $e ) {
if ( self::REDIRECT_EXCEPTION_MESSAGE !== $e->getMessage() ) {
throw $e;
}
}
}

/**
Expand Down Expand Up @@ -1877,17 +1901,6 @@ 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' );

$this->go_to( home_url( '/random/' ) );

$this->assertNotRedirected( 'An empty permalink should not redirect.' );
}

/**
* Ensure the redirect happens before the `wp` action fires.
*/
Expand Down Expand Up @@ -2075,9 +2088,9 @@ static function ( WP_Query $query ): void {
*/

/**
* Ensure the request does not redirect to a permalink on another site.
* Ensure the request redirects to the permalink when it is on another site.
*/
public function test_no_redirect_when_the_permalink_is_on_another_site(): void {
public function test_redirect_to_a_permalink_on_another_site(): void {
add_filter(
'post_link',
static function (): string {
Expand All @@ -2087,30 +2100,7 @@ static function (): string {

$this->go_to( home_url( '/random/' ) );

$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',
static function ( array $hosts ): array {
$hosts[] = 'cdn.example.org';
return $hosts;
}
);
add_filter(
'post_link',
static function (): string {
return 'https://cdn.example.org/post/';
}
);

$this->go_to( home_url( '/random/' ) );

$this->assertSame( 'https://cdn.example.org/post/', $this->redirect_location, 'Permalinks on an allowed host should be redirected to.' );
$this->assertSame( 'https://elsewhere.example/post/', $this->redirect_location, 'Permalinks on another site should be redirected to.' );
}

/**
Expand Down Expand Up @@ -2502,10 +2492,14 @@ public function test_handle_random_runs_the_main_query_for_random_requests(): vo
$wp = new WP();
$wp->query_vars = array( 'random' => '' );

$this->assertTrue( $wp->handle_random(), 'Random content requests should be handled.' );
try {
$wp->handle_random();
} catch ( WP_Tests_Exception $e ) {
$this->assertSame( self::REDIRECT_EXCEPTION_MESSAGE, $e->getMessage(), 'An unexpected exception was thrown.' );
}

$this->assertTrue( $this->get_main_query()->is_random(), 'The main query should be the random content query.' );
$this->assertRedirectedToOneOf( array( self::$arts_post, self::$sport_post ), 'The random content request should redirect after running the main query.' );
$this->assertFalse( is_404(), 'A random content request with a post should not 404 when the redirect does not exit.' );
}

/**
Expand Down
96 changes: 96 additions & 0 deletions tests/phpunit/tests/query/randomContentRedirectHeaders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

/**
* Tests for the HTTP headers sent on random content requests.
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*
* @group query
* @group rewrite
* @group xdebug
*
* @covers ::wp_random_content_redirect
*/
class Tests_Query_RandomContentRedirectHeaders extends WP_UnitTestCase {

/**
* Message of the exception thrown in place of the `exit` following a redirect.
*
* @var string
*/
const REDIRECT_EXCEPTION_MESSAGE = 'Random content redirect.';

/**
* Sets up pretty permalinks and stops redirects from ending the test run.
*/
public function set_up(): void {
parent::set_up();

$this->set_permalink_structure( '/%postname%/' );
self::factory()->post->create( array( 'post_status' => 'publish' ) );
add_filter(
'wp_redirect',
static function (): void {
throw new WP_Tests_Exception( self::REDIRECT_EXCEPTION_MESSAGE );
}
);
}

/**
* Requests a URL, stopping where a random content redirect ends the request.
*
* @param string $url The URL for the request.
*/
public function go_to( $url ) {
try {
parent::go_to( $url );
} catch ( WP_Tests_Exception $e ) {
if ( self::REDIRECT_EXCEPTION_MESSAGE !== $e->getMessage() ) {
throw $e;
}
}
}

/**
* Ensure random content redirects are neither cached nor indexed, whatever the permalink.
*
* @ticket 66625
*
* @requires function xdebug_get_headers
*
* @dataProvider data_permalinks
*
* @param string|null $permalink Permalink of the selected post, or null to keep the real one.
*/
public function test_random_content_response_is_not_cached_or_indexed( ?string $permalink ): void {
if ( null !== $permalink ) {
add_filter(
'post_link',
static function () use ( $permalink ): string {
return $permalink;
}
);
}

$this->go_to( home_url( '/random/' ) );

$headers = xdebug_get_headers();

$this->assertContains( 'Cache-Control: ' . wp_get_nocache_headers()['Cache-Control'], $headers, 'The random content response should not be cached.' );
$this->assertContains( 'X-Robots-Tag: noindex, follow', $headers, 'The random content response should not be indexed.' );
}

/**
* Data provider.
*
* @return array<string, array{0: string|null}>
*/
public static function data_permalinks(): array {
return array(
'permalink on this site' => array( null ),
'permalink on another site' => array( 'https://elsewhere.example/post/' ),
'empty permalink' => array( '' ),
);
}
}
31 changes: 29 additions & 2 deletions tests/phpunit/tests/rewrite/randomRewriteRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,38 @@
*/
class Tests_Rewrite_RandomRewriteRules extends WP_UnitTestCase {

/**
* Message of the exception thrown in place of the `exit` following a redirect.
*
* @var string
*/
const REDIRECT_EXCEPTION_MESSAGE = 'Random content redirect.';

/**
* Requests a URL, stopping where a random content redirect ends the request.
*
* @param string $url The URL for the request.
*/
public function go_to( $url ) {
try {
parent::go_to( $url );
} catch ( WP_Tests_Exception $e ) {
if ( self::REDIRECT_EXCEPTION_MESSAGE !== $e->getMessage() ) {
throw $e;
}
}
}

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

// Prevent random content redirects from exiting, only the rewrite rules are under test.
add_filter( 'wp_redirect', '__return_false' );
// End the request before random content redirects exit, only the rewrite rules are under test.
add_filter(
'wp_redirect',
static function (): void {
throw new WP_Tests_Exception( self::REDIRECT_EXCEPTION_MESSAGE );
}
);

$this->set_permalink_structure( '/%postname%/' );
// Register the taxonomy rewrite rules for the permalink structure.
Expand Down
Loading
Loading