diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 6b8c17c07d55a..51135d66491db 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -9,6 +9,31 @@ * @since 2.3.0 */ +/** + * Redirects HTTP requests to HTTPS when SSL is forced for the site. + * + * @since x.x.x + * @access private + */ +function _wp_force_ssl_redirect() { + if ( ! force_ssl() || is_ssl() || ! isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { + return; + } + + if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'HEAD' ), true ) ) { + return; + } + + if ( str_starts_with( $_SERVER['REQUEST_URI'], 'http' ) ) { + $redirect = set_url_scheme( wp_unslash( $_SERVER['REQUEST_URI'] ), 'https' ); + } else { + $redirect = 'https://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ); + } + + wp_safe_redirect( $redirect ); + exit; +} + /** * Redirects incoming links to the proper URL based on the site url. * diff --git a/src/wp-includes/class-wp-recovery-mode-cookie-service.php b/src/wp-includes/class-wp-recovery-mode-cookie-service.php index a2ee34a723aa8..d62bd50453759 100644 --- a/src/wp-includes/class-wp-recovery-mode-cookie-service.php +++ b/src/wp-includes/class-wp-recovery-mode-cookie-service.php @@ -47,10 +47,12 @@ public function set_cookie() { $expire = time() + $length; - setcookie( RECOVERY_MODE_COOKIE, $value, $expire, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + $secure = is_ssl() || force_ssl(); + + setcookie( RECOVERY_MODE_COOKIE, $value, $expire, COOKIEPATH, COOKIE_DOMAIN, $secure, true ); if ( COOKIEPATH !== SITECOOKIEPATH ) { - setcookie( RECOVERY_MODE_COOKIE, $value, $expire, SITECOOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); + setcookie( RECOVERY_MODE_COOKIE, $value, $expire, SITECOOKIEPATH, COOKIE_DOMAIN, $secure, true ); } } diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index acfc878fb7138..0da2f5935e321 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -347,17 +347,25 @@ function wp_cookie_constants() { * @since 3.0.0 */ function wp_ssl_constants() { + /** + * @since x.x.x + */ + if ( ! defined( 'FORCE_SSL' ) ) { + define( 'FORCE_SSL', false ); + } + force_ssl( FORCE_SSL ); + /** * @since 2.6.0 */ if ( ! defined( 'FORCE_SSL_ADMIN' ) ) { - if ( 'https' === parse_url( get_option( 'siteurl' ), PHP_URL_SCHEME ) ) { + if ( force_ssl() || 'https' === parse_url( get_option( 'siteurl' ), PHP_URL_SCHEME ) ) { define( 'FORCE_SSL_ADMIN', true ); } else { define( 'FORCE_SSL_ADMIN', false ); } } - force_ssl_admin( FORCE_SSL_ADMIN ); + force_ssl_admin( force_ssl() || FORCE_SSL_ADMIN ); /** * @since 2.6.0 diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 4e0b74a8d22c2..d9597852a6faf 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -659,7 +659,9 @@ add_action( 'wp_footer', 'wp_enqueue_stored_styles', 1 ); add_action( 'wp_default_styles', 'wp_default_styles' ); +add_filter( 'script_loader_src', '_wp_force_ssl_url' ); add_filter( 'style_loader_src', 'wp_style_loader_src', 10, 2 ); +add_filter( 'style_loader_src', '_wp_force_ssl_url', 11 ); add_action( 'wp_head', 'wp_enqueue_img_auto_sizes_contain_css_fix', 0 ); // Must run before wp_print_auto_sizes_contain_css_fix(). add_action( 'wp_head', 'wp_print_auto_sizes_contain_css_fix', 1 ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_img_auto_sizes_contain_css_fix(). @@ -687,6 +689,7 @@ add_action( 'change_locale', 'create_initial_taxonomies' ); // Canonical. +add_action( 'template_redirect', '_wp_force_ssl_redirect', 0 ); add_action( 'template_redirect', 'redirect_canonical' ); add_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 64dce4a159755..e844539506cce 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -6360,6 +6360,26 @@ function force_ssl_admin( $force = null ) { return $forced; } +/** + * Determines whether to force SSL for the entire site. + * + * @since x.x.x + * + * @param string|bool|null $force Optional. Whether to force SSL. Default null. + * @return bool True if forced, false if not forced. + */ +function force_ssl( $force = null ) { + static $forced = false; + + if ( ! is_null( $force ) ) { + $old_forced = $forced; + $forced = (bool) $force; + return $old_forced; + } + + return $forced; +} + /** * Guesses the URL for the site. * diff --git a/src/wp-includes/https-migration.php b/src/wp-includes/https-migration.php index 1232154bbced1..4be75e2da8c93 100644 --- a/src/wp-includes/https-migration.php +++ b/src/wp-includes/https-migration.php @@ -18,8 +18,8 @@ * @return bool True if insecure URLs should replaced, false otherwise. */ function wp_should_replace_insecure_home_url() { - $should_replace_insecure_home_url = wp_is_using_https() - && get_option( 'https_migration_required' ) + $should_replace_insecure_home_url = ( force_ssl() || wp_is_using_https() ) + && ( force_ssl() || get_option( 'https_migration_required' ) ) // For automatic replacement, both 'home' and 'siteurl' need to not only use HTTPS, they also need to be using // the same domain. && wp_parse_url( home_url(), PHP_URL_HOST ) === wp_parse_url( site_url(), PHP_URL_HOST ); diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 223d6b5548fc6..76b67c446112e 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -3469,7 +3469,7 @@ function get_home_url( $blog_id = null, $path = '', $scheme = null ) { } if ( ! in_array( $scheme, array( 'http', 'https', 'relative' ), true ) ) { - if ( is_ssl() ) { + if ( is_ssl() || force_ssl() ) { $scheme = 'https'; } else { $scheme = parse_url( $url, PHP_URL_SCHEME ); @@ -3790,7 +3790,7 @@ function network_home_url( $path = '', $scheme = null ) { $orig_scheme = $scheme; if ( ! in_array( $scheme, array( 'http', 'https', 'relative' ), true ) ) { - $scheme = is_ssl() ? 'https' : 'http'; + $scheme = is_ssl() || force_ssl() ? 'https' : 'http'; } if ( 'relative' === $scheme ) { @@ -3931,11 +3931,11 @@ function set_url_scheme( $url, $scheme = null ) { $orig_scheme = $scheme; if ( ! $scheme ) { - $scheme = is_ssl() ? 'https' : 'http'; + $scheme = is_ssl() || force_ssl() ? 'https' : 'http'; } elseif ( 'admin' === $scheme || 'login' === $scheme || 'login_post' === $scheme || 'rpc' === $scheme ) { - $scheme = is_ssl() || force_ssl_admin() ? 'https' : 'http'; + $scheme = is_ssl() || force_ssl() || force_ssl_admin() ? 'https' : 'http'; } elseif ( 'http' !== $scheme && 'https' !== $scheme && 'relative' !== $scheme ) { - $scheme = is_ssl() ? 'https' : 'http'; + $scheme = is_ssl() || force_ssl() ? 'https' : 'http'; } $url = trim( $url ); @@ -3965,6 +3965,37 @@ function set_url_scheme( $url, $scheme = null ) { return apply_filters( 'set_url_scheme', $url, $scheme, $orig_scheme ); } +/** + * Converts a local URL to HTTPS when SSL is forced. + * + * @since x.x.x + * @access private + * + * @param string $url URL to check. + * @return string HTTPS URL when local SSL is forced, otherwise the original URL. + */ +function _wp_force_ssl_url( $url ) { + if ( ! force_ssl() || ! is_string( $url ) || ! preg_match( '#^(https?:)?//#i', $url ) ) { + return $url; + } + + $host = wp_parse_url( $url, PHP_URL_HOST ); + if ( ! $host ) { + return $url; + } + + $host = strtolower( $host ); + $site_hosts = array_filter( + array( + wp_parse_url( home_url( '', 'http' ), PHP_URL_HOST ), + wp_parse_url( site_url( '', 'http' ), PHP_URL_HOST ), + ) + ); + $site_hosts = array_map( 'strtolower', $site_hosts ); + + return in_array( $host, $site_hosts, true ) ? set_url_scheme( $url, 'https' ) : $url; +} + /** * Retrieves the URL to the user's dashboard. * diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index f659186b9a70c..2a2e2678db737 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -1093,11 +1093,11 @@ function wp_set_auth_cookie( $user_id, $remember = false, $secure = '', $token = } if ( '' === $secure ) { - $secure = is_ssl(); + $secure = is_ssl() || force_ssl(); } // Front-end cookie is secure when the auth cookie is secure and the site's home URL uses HTTPS. - $secure_logged_in_cookie = $secure && 'https' === parse_url( get_option( 'home' ), PHP_URL_SCHEME ); + $secure_logged_in_cookie = $secure && ( force_ssl() || 'https' === parse_url( get_option( 'home' ), PHP_URL_SCHEME ) ); /** * Filters whether the auth cookie should only be sent over HTTPS. @@ -1278,7 +1278,7 @@ function is_user_logged_in() { * @since 1.5.0 */ function auth_redirect() { - $secure = ( is_ssl() || force_ssl_admin() ); + $secure = ( is_ssl() || force_ssl() || force_ssl_admin() ); /** * Filters whether to use a secure authentication redirect. diff --git a/src/wp-login.php b/src/wp-login.php index abedea82c3589..bfe7af0deacd1 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -792,6 +792,8 @@ function wp_login_viewport_meta() { $secure = false; } + $secure = $secure || force_ssl(); + setcookie( 'wp-postpass_' . COOKIEHASH, $hasher->HashPassword( wp_unslash( $_POST['post_password'] ) ), $expire, COOKIEPATH, COOKIE_DOMAIN, $secure ); wp_safe_redirect( $redirect_to ); @@ -942,7 +944,7 @@ function wp_login_viewport_meta() { if ( isset( $_GET['key'] ) && isset( $_GET['login'] ) ) { $value = sprintf( '%s:%s', wp_unslash( $_GET['login'] ), wp_unslash( $_GET['key'] ) ); - setcookie( $rp_cookie, $value, 0, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); + setcookie( $rp_cookie, $value, 0, $rp_path, COOKIE_DOMAIN, is_ssl() || force_ssl(), true ); wp_safe_redirect( remove_query_arg( array( 'key', 'login' ) ) ); exit; @@ -961,7 +963,7 @@ function wp_login_viewport_meta() { } if ( ! $user || is_wp_error( $user ) ) { - setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); + setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl() || force_ssl(), true ); if ( $user && $user->get_error_code() === 'expired_key' ) { wp_redirect( site_url( 'wp-login.php?action=lostpassword&error=expiredkey' ) ); @@ -1491,7 +1493,7 @@ function wp_login_viewport_meta() { if ( isset( $_COOKIE[ $rp_cookie ] ) && is_string( $_COOKIE[ $rp_cookie ] ) ) { $user_login = sanitize_user( strtok( wp_unslash( $_COOKIE[ $rp_cookie ] ), ':' ) ); list( $rp_path ) = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) ); - setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); + setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl() || force_ssl(), true ); } login_header( __( 'Log In' ), '', $errors ); diff --git a/tests/phpunit/tests/functions/forceSslAdmin.php b/tests/phpunit/tests/functions/forceSslAdmin.php index e0432c382e959..3792192673bd2 100644 --- a/tests/phpunit/tests/functions/forceSslAdmin.php +++ b/tests/phpunit/tests/functions/forceSslAdmin.php @@ -1,19 +1,21 @@ array( 0, false ), ); } + + /** + * Tests that force_ssl() returns expected values based on various inputs. + * + * @ticket 28521 + * + * @dataProvider data_force_ssl_admin + * + * @param mixed $input The input value to test. + * @param bool $expected The expected result for subsequent calls. + */ + public function test_force_ssl( $input, $expected ) { + // The first call always returns the previous value. + $this->assertFalse( force_ssl( $input ), 'First call did not return the expected value' ); + + // Call again to check subsequent behavior. + $this->assertSame( $expected, force_ssl( $input ), 'Subsequent call did not return the expected value' ); + $this->assertSame( $expected, force_ssl(), 'Getter did not return the expected value' ); + } } diff --git a/tests/phpunit/tests/https-migration.php b/tests/phpunit/tests/https-migration.php index ae66738603e13..1d0938a34579d 100644 --- a/tests/phpunit/tests/https-migration.php +++ b/tests/phpunit/tests/https-migration.php @@ -5,6 +5,11 @@ */ class Tests_HTTPS_Migration extends WP_UnitTestCase { + public function tear_down() { + force_ssl( false ); + parent::tear_down(); + } + /** * @ticket 51437 */ @@ -25,6 +30,11 @@ public function test_wp_should_replace_insecure_home_url() { update_option( 'https_migration_required', '1' ); $this->assertTrue( wp_should_replace_insecure_home_url() ); + // Should return true when SSL is forced, even without the HTTPS migration flag. + update_option( 'https_migration_required', false ); + force_ssl( true ); + $this->assertTrue( wp_should_replace_insecure_home_url() ); + // Should be overridable via filter. add_filter( 'wp_should_replace_insecure_home_url', '__return_false' ); $this->assertFalse( wp_should_replace_insecure_home_url() ); diff --git a/tests/phpunit/tests/url.php b/tests/phpunit/tests/url.php index 3734891eb19ac..2840606fb4f10 100644 --- a/tests/phpunit/tests/url.php +++ b/tests/phpunit/tests/url.php @@ -356,6 +356,48 @@ public function test_set_url_scheme() { force_ssl_admin( $forced_admin ); } + /** + * @ticket 28521 + * + * @covers ::set_url_scheme + * @covers ::get_home_url + * @covers ::_wp_force_ssl_url + */ + public function test_force_ssl_sets_local_url_schemes_to_https() { + $forced_ssl = force_ssl(); + $forced_admin = force_ssl_admin(); + $server_https = $_SERVER['HTTPS'] ?? null; + $_SERVER['HTTPS'] = 'off'; + force_ssl_admin( false ); + force_ssl( true ); + + try { + $http_home = home_url( '/foo/', 'http' ); + $https_home = home_url( '/foo/', 'https' ); + + $this->assertSame( $https_home, set_url_scheme( $http_home ) ); + $this->assertSame( $https_home, set_url_scheme( $http_home, 'admin' ) ); + $this->assertSame( $https_home, home_url( '/foo/' ) ); + $this->assertSame( $https_home, network_home_url( '/foo/' ) ); + $this->assertSame( $http_home, home_url( '/foo/', 'http' ) ); + $this->assertSame( $https_home, _wp_force_ssl_url( $http_home ) ); + $this->assertSame( 'https://example.org/foo/', _wp_force_ssl_url( '//example.org/foo/' ) ); + $this->assertSame( 'http://example.com/foo/', _wp_force_ssl_url( 'http://example.com/foo/' ) ); + $this->assertSame( '/foo/', _wp_force_ssl_url( '/foo/' ) ); + + force_ssl( false ); + $this->assertSame( $http_home, _wp_force_ssl_url( $http_home ) ); + } finally { + force_ssl( $forced_ssl ); + force_ssl_admin( $forced_admin ); + if ( null === $server_https ) { + unset( $_SERVER['HTTPS'] ); + } else { + $_SERVER['HTTPS'] = $server_https; + } + } + } + /** * @covers ::get_adjacent_post */