From 4e0bc3bc9395d8f24349ec9b7e3d1daa4271a9bd Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 29 Jan 2021 19:09:49 +0000 Subject: [PATCH] Security, Site Health: Improve accuracy in messaging about HTTPS support. Following up on [49904], this changeset focuses mainly on improving the guidance about the current state of HTTPS in Site Health. * Correct the existing copy to indicate that both the Site Address and the WordPress Address need to be changed to fully switch to HTTPS. * Link to the respective input fields via anchor links rather than to the overall General Settings screen. * Show different copy if the site is using HTTPS for the WordPress Address (for example to have only the administration panel in HTTPS), but not for the Site Address. * Inform the user about potential problems even when the site is already using HTTPS, for example if the SSL certificate was no longer valid. * Always rely on fresh information for determining HTTPS support issues in Site Health, and therefore change the `https_status` test to become asynchronous. * Rename the new private `wp_is_owned_html_output()` function to a more appropriate `wp_is_local_html_output()`. Props adamsilverstein, flixos90, johnjamesjacoby, timothyblynjacobs. See #47577. git-svn-id: https://develop.svn.wordpress.org/trunk@50072 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-site-health.php | 108 ++++++++++++++---- src/wp-includes/https-detection.php | 45 ++++++-- .../class-wp-rest-site-health-controller.php | 31 +++++ tests/phpunit/tests/https-detection.php | 32 +++--- .../tests/rest-api/rest-schema-setup.php | 1 + tests/qunit/fixtures/wp-api-generated.js | 21 ++++ 6 files changed, 189 insertions(+), 49 deletions(-) diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index f08215659f..9d3a455f23 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -108,7 +108,7 @@ class WP_Site_Health { // Don't run https test on development environments. if ( $this->is_development_environment() ) { - unset( $tests['direct']['https_status'] ); + unset( $tests['async']['https_status'] ); } foreach ( $tests['direct'] as $test ) { @@ -1498,6 +1498,10 @@ class WP_Site_Health { * @return array The test results. */ public function get_test_https_status() { + // Enforce fresh HTTPS detection results. This is normally invoked by using cron, but for Site Health it should + // always rely on the latest results. + wp_update_https_detection_errors(); + $result = array( 'label' => __( 'Your website is using an active HTTPS connection' ), 'status' => 'good', @@ -1521,27 +1525,53 @@ class WP_Site_Health { ); if ( ! wp_is_using_https() ) { + // If the website is not using HTTPS, provide more information about whether it is supported and how it can + // be enabled. $result['status'] = 'critical'; $result['label'] = __( 'Your website does not use HTTPS' ); - if ( is_ssl() ) { - $result['description'] = sprintf( - '

%s

', - sprintf( - /* translators: %s: URL to General Settings screen. */ - __( 'You are accessing this website using HTTPS, but your WordPress Address is not set up to use HTTPS by default.' ), - esc_url( admin_url( 'options-general.php' ) ) - ) - ); + if ( wp_is_site_url_using_https() ) { + if ( is_ssl() ) { + $result['description'] = sprintf( + '

%s

', + sprintf( + /* translators: %s: URL to Settings > General > Site Address. */ + __( 'You are accessing this website using HTTPS, but your Site Address is not set up to use HTTPS by default.' ), + esc_url( admin_url( 'options-general.php' ) . '#home' ) + ) + ); + } else { + $result['description'] = sprintf( + '

%s

', + sprintf( + /* translators: %s: URL to Settings > General > Site Address. */ + __( 'Your Site Address is not set up to use HTTPS.' ), + esc_url( admin_url( 'options-general.php' ) . '#home' ) + ) + ); + } } else { - $result['description'] = sprintf( - '

%s

', - sprintf( - /* translators: %s: URL to General Settings screen. */ - __( 'Your WordPress Address is not set up to use HTTPS.' ), - esc_url( admin_url( 'options-general.php' ) ) - ) - ); + if ( is_ssl() ) { + $result['description'] = sprintf( + '

%s

