diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 964e56c6148fa..b14e2ac5bcf37 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -1067,4 +1067,23 @@ function wp_redirect_admin_locations() { wp_redirect( wp_login_url() ); exit; } + + $password_change_urls = array( + '/.well-known/change-password', + home_url( '.well-known/change-password', 'relative' ), + site_url( '.well-known/change-password', 'relative' ), + ); + + if ( in_array( untrailingslashit( $_SERVER['REQUEST_URI'] ), $password_change_urls, true ) ) { + /** + * Filters the URL to redirect to when a browser or password manager + * requests the well-known change-password URL (/.well-known/change-password). + * + * @since 6.9.0 + * + * @param string $url The URL to redirect to. Default is the user profile page. + */ + wp_redirect( apply_filters( 'wp_change_password_url', admin_url( 'profile.php' ) ) ); + exit; + } } diff --git a/tests/phpunit/tests/canonical/changePassword.php b/tests/phpunit/tests/canonical/changePassword.php new file mode 100644 index 0000000000000..9ffdf1b5760b3 --- /dev/null +++ b/tests/phpunit/tests/canonical/changePassword.php @@ -0,0 +1,119 @@ +request_uri_was_set = isset( $_SERVER['REQUEST_URI'] ); + $this->original_request_uri = $_SERVER['REQUEST_URI'] ?? ''; + $this->set_permalink_structure( '/%postname%/' ); + } + + public function tear_down() { + if ( $this->request_uri_was_set ) { + $_SERVER['REQUEST_URI'] = $this->original_request_uri; + } else { + unset( $_SERVER['REQUEST_URI'] ); + } + parent::tear_down(); + } + + /** + * Returns the redirect URL triggered by wp_redirect_admin_locations() + * for the given REQUEST_URI, or null if no redirect fires. + * + * @param string $request_uri + * @return string|null + */ + private function get_redirect_for( $request_uri ) { + $_SERVER['REQUEST_URI'] = $request_uri; + + global $wp_query; + $original_is_404 = $wp_query->is_404; + $wp_query->is_404 = true; + + $captured = null; + + $capture = static function ( $location ) use ( &$captured ) { + $captured = $location; + throw new RuntimeException( 'Redirect intercepted.' ); + }; + + add_filter( 'wp_redirect', $capture ); + try { + wp_redirect_admin_locations(); + } catch ( RuntimeException $e ) { + // Redirect was intercepted; $captured holds the URL. + } finally { + remove_filter( 'wp_redirect', $capture ); + $wp_query->is_404 = $original_is_404; + } + + return $captured; + } + + /** + * @covers ::wp_redirect_admin_locations + */ + public function test_well_known_change_password_redirects_to_profile() { + $redirect = $this->get_redirect_for( '/.well-known/change-password' ); + $this->assertNotNull( $redirect, 'A redirect should fire for the well-known change-password URL.' ); + $this->assertStringContainsString( 'profile.php', $redirect, 'Should redirect to the profile page.' ); + } + + /** + * @covers ::wp_redirect_admin_locations + */ + public function test_well_known_change_password_with_trailing_slash_redirects_to_profile() { + $redirect = $this->get_redirect_for( '/.well-known/change-password/' ); + $this->assertNotNull( $redirect, 'A redirect should fire for the trailing-slash variant.' ); + $this->assertStringContainsString( 'profile.php', $redirect, 'Trailing slash variant should also redirect to the profile page.' ); + } + + /** + * @covers ::wp_redirect_admin_locations + */ + public function test_well_known_change_password_no_redirect_without_pretty_permalinks() { + $this->set_permalink_structure( '' ); + + $redirect = $this->get_redirect_for( '/.well-known/change-password' ); + + $this->assertNull( $redirect, 'Should not redirect when pretty permalinks are disabled.' ); + } + + /** + * @covers ::wp_redirect_admin_locations + */ + public function test_wp_change_password_url_filter() { + $custom_url = 'https://example.com/my-account/change-password/'; + $filter = static fn() => $custom_url; + + add_filter( 'wp_change_password_url', $filter ); + $redirect = $this->get_redirect_for( '/.well-known/change-password' ); + remove_filter( 'wp_change_password_url', $filter ); + + $this->assertSame( $custom_url, $redirect ); + } +}