From 0187bbdd7e4554b8c95c22fc9801cb73bd9086f1 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Tue, 27 Oct 2020 18:30:03 +0000 Subject: [PATCH] Site Health, App Passwords: Test if the Authorization header is populated correctly. App Passwords rely on the Authorization header to transport the Basic Auth credentials. For Apache web servers, WordPress automatically includes a RewriteRule to populate the value for servers running in CGI or FastCGI that wouldn't ordinarily populate the value. This tests if the header is being filled with the expected values. For Apache users, we direct the user to visit the Permalinks settings to flush their permalinks. For all other users, we direct them to a help document on developer.wordpress.org. Props Clorith, marybaum, TimothyBlynJacobs. Fixes #51638. git-svn-id: https://develop.svn.wordpress.org/trunk@49334 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/admin/site-health.js | 3 +- .../includes/class-wp-site-health.php | 70 +++++++++++++++++++ .../class-wp-rest-site-health-controller.php | 30 ++++++++ .../tests/rest-api/rest-schema-setup.php | 1 + 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index 54497464cd..3314f41e1e 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -212,7 +212,8 @@ jQuery( document ).ready( function( $ ) { if ( 'undefined' !== typeof( this.has_rest ) && this.has_rest ) { wp.apiRequest( { - url: this.test + url: this.test, + headers: this.headers } ) .done( function( response ) { /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 2e556f40a8..19491d2eed 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -135,6 +135,7 @@ class WP_Site_Health { 'test' => $test['test'], 'has_rest' => ( isset( $test['has_rest'] ) ? $test['has_rest'] : false ), 'completed' => false, + 'headers' => isset( $test['headers'] ) ? $test['headers'] : array(), ); } } @@ -2078,6 +2079,62 @@ class WP_Site_Health { return $result; } + /** + * Tests if the Authorization header has the expected values. + * + * @since 5.6.0 + * + * @return array + */ + public function get_test_authorization_header() { + $result = array( + 'label' => __( 'The Authorization header is working as expected.' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Security' ), + 'color' => 'blue', + ), + 'description' => sprintf( + '

%s

', + __( 'The Authorization header comes from the third-party applications you approve. Without it, those apps cannot connect to your site.' ) + ), + 'actions' => '', + 'test' => 'authorization_header', + ); + + if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) { + $result['label'] = __( 'The authorization header is missing.' ); + } elseif ( 'user' !== $_SERVER['PHP_AUTH_USER'] || 'pwd' !== $_SERVER['PHP_AUTH_PW'] ) { + $result['label'] = __( 'The authorization header is invalid.' ); + } else { + return $result; + } + + $result['status'] = 'recommended'; + + if ( ! function_exists( 'got_mod_rewrite' ) ) { + require_once ABSPATH . 'wp-admin/includes/misc.php'; + } + + if ( got_mod_rewrite() ) { + $result['actions'] .= sprintf( + '

%s

', + esc_url( admin_url( 'options-permalink.php' ) ), + __( 'Flush permalinks' ) + ); + } else { + $result['actions'] .= sprintf( + '

%s %s

', + 'https://developer.wordpress.org/rest-api/frequently-asked-questions/#why-is-authentication-not-working', + __( 'Learn how to configure the Authorization header.' ), + /* translators: Accessibility text. */ + __( '(opens in a new tab)' ) + ); + } + + return $result; + } + /** * Return a set of tests that belong to the site status page. * @@ -2177,6 +2234,13 @@ class WP_Site_Health { 'has_rest' => true, 'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_loopback_requests' ), ), + 'authorization_header' => array( + 'label' => __( 'Authorization header' ), + 'test' => rest_url( 'wp-site-health/v1/tests/authorization-header' ), + 'has_rest' => true, + 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( 'user:pwd' ) ), + 'skip_cron' => true, + ), ), ); @@ -2203,6 +2267,7 @@ class WP_Site_Health { * * @since 5.2.0 * @since 5.6.0 Added the `async_direct_test` array key. + * Added the `skip_cron` array key. * * @param array $test_type { * An associative array, where the `$test_type` is either `direct` or @@ -2217,6 +2282,7 @@ class WP_Site_Health { * @type mixed $test A callable to perform a direct test, or a string AJAX action * to be called to perform an async test. * @type boolean $has_rest Optional. Denote if `$test` has a REST API endpoint. + * @type boolean $skip_cron Whether to skip this test when running as cron. * @type callable $async_direct_test A manner of directly calling the test marked as asynchronous, * as the scheduled event can not authenticate, and endpoints * may require authentication. @@ -2557,6 +2623,10 @@ class WP_Site_Health { } foreach ( $tests['async'] as $test ) { + if ( ! empty( $test['skip_cron'] ) ) { + continue; + } + // Local endpoints may require authentication, so asynchronous tests can pass a direct test runner as well. if ( ! empty( $test['async_direct_test'] ) && is_callable( $test['async_direct_test'] ) ) { // This test is callable, do so and continue to the next asynchronous check. 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 11c5151799..6dbc239ec0 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 @@ -104,6 +104,25 @@ class WP_REST_Site_Health_Controller extends WP_REST_Controller { ) ); + register_rest_route( + $this->namespace, + sprintf( + '/%s/%s', + $this->rest_base, + 'authorization-header' + ), + array( + array( + 'methods' => 'GET', + 'callback' => array( $this, 'test_authorization_header' ), + 'permission_callback' => function () { + return $this->validate_request_permission( 'authorization_header' ); + }, + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( $this->namespace, sprintf( @@ -177,6 +196,17 @@ class WP_REST_Site_Health_Controller extends WP_REST_Controller { return $this->site_health->get_test_loopback_requests(); } + /** + * Checks that the authorization header is valid. + * + * @since 5.6.0 + * + * @return array + */ + public function test_authorization_header() { + return $this->site_health->get_test_authorization_header(); + } + /** * Gets the current directory sizes for this install. * diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 086edeab9b..e9f88f6951 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -136,6 +136,7 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp-site-health/v1/tests/background-updates', '/wp-site-health/v1/tests/loopback-requests', '/wp-site-health/v1/tests/dotorg-communication', + '/wp-site-health/v1/tests/authorization-header', '/wp-site-health/v1/directory-sizes', );