', + sprintf( + /* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */ + __( 'You are accessing this website using HTTPS, but your WordPress Address and Site Address are not set up to use HTTPS by default.' ), + esc_url( admin_url( 'options-general.php' ) . '#siteurl' ), + esc_url( admin_url( 'options-general.php' ) . '#home' ) + ) + ); + } else { + $result['description'] = sprintf( + '

%s

', + sprintf( + /* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */ + __( 'Your WordPress Address and Site Address are not set up to use HTTPS.' ), + esc_url( admin_url( 'options-general.php' ) . '#siteurl' ), + esc_url( admin_url( 'options-general.php' ) . '#home' ) + ) + ); + } } if ( wp_is_https_supported() ) { @@ -1561,6 +1591,36 @@ class WP_Site_Health { __( 'Talk to your web host about supporting HTTPS for your website.' ) ); } + } elseif ( ! wp_is_https_supported() ) { + // If the website is using HTTPS, but HTTPS is actually not supported, inform the user about the potential + // problems. + $result['status'] = 'critical'; + $result['label'] = __( 'There are problems with the HTTPS connection of your website' ); + + $https_detection_errors = get_option( 'https_detection_errors' ); + if ( ! empty( $https_detection_errors['ssl_verification_failed'] ) ) { + $result['description'] = sprintf( + '

%s

', + sprintf( + /* translators: %s: URL to Settings > General > WordPress Address. */ + __( 'Your WordPress Address is set up to use HTTPS, but the SSL certificate appears to be invalid.' ), + esc_url( admin_url( 'options-general.php' ) . '#siteurl' ) + ) + ); + } else { + $result['description'] = sprintf( + '

%s

', + sprintf( + /* translators: %s: URL to Settings > General > WordPress Address. */ + __( 'Your WordPress Address is set up to use HTTPS, but your website appears to be unavailable when using an HTTPS connection.' ), + esc_url( admin_url( 'options-general.php' ) . '#siteurl' ) + ) + ); + } + $result['description'] .= sprintf( + '

%s

