diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 58723ebc0d8a4..00db00c3c8bcd 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -600,6 +600,10 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { // www.example.com vs. example.com $user_home = parse_url( home_url() ); + if ( ! empty( $user_home['scheme'] ) ) { + $redirect['scheme'] = $user_home['scheme']; + } + if ( ! empty( $user_home['host'] ) ) { $redirect['host'] = $user_home['host']; } @@ -717,13 +721,24 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { $original_host_low = strtolower( $original['host'] ); $redirect_host_low = strtolower( $redirect['host'] ); + $original_port = isset( $original['port'] ) ? strtolower( $original['port'] ) : '80'; + $redirect_port = isset( $redirect['port'] ) ? strtolower( $redirect['port'] ) : '80'; + /* - * Ignore differences in host capitalization, as this can lead to infinite redirects. - * Only redirect no-www <=> yes-www. + * Preserve original host casing if the hostnames are identical (ignoring case), + * or differ in a way that is not just a www <=> non-www variation, + * and the port is the same. + * + * This prevents unnecessary redirects and avoids redirect loops due to host casing, + * but still allows canonical redirects between www and non-www variants. */ - if ( $original_host_low === $redirect_host_low - || ( 'www.' . $original_host_low !== $redirect_host_low - && 'www.' . $redirect_host_low !== $original_host_low ) + if ( + $original_host_low === $redirect_host_low + || ( + 'www.' . $original_host_low !== $redirect_host_low + && 'www.' . $redirect_host_low !== $original_host_low + && $original_port === $redirect_port + ) ) { $redirect['host'] = $original['host']; } diff --git a/tests/phpunit/tests/canonical.php b/tests/phpunit/tests/canonical.php index 886b09312910e..140d8fbcf26b5 100644 --- a/tests/phpunit/tests/canonical.php +++ b/tests/phpunit/tests/canonical.php @@ -545,4 +545,92 @@ public function data_canonical_attachment_page_redirect_with_option_disabled() { ), ); } + + /** + * Test domain and port redirections. + * + * @ticket 33821 + * @dataProvider data_domain_and_port_redirections + * + * @covers ::redirect_canonical + * + * @param string $home_url The home URL to set. + * @param string $site_url The site URL to set. + * @param string $request_url The URL to test redirection for. + * @param string $expected_url The expected redirect URL, or null if no redirect. + * @param string $failure_msg The message to display on test failure. + */ + public function test_domain_and_port_redirections( $home_url, $site_url, $request_url, $expected_url, $failure_msg ) { + update_option( 'home', $home_url ); + update_option( 'siteurl', $site_url ); + + $redirect = redirect_canonical( $request_url, false ); + + $this->assertSame( $expected_url, $redirect, $failure_msg ); + } + + /** + * Data provider for test_domain_and_port_redirections(). + * + * @return array[] { + * @type string $home_url The home URL to set. + * @type string $site_url The site URL to set. + * @type string $request_url The URL to test redirection for. + * @type string $expected_url The expected redirect URL, or null if no redirect. + * @type string $failure_msg The message to display on test failure. + * } + */ + public function data_domain_and_port_redirections() { + return array( + 'non-standard localhost port to canonical domain' => array( + 'home_url' => 'http://example.com', + 'site_url' => 'http://example.com', + 'request_url' => 'http://localhost:10020/', + 'expected_url' => 'http://example.com/', + 'failure_msg' => 'Failed to redirect non-standard localhost port to canonical domain', + ), + 'non-standard localhost port to canonical domain with SSL' => array( + 'home_url' => 'https://example.com', + 'site_url' => 'https://example.com', + 'request_url' => 'http://localhost:10020/', + 'expected_url' => 'https://example.com/', + 'failure_msg' => 'Failed to redirect non-standard localhost port to canonical domain with SSL', + ), + 'different host casing do not redirect' => array( + 'home_url' => 'http://example.com', + 'site_url' => 'http://example.com', + 'request_url' => 'http://Example.com/', + 'expected_url' => null, + 'failure_msg' => 'Should not redirect when only host casing differs', + ), + 'different host casing with port redirect without host change' => array( + 'home_url' => 'http://example.com:8080', + 'site_url' => 'http://example.com:8080', + 'request_url' => 'http://Example.com:10200/', + 'expected_url' => 'http://Example.com:8080/', + 'failure_msg' => 'Failed to redirect to correct port while preserving host casing', + ), + 'www to non-www' => array( + 'home_url' => 'http://example.com', + 'site_url' => 'http://example.com', + 'request_url' => 'http://www.example.com/', + 'expected_url' => 'http://example.com/', + 'failure_msg' => 'Failed to redirect www to non-www domain', + ), + 'non-www to www' => array( + 'home_url' => 'http://www.example.com', + 'site_url' => 'http://www.example.com', + 'request_url' => 'http://example.com/', + 'expected_url' => 'http://www.example.com/', + 'failure_msg' => 'Failed to redirect non-www to www domain', + ), + 'port for same host' => array( + 'home_url' => 'http://example.com:8080', + 'site_url' => 'http://example.com:8080', + 'request_url' => 'http://example.com:10200/', + 'expected_url' => 'http://example.com:8080/', + 'failure_msg' => 'Failed to redirect to correct port for same host', + ), + ); + } }