From 3bbbd6c999c073ffcb6be7ec205c447ffa58319f Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Wed, 8 Jul 2026 07:25:59 +0530 Subject: [PATCH 1/4] HTTP API: Add Link preload header to internal redirects in wp_redirect(). --- src/wp-includes/pluggable.php | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index f659186b9a70c..95a4c20ac7571 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -1535,6 +1535,8 @@ function wp_redirect( $location, $status = 302, $x_redirect_by = 'WordPress' ) { header( "X-Redirect-By: $x_redirect_by" ); } + wp_add_preload_header( $location ); + header( "Location: $location", true, $status ); return true; @@ -1592,6 +1594,77 @@ function _wp_sanitize_utf8_in_redirect( $matches ) { } endif; +if ( ! function_exists( 'wp_is_internal_url' ) ) : + /** + * Checks whether a URL is internal to the site. + * + * @since 6.9.0 + * + * @param string $url The URL to check. + * @return bool True if the URL is relative or matches the site URL, false otherwise. + */ + function wp_is_internal_url( $url ) { + if ( '/' === substr( $url, 0, 1 ) ) { + return true; + } + + $site_url = site_url(); + + return substr( $url, 0, strlen( $site_url ) ) === $site_url; + } +endif; + +if ( ! function_exists( 'wp_create_preload_header' ) ) : + /** + * Creates a preload Link header string for the given URL. + * + * @since 6.9.0 + * + * @param string $url The URL to preload. + * @param string $type Optional. The preload type. Default 'document'. + * @return string The Link header string, or an empty string if not applicable. + */ + function wp_create_preload_header( $url, $type = 'document' ) { + if ( ! wp_is_internal_url( $url ) ) { + return ''; + } + + $parts = parse_url( $url ); + $path = isset( $parts['path'] ) ? $parts['path'] : ''; + + if ( '/' !== substr( $path, -1 ) && + '.php' !== substr( $path, -4 ) && + '.html' !== substr( $path, -5 ) ) { + return ''; + } + + $link = $path; + if ( isset( $parts['query'] ) ) { + $link .= '?' . $parts['query']; + } + + return "Link: <{$link}>; rel=preload; as={$type}"; + } +endif; + +if ( ! function_exists( 'wp_add_preload_header' ) ) : + /** + * Sends a preload Link header for the given URL if it is an internal URL. + * + * @since 6.9.0 + * + * @param string $url The URL to preload. + * @param string $type Optional. The preload type. Default 'document'. + */ + function wp_add_preload_header( $url, $type = 'document' ) { + $header = wp_create_preload_header( $url, $type ); + + if ( $header ) { + header( $header, false ); + } + } +endif; + if ( ! function_exists( 'wp_safe_redirect' ) ) : /** * Performs a safe (local) redirect, using wp_redirect(). From 6aeef88225d5e002d2ce86a7c1ded281fcb791f6 Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Wed, 8 Jul 2026 07:30:04 +0530 Subject: [PATCH 2/4] Tests: Add unit tests for wp_create_preload_header(). --- tests/phpunit/tests/formatting/redirect.php | 49 ++++++++++++++++++++ tests/phpunit/tests/pluggable/signatures.php | 9 ++++ 2 files changed, 58 insertions(+) diff --git a/tests/phpunit/tests/formatting/redirect.php b/tests/phpunit/tests/formatting/redirect.php index c4b1d682094f4..6b38a1256a154 100644 --- a/tests/phpunit/tests/formatting/redirect.php +++ b/tests/phpunit/tests/formatting/redirect.php @@ -260,4 +260,53 @@ public function data_wp_validate_redirect_relative_url() { ), ); } + + /** + * @ticket 39695 + * + * @dataProvider data_wp_create_preload_header + * + * @covers ::wp_create_preload_header + * + * @param string $url The URL to preload. + * @param string $expected Expected header string. + */ + public function test_wp_create_preload_header( $url, $expected ) { + $this->assertSame( $expected, wp_create_preload_header( $url ) ); + } + + /** + * Data provider for test_wp_create_preload_header(). + * + * @return array[] + */ + public function data_wp_create_preload_header() { + return array( + 'relative path ending in slash' => array( + '/wp-admin/', + 'Link: ; rel=preload; as=document', + ), + 'relative path ending in .php' => array( + '/wp-login.php', + 'Link: ; rel=preload; as=document', + ), + 'relative path with query string' => array( + '/wp-login.php?redirect_to=%2Fwp-admin%2F&reauth=1', + 'Link: ; rel=preload; as=document', + ), + 'same-site absolute URL' => array( + site_url( '/wp-login.php' ), + 'Link: ; rel=preload; as=document', + ), + 'external URL returns empty string' => array( + 'https://external.example.com/wp-login.php', + '', + ), + 'non-document extension returns empty string' => array( + '/wp-content/uploads/file.zip', + '', + ), + ); + } + } diff --git a/tests/phpunit/tests/pluggable/signatures.php b/tests/phpunit/tests/pluggable/signatures.php index 68b511b9d0d2d..80ec0b71d317b 100644 --- a/tests/phpunit/tests/pluggable/signatures.php +++ b/tests/phpunit/tests/pluggable/signatures.php @@ -180,6 +180,15 @@ public function get_pluggable_function_signatures() { ), 'wp_sanitize_redirect' => array( 'location' ), '_wp_sanitize_utf8_in_redirect' => array( 'matches' ), + 'wp_is_internal_url' => array( 'url' ), + 'wp_create_preload_header' => array( + 'url', + 'type' => 'document', + ), + 'wp_add_preload_header' => array( + 'url', + 'type' => 'document', + ), 'wp_safe_redirect' => array( 'location', 'status' => 302, From 2dc993130e60f00148b21bfff5ee039a4ab8d223 Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Wed, 8 Jul 2026 07:37:13 +0530 Subject: [PATCH 3/4] HTTP API: Fixed PHPCS error --- src/wp-includes/pluggable.php | 2 +- tests/phpunit/tests/formatting/redirect.php | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 95a4c20ac7571..b517cef2ff015 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -2445,7 +2445,7 @@ function wp_new_user_notification( $user_id, $deprecated = null, $notify = '' ) $switched_locale = switch_to_user_locale( $user_id ); - $message = __( 'To set your password, visit the following address:' ) . "\r\n\r\n"; + $message = __( 'To set your password, visit the following address:' ) . "\r\n\r\n"; /* * Since some user login names end in a period, this could produce ambiguous URLs that diff --git a/tests/phpunit/tests/formatting/redirect.php b/tests/phpunit/tests/formatting/redirect.php index 6b38a1256a154..9e4f2e7a9aeb8 100644 --- a/tests/phpunit/tests/formatting/redirect.php +++ b/tests/phpunit/tests/formatting/redirect.php @@ -282,23 +282,23 @@ public function test_wp_create_preload_header( $url, $expected ) { */ public function data_wp_create_preload_header() { return array( - 'relative path ending in slash' => array( + 'relative path ending in slash' => array( '/wp-admin/', 'Link: ; rel=preload; as=document', ), - 'relative path ending in .php' => array( + 'relative path ending in .php' => array( '/wp-login.php', 'Link: ; rel=preload; as=document', ), - 'relative path with query string' => array( + 'relative path with query string' => array( '/wp-login.php?redirect_to=%2Fwp-admin%2F&reauth=1', 'Link: ; rel=preload; as=document', ), - 'same-site absolute URL' => array( + 'same-site absolute URL' => array( site_url( '/wp-login.php' ), 'Link: ; rel=preload; as=document', ), - 'external URL returns empty string' => array( + 'external URL returns empty string' => array( 'https://external.example.com/wp-login.php', '', ), @@ -308,5 +308,4 @@ public function data_wp_create_preload_header() { ), ); } - } From 1ee71190c7e7c4f65846a96dfd818c5affa0b4ef Mon Sep 17 00:00:00 2001 From: Sainath Poojary Date: Thu, 9 Jul 2026 02:18:20 +0530 Subject: [PATCH 4/4] HTTP API: Avoid calling site_url() before WordPress is installed in wp_is_internal_url(). --- src/wp-includes/pluggable.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index b517cef2ff015..57c32801993ce 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -1608,6 +1608,10 @@ function wp_is_internal_url( $url ) { return true; } + if ( ! is_blog_installed() ) { + return false; + } + $site_url = site_url(); return substr( $url, 0, strlen( $site_url ) ) === $site_url;