', + __( 'Talk to your web host about resolving this HTTPS issue for your website.' ) + ); } return $result; @@ -2200,10 +2260,6 @@ class WP_Site_Health { 'label' => __( 'MySQL utf8mb4 support' ), 'test' => 'utf8mb4_support', ), - 'https_status' => array( - 'label' => __( 'HTTPS status' ), - 'test' => 'https_status', - ), 'ssl_support' => array( 'label' => __( 'Secure communication' ), 'test' => 'ssl_support', @@ -2248,6 +2304,12 @@ class WP_Site_Health { 'has_rest' => true, 'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_loopback_requests' ), ), + 'https_status' => array( + 'label' => __( 'HTTPS status' ), + 'test' => rest_url( 'wp-site-health/v1/tests/https-status' ), + 'has_rest' => true, + 'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_https_status' ), + ), 'authorization_header' => array( 'label' => __( 'Authorization header' ), 'test' => rest_url( 'wp-site-health/v1/tests/authorization-header' ), @@ -2614,7 +2676,7 @@ class WP_Site_Health { // Don't run https test on development environments. if ( $this->is_development_environment() ) { - unset( $tests['direct']['https_status'] ); + unset( $tests['async']['https_status'] ); } foreach ( $tests['direct'] as $test ) { diff --git a/src/wp-includes/https-detection.php b/src/wp-includes/https-detection.php index 87c331bf24..f6562bbca0 100644 --- a/src/wp-includes/https-detection.php +++ b/src/wp-includes/https-detection.php @@ -9,28 +9,53 @@ /** * Checks whether the website is using HTTPS. * - * This is based on whether the home and site URL are using HTTPS. + * This is based on whether both the home and site URL are using HTTPS. * * @since 5.7.0 + * @see wp_is_home_url_using_https() + * @see wp_is_site_url_using_https() * * @return bool True if using HTTPS, false otherwise. */ function wp_is_using_https() { - if ( 'https' !== wp_parse_url( home_url(), PHP_URL_SCHEME ) ) { + if ( ! wp_is_home_url_using_https() ) { return false; } + return wp_is_site_url_using_https(); +} + +/** + * Checks whether the current site URL is using HTTPS. + * + * @since 5.7.0 + * @see home_url() + * + * @return bool True if using HTTPS, false otherwise. + */ +function wp_is_home_url_using_https() { + return 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ); +} + +/** + * Checks whether the current site's URL where WordPress is stored is using HTTPS. + * + * This checks the URL where WordPress application files (e.g. wp-blog-header.php or the wp-admin/ folder) are + * accessible. + * + * @since 5.7.0 + * @see site_url() + * + * @return bool True if using HTTPS, false otherwise. + */ +function wp_is_site_url_using_https() { // Use direct option access for 'siteurl' and manually run the 'site_url' - // filter because site_url() will adjust the scheme based on what the + // filter because `site_url()` will adjust the scheme based on what the // current request is using. /** This filter is documented in wp-includes/link-template.php */ $site_url = apply_filters( 'site_url', get_option( 'siteurl' ), '', null, null ); - if ( 'https' !== wp_parse_url( $site_url, PHP_URL_SCHEME ) ) { - return false; - } - - return true; + return 'https' === wp_parse_url( $site_url, PHP_URL_SCHEME ); } /** @@ -104,7 +129,7 @@ function wp_update_https_detection_errors() { if ( ! is_wp_error( $response ) ) { if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $support_errors->add( 'bad_response_code', wp_remote_retrieve_response_message( $response ) ); - } elseif ( false === wp_is_owned_html_output( wp_remote_retrieve_body( $response ) ) ) { + } elseif ( false === wp_is_local_html_output( wp_remote_retrieve_body( $response ) ) ) { $support_errors->add( 'bad_response_source', __( 'It looks like the response did not come from this site.' ) ); } } @@ -159,7 +184,7 @@ function wp_cron_conditionally_prevent_sslverify( $request ) { * @param string $html Full HTML output string, e.g. from a HTTP response. * @return bool|null True/false for whether HTML was generated by this site, null if unable to determine. */ -function wp_is_owned_html_output( $html ) { +function wp_is_local_html_output( $html ) { // 1. Check if HTML includes the site's Really Simple Discovery link. if ( has_action( 'wp_head', 'rsd_link' ) ) { $pattern = esc_url( site_url( 'xmlrpc.php?rsd', 'rpc' ) ); // See rsd_link(). diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php index f790dadb04..d5d78255cf 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-site-health-controller.php @@ -85,6 +85,25 @@ class WP_REST_Site_Health_Controller extends WP_REST_Controller { ) ); + register_rest_route( + $this->namespace, + sprintf( + '/%s/%s', + $this->rest_base, + 'https-status' + ), + array( + array( + 'methods' => 'GET', + 'callback' => array( $this, 'test_https_status' ), + 'permission_callback' => function () { + return $this->validate_request_permission( 'https_status' ); + }, + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( $this->namespace, sprintf( @@ -199,6 +218,18 @@ class WP_REST_Site_Health_Controller extends WP_REST_Controller { return $this->site_health->get_test_loopback_requests(); } + /** + * Checks that the site's frontend can be accessed over HTTPS. + * + * @since 5.7.0 + * + * @return array + */ + public function test_https_status() { + $this->load_admin_textdomain(); + return $this->site_health->get_test_https_status(); + } + /** * Checks that the authorization header is valid. * diff --git a/tests/phpunit/tests/https-detection.php b/tests/phpunit/tests/https-detection.php index b02cbb5fb5..b51284b6fc 100644 --- a/tests/phpunit/tests/https-detection.php +++ b/tests/phpunit/tests/https-detection.php @@ -141,88 +141,88 @@ class Tests_HTTPS_Detection extends WP_UnitTestCase { /** * @ticket 47577 */ - public function test_wp_is_owned_html_output_via_rsd_link() { + public function test_wp_is_local_html_output_via_rsd_link() { // HTML includes RSD link. $head_tag = get_echo( 'rsd_link' ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML includes modified RSD link but same URL. $head_tag = str_replace( ' />', '>', get_echo( 'rsd_link' ) ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML does not include RSD link. $html = $this->get_sample_html_string(); - $this->assertFalse( wp_is_owned_html_output( $html ) ); + $this->assertFalse( wp_is_local_html_output( $html ) ); } /** * @ticket 47577 */ - public function test_wp_is_owned_html_output_via_wlwmanifest_link() { + public function test_wp_is_local_html_output_via_wlwmanifest_link() { remove_action( 'wp_head', 'rsd_link' ); // HTML includes WLW manifest link. $head_tag = get_echo( 'wlwmanifest_link' ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML includes modified WLW manifest link but same URL. $head_tag = str_replace( ' />', '>', get_echo( 'wlwmanifest_link' ) ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML includes WLW manifest link with alternative URL scheme. $head_tag = get_echo( 'wlwmanifest_link' ); $head_tag = false !== strpos( $head_tag, 'https://' ) ? str_replace( 'https://', 'http://', $head_tag ) : str_replace( 'http://', 'https://', $head_tag ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML does not include WLW manifest link. $html = $this->get_sample_html_string(); - $this->assertFalse( wp_is_owned_html_output( $html ) ); + $this->assertFalse( wp_is_local_html_output( $html ) ); } /** * @ticket 47577 */ - public function test_wp_is_owned_html_output_via_rest_link() { + public function test_wp_is_local_html_output_via_rest_link() { remove_action( 'wp_head', 'rsd_link' ); remove_action( 'wp_head', 'wlwmanifest_link' ); // HTML includes REST API link. $head_tag = get_echo( 'rest_output_link_wp_head' ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML includes modified REST API link but same URL. $head_tag = str_replace( ' />', '>', get_echo( 'rest_output_link_wp_head' ) ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML includes REST API link with alternative URL scheme. $head_tag = get_echo( 'rest_output_link_wp_head' ); $head_tag = false !== strpos( $head_tag, 'https://' ) ? str_replace( 'https://', 'http://', $head_tag ) : str_replace( 'http://', 'https://', $head_tag ); $html = $this->get_sample_html_string( $head_tag ); - $this->assertTrue( wp_is_owned_html_output( $html ) ); + $this->assertTrue( wp_is_local_html_output( $html ) ); // HTML does not include REST API link. $html = $this->get_sample_html_string(); - $this->assertFalse( wp_is_owned_html_output( $html ) ); + $this->assertFalse( wp_is_local_html_output( $html ) ); } /** * @ticket 47577 */ - public function test_wp_is_owned_html_output_cannot_determine() { + public function test_wp_is_local_html_output_cannot_determine() { remove_action( 'wp_head', 'rsd_link' ); remove_action( 'wp_head', 'wlwmanifest_link' ); remove_action( 'wp_head', 'rest_output_link_wp_head' ); // The HTML here doesn't matter because all hooks are removed. $html = $this->get_sample_html_string(); - $this->assertNull( wp_is_owned_html_output( $html ) ); + $this->assertNull( wp_is_local_html_output( $html ) ); } public function record_request_url( $preempt, $parsed_args, $url ) { diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 7e3d0279ed..69e265047b 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -137,6 +137,7 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp-site-health/v1', '/wp-site-health/v1/tests/background-updates', '/wp-site-health/v1/tests/loopback-requests', + '/wp-site-health/v1/tests/https-status', '/wp-site-health/v1/tests/dotorg-communication', '/wp-site-health/v1/tests/authorization-header', '/wp-site-health/v1/directory-sizes', diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 40f3ef6edd..bbfafc5af9 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -6191,6 +6191,27 @@ mockedApiResponse.Schema = { ] } }, + "/wp-site-health/v1/tests/https-status": { + "namespace": "wp-site-health/v1", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": [] + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp-site-health/v1/tests/https-status" + } + ] + } + }, "/wp-site-health/v1/tests/dotorg-communication": { "namespace": "wp-site-health/v1", "methods": [