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
28 changes: 28 additions & 0 deletions src/wp-includes/canonical.php
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,34 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) {
return null;
}

/*
* Don't redirect if the URLs only differ in how the query string is written,
* for example '+' vs '%20', 'a=' vs 'a', or a stray '&'. Both URLs produce the
* same query variables, so a page cache keyed on those could serve this redirect
* for its own target URL, resulting in a redirect loop.
*/
$_redirect_parts = parse_url( $redirect_url );
$_requested_parts = parse_url( $requested_url );

if ( is_array( $_redirect_parts ) && is_array( $_requested_parts ) ) {
$_redirect_query = $_redirect_parts['query'] ?? '';
$_requested_query = $_requested_parts['query'] ?? '';

unset( $_redirect_parts['query'], $_redirect_parts['fragment'] );
unset( $_requested_parts['query'], $_requested_parts['fragment'] );

if ( $_redirect_parts === $_requested_parts ) {
parse_str( $_redirect_query, $_redirect_query_vars );
parse_str( $_requested_query, $_requested_query_vars );
ksort( $_redirect_query_vars );
ksort( $_requested_query_vars );

if ( $_redirect_query_vars === $_requested_query_vars ) {
return null;
}
}
}

// Hex-encoded octets are case-insensitive.
if ( str_contains( $requested_url, '%' ) ) {
if ( ! function_exists( 'lowercase_octets' ) ) {
Expand Down
44 changes: 44 additions & 0 deletions tests/phpunit/tests/canonical.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ public function data_canonical() {

array( '/2008/09/03/non-paged-post-test/3/', '/2008/09/03/non-paged-post-test/' ),
array( '/2008/09/03/non-paged-post-test/?page=3', '/2008/09/03/non-paged-post-test/' ),
array( '/2008/09/03/non-paged-post-test/3/?x=a|b', '/2008/09/03/non-paged-post-test/?x=a%7Cb', 41712 ), // Extra query vars stay encoded when the path changes.
array( '/2008/09/03/non-paged-post-test/?page=3&x=a+b', '/2008/09/03/non-paged-post-test/?x=a%20b', 41712 ),
array( '/2008/09/03/non-paged-post-test/?x=1&', '/2008/09/03/non-paged-post-test/?x=1&', 41712 ), // A stray '&' alone doesn't redirect.

// Comments.
array( '/2008/03/03/comment-test/?cpage=2', '/2008/03/03/comment-test/comment-page-2/' ),
Expand Down Expand Up @@ -437,6 +440,47 @@ public function test_utf8_query_keys_canonical() {
$this->assertNull( $redirect );
}

/**
* @ticket 41712
*
* @dataProvider data_no_redirect_when_query_only_differs_in_encoding
*
* @covers ::redirect_canonical
*
* @param string $query Query string requested on the front page.
*/
Comment on lines +443 to +451

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing @covers.

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.

Test removed, since the new provider's "plus for space" case covers it, and the provider test has @covers ::redirect_canonical.

public function test_no_redirect_when_query_only_differs_in_encoding( $query ) {
$p = self::factory()->post->create(
array(
'post_type' => 'page',
)
);
update_option( 'show_on_front', 'page' );
update_option( 'page_on_front', $p );

$url = home_url( '/?' . $query );
$this->go_to( $url );

$this->assertNull( redirect_canonical( $url, false ) );
}

/**
* Data provider.
*
* @return array[]
*/
public function data_no_redirect_when_query_only_differs_in_encoding() {
return array(
'plus for space' => array( 'utm_source=foo+bar' ),
'empty value' => array( 'utm_source=' ),
'unencoded pipe' => array( 'x=a|b' ),
'leading ampersand' => array( '&x=a+b' ),
'trailing ampersand' => array( 'x=1&' ),
'period in key' => array( 'a.b=c' ),
'array append' => array( 'a[]=1&a[]=2' ),
);
}

/**
* Ensure NOT EXISTS queries do not trigger not-countable or undefined array key errors.
*
Expand Down
Loading