balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-site-health.php (91217B)


      1 <?php
      2 /**
      3  * Class for looking up a site's health based on a user's WordPress environment.
      4  *
      5  * @package WordPress
      6  * @subpackage Site_Health
      7  * @since 5.2.0
      8  */
      9 
     10 class WP_Site_Health {
     11 	private static $instance = null;
     12 
     13 	private $mysql_min_version_check;
     14 	private $mysql_rec_version_check;
     15 
     16 	public $is_mariadb                           = false;
     17 	private $mysql_server_version                = '';
     18 	private $health_check_mysql_required_version = '5.5';
     19 	private $health_check_mysql_rec_version      = '';
     20 
     21 	public $php_memory_limit;
     22 
     23 	public $schedules;
     24 	public $crons;
     25 	public $last_missed_cron     = null;
     26 	public $last_late_cron       = null;
     27 	private $timeout_missed_cron = null;
     28 	private $timeout_late_cron   = null;
     29 
     30 	/**
     31 	 * WP_Site_Health constructor.
     32 	 *
     33 	 * @since 5.2.0
     34 	 */
     35 	public function __construct() {
     36 		$this->maybe_create_scheduled_event();
     37 
     38 		// Save memory limit before it's affected by wp_raise_memory_limit( 'admin' ).
     39 		$this->php_memory_limit = ini_get( 'memory_limit' );
     40 
     41 		$this->timeout_late_cron   = 0;
     42 		$this->timeout_missed_cron = - 5 * MINUTE_IN_SECONDS;
     43 
     44 		if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
     45 			$this->timeout_late_cron   = - 15 * MINUTE_IN_SECONDS;
     46 			$this->timeout_missed_cron = - 1 * HOUR_IN_SECONDS;
     47 		}
     48 
     49 		add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
     50 
     51 		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
     52 		add_action( 'wp_site_health_scheduled_check', array( $this, 'wp_cron_scheduled_check' ) );
     53 
     54 		add_action( 'site_health_tab_content', array( $this, 'show_site_health_tab' ) );
     55 	}
     56 
     57 	/**
     58 	 * Output the content of a tab in the Site Health screen.
     59 	 *
     60 	 * @since 5.8.0
     61 	 *
     62 	 * @param string $tab Slug of the current tab being displayed.
     63 	 */
     64 	public function show_site_health_tab( $tab ) {
     65 		if ( 'debug' === $tab ) {
     66 			require_once ABSPATH . '/wp-admin/site-health-info.php';
     67 		}
     68 	}
     69 
     70 	/**
     71 	 * Return an instance of the WP_Site_Health class, or create one if none exist yet.
     72 	 *
     73 	 * @since 5.4.0
     74 	 *
     75 	 * @return WP_Site_Health|null
     76 	 */
     77 	public static function get_instance() {
     78 		if ( null === self::$instance ) {
     79 			self::$instance = new WP_Site_Health();
     80 		}
     81 
     82 		return self::$instance;
     83 	}
     84 
     85 	/**
     86 	 * Enqueues the site health scripts.
     87 	 *
     88 	 * @since 5.2.0
     89 	 */
     90 	public function enqueue_scripts() {
     91 		$screen = get_current_screen();
     92 		if ( 'site-health' !== $screen->id && 'dashboard' !== $screen->id ) {
     93 			return;
     94 		}
     95 
     96 		$health_check_js_variables = array(
     97 			'screen'      => $screen->id,
     98 			'nonce'       => array(
     99 				'site_status'        => wp_create_nonce( 'health-check-site-status' ),
    100 				'site_status_result' => wp_create_nonce( 'health-check-site-status-result' ),
    101 			),
    102 			'site_status' => array(
    103 				'direct' => array(),
    104 				'async'  => array(),
    105 				'issues' => array(
    106 					'good'        => 0,
    107 					'recommended' => 0,
    108 					'critical'    => 0,
    109 				),
    110 			),
    111 		);
    112 
    113 		$issue_counts = get_transient( 'health-check-site-status-result' );
    114 
    115 		if ( false !== $issue_counts ) {
    116 			$issue_counts = json_decode( $issue_counts );
    117 
    118 			$health_check_js_variables['site_status']['issues'] = $issue_counts;
    119 		}
    120 
    121 		if ( 'site-health' === $screen->id && ( ! isset( $_GET['tab'] ) || empty( $_GET['tab'] ) ) ) {
    122 			$tests = WP_Site_Health::get_tests();
    123 
    124 			// Don't run https test on development environments.
    125 			if ( $this->is_development_environment() ) {
    126 				unset( $tests['async']['https_status'] );
    127 			}
    128 
    129 			foreach ( $tests['direct'] as $test ) {
    130 				if ( is_string( $test['test'] ) ) {
    131 					$test_function = sprintf(
    132 						'get_test_%s',
    133 						$test['test']
    134 					);
    135 
    136 					if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
    137 						$health_check_js_variables['site_status']['direct'][] = $this->perform_test( array( $this, $test_function ) );
    138 						continue;
    139 					}
    140 				}
    141 
    142 				if ( is_callable( $test['test'] ) ) {
    143 					$health_check_js_variables['site_status']['direct'][] = $this->perform_test( $test['test'] );
    144 				}
    145 			}
    146 
    147 			foreach ( $tests['async'] as $test ) {
    148 				if ( is_string( $test['test'] ) ) {
    149 					$health_check_js_variables['site_status']['async'][] = array(
    150 						'test'      => $test['test'],
    151 						'has_rest'  => ( isset( $test['has_rest'] ) ? $test['has_rest'] : false ),
    152 						'completed' => false,
    153 						'headers'   => isset( $test['headers'] ) ? $test['headers'] : array(),
    154 					);
    155 				}
    156 			}
    157 		}
    158 
    159 		wp_localize_script( 'site-health', 'SiteHealth', $health_check_js_variables );
    160 	}
    161 
    162 	/**
    163 	 * Run a Site Health test directly.
    164 	 *
    165 	 * @since 5.4.0
    166 	 *
    167 	 * @param callable $callback
    168 	 * @return mixed|void
    169 	 */
    170 	private function perform_test( $callback ) {
    171 		/**
    172 		 * Filters the output of a finished Site Health test.
    173 		 *
    174 		 * @since 5.3.0
    175 		 *
    176 		 * @param array $test_result {
    177 		 *     An associative array of test result data.
    178 		 *
    179 		 *     @type string $label       A label describing the test, and is used as a header in the output.
    180 		 *     @type string $status      The status of the test, which can be a value of `good`, `recommended` or `critical`.
    181 		 *     @type array  $badge {
    182 		 *         Tests are put into categories which have an associated badge shown, these can be modified and assigned here.
    183 		 *
    184 		 *         @type string $label The test label, for example `Performance`.
    185 		 *         @type string $color Default `blue`. A string representing a color to use for the label.
    186 		 *     }
    187 		 *     @type string $description A more descriptive explanation of what the test looks for, and why it is important for the end user.
    188 		 *     @type string $actions     An action to direct the user to where they can resolve the issue, if one exists.
    189 		 *     @type string $test        The name of the test being ran, used as a reference point.
    190 		 * }
    191 		 */
    192 		return apply_filters( 'site_status_test_result', call_user_func( $callback ) );
    193 	}
    194 
    195 	/**
    196 	 * Run the SQL version checks.
    197 	 *
    198 	 * These values are used in later tests, but the part of preparing them is more easily managed
    199 	 * early in the class for ease of access and discovery.
    200 	 *
    201 	 * @since 5.2.0
    202 	 *
    203 	 * @global wpdb $wpdb WordPress database abstraction object.
    204 	 */
    205 	private function prepare_sql_data() {
    206 		global $wpdb;
    207 
    208 		if ( $wpdb->use_mysqli ) {
    209 			// phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_server_info
    210 			$mysql_server_type = mysqli_get_server_info( $wpdb->dbh );
    211 		} else {
    212 			// phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_server_info,PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
    213 			$mysql_server_type = mysql_get_server_info( $wpdb->dbh );
    214 		}
    215 
    216 		$this->mysql_server_version = $wpdb->get_var( 'SELECT VERSION()' );
    217 
    218 		$this->health_check_mysql_rec_version = '5.6';
    219 
    220 		if ( stristr( $mysql_server_type, 'mariadb' ) ) {
    221 			$this->is_mariadb                     = true;
    222 			$this->health_check_mysql_rec_version = '10.0';
    223 		}
    224 
    225 		$this->mysql_min_version_check = version_compare( '5.5', $this->mysql_server_version, '<=' );
    226 		$this->mysql_rec_version_check = version_compare( $this->health_check_mysql_rec_version, $this->mysql_server_version, '<=' );
    227 	}
    228 
    229 	/**
    230 	 * Test if `wp_version_check` is blocked.
    231 	 *
    232 	 * It's possible to block updates with the `wp_version_check` filter, but this can't be checked
    233 	 * during an Ajax call, as the filter is never introduced then.
    234 	 *
    235 	 * This filter overrides a standard page request if it's made by an admin through the Ajax call
    236 	 * with the right query argument to check for this.
    237 	 *
    238 	 * @since 5.2.0
    239 	 */
    240 	public function check_wp_version_check_exists() {
    241 		if ( ! is_admin() || ! is_user_logged_in() || ! current_user_can( 'update_core' ) || ! isset( $_GET['health-check-test-wp_version_check'] ) ) {
    242 			return;
    243 		}
    244 
    245 		echo ( has_filter( 'wp_version_check', 'wp_version_check' ) ? 'yes' : 'no' );
    246 
    247 		die();
    248 	}
    249 
    250 	/**
    251 	 * Tests for WordPress version and outputs it.
    252 	 *
    253 	 * Gives various results depending on what kind of updates are available, if any, to encourage
    254 	 * the user to install security updates as a priority.
    255 	 *
    256 	 * @since 5.2.0
    257 	 *
    258 	 * @return array The test result.
    259 	 */
    260 	public function get_test_wordpress_version() {
    261 		$result = array(
    262 			'label'       => '',
    263 			'status'      => '',
    264 			'badge'       => array(
    265 				'label' => __( 'Performance' ),
    266 				'color' => 'blue',
    267 			),
    268 			'description' => '',
    269 			'actions'     => '',
    270 			'test'        => 'wordpress_version',
    271 		);
    272 
    273 		$core_current_version = get_bloginfo( 'version' );
    274 		$core_updates         = get_core_updates();
    275 
    276 		if ( ! is_array( $core_updates ) ) {
    277 			$result['status'] = 'recommended';
    278 
    279 			$result['label'] = sprintf(
    280 				/* translators: %s: Your current version of WordPress. */
    281 				__( 'WordPress version %s' ),
    282 				$core_current_version
    283 			);
    284 
    285 			$result['description'] = sprintf(
    286 				'<p>%s</p>',
    287 				__( 'We were unable to check if any new versions of WordPress are available.' )
    288 			);
    289 
    290 			$result['actions'] = sprintf(
    291 				'<a href="%s">%s</a>',
    292 				esc_url( admin_url( 'update-core.php?force-check=1' ) ),
    293 				__( 'Check for updates manually' )
    294 			);
    295 		} else {
    296 			foreach ( $core_updates as $core => $update ) {
    297 				if ( 'upgrade' === $update->response ) {
    298 					$current_version = explode( '.', $core_current_version );
    299 					$new_version     = explode( '.', $update->version );
    300 
    301 					$current_major = $current_version[0] . '.' . $current_version[1];
    302 					$new_major     = $new_version[0] . '.' . $new_version[1];
    303 
    304 					$result['label'] = sprintf(
    305 						/* translators: %s: The latest version of WordPress available. */
    306 						__( 'WordPress update available (%s)' ),
    307 						$update->version
    308 					);
    309 
    310 					$result['actions'] = sprintf(
    311 						'<a href="%s">%s</a>',
    312 						esc_url( admin_url( 'update-core.php' ) ),
    313 						__( 'Install the latest version of WordPress' )
    314 					);
    315 
    316 					if ( $current_major !== $new_major ) {
    317 						// This is a major version mismatch.
    318 						$result['status']      = 'recommended';
    319 						$result['description'] = sprintf(
    320 							'<p>%s</p>',
    321 							__( 'A new version of WordPress is available.' )
    322 						);
    323 					} else {
    324 						// This is a minor version, sometimes considered more critical.
    325 						$result['status']         = 'critical';
    326 						$result['badge']['label'] = __( 'Security' );
    327 						$result['description']    = sprintf(
    328 							'<p>%s</p>',
    329 							__( 'A new minor update is available for your site. Because minor updates often address security, it&#8217;s important to install them.' )
    330 						);
    331 					}
    332 				} else {
    333 					$result['status'] = 'good';
    334 					$result['label']  = sprintf(
    335 						/* translators: %s: The current version of WordPress installed on this site. */
    336 						__( 'Your version of WordPress (%s) is up to date' ),
    337 						$core_current_version
    338 					);
    339 
    340 					$result['description'] = sprintf(
    341 						'<p>%s</p>',
    342 						__( 'You are currently running the latest version of WordPress available, keep it up!' )
    343 					);
    344 				}
    345 			}
    346 		}
    347 
    348 		return $result;
    349 	}
    350 
    351 	/**
    352 	 * Test if plugins are outdated, or unnecessary.
    353 	 *
    354 	 * The tests checks if your plugins are up to date, and encourages you to remove any
    355 	 * that are not in use.
    356 	 *
    357 	 * @since 5.2.0
    358 	 *
    359 	 * @return array The test result.
    360 	 */
    361 	public function get_test_plugin_version() {
    362 		$result = array(
    363 			'label'       => __( 'Your plugins are all up to date' ),
    364 			'status'      => 'good',
    365 			'badge'       => array(
    366 				'label' => __( 'Security' ),
    367 				'color' => 'blue',
    368 			),
    369 			'description' => sprintf(
    370 				'<p>%s</p>',
    371 				__( 'Plugins extend your site&#8217;s functionality with things like contact forms, ecommerce and much more. That means they have deep access to your site, so it&#8217;s vital to keep them up to date.' )
    372 			),
    373 			'actions'     => sprintf(
    374 				'<p><a href="%s">%s</a></p>',
    375 				esc_url( admin_url( 'plugins.php' ) ),
    376 				__( 'Manage your plugins' )
    377 			),
    378 			'test'        => 'plugin_version',
    379 		);
    380 
    381 		$plugins        = get_plugins();
    382 		$plugin_updates = get_plugin_updates();
    383 
    384 		$plugins_have_updates = false;
    385 		$plugins_active       = 0;
    386 		$plugins_total        = 0;
    387 		$plugins_need_update  = 0;
    388 
    389 		// Loop over the available plugins and check their versions and active state.
    390 		foreach ( $plugins as $plugin_path => $plugin ) {
    391 			$plugins_total++;
    392 
    393 			if ( is_plugin_active( $plugin_path ) ) {
    394 				$plugins_active++;
    395 			}
    396 
    397 			$plugin_version = $plugin['Version'];
    398 
    399 			if ( array_key_exists( $plugin_path, $plugin_updates ) ) {
    400 				$plugins_need_update++;
    401 				$plugins_have_updates = true;
    402 			}
    403 		}
    404 
    405 		// Add a notice if there are outdated plugins.
    406 		if ( $plugins_need_update > 0 ) {
    407 			$result['status'] = 'critical';
    408 
    409 			$result['label'] = __( 'You have plugins waiting to be updated' );
    410 
    411 			$result['description'] .= sprintf(
    412 				'<p>%s</p>',
    413 				sprintf(
    414 					/* translators: %d: The number of outdated plugins. */
    415 					_n(
    416 						'Your site has %d plugin waiting to be updated.',
    417 						'Your site has %d plugins waiting to be updated.',
    418 						$plugins_need_update
    419 					),
    420 					$plugins_need_update
    421 				)
    422 			);
    423 
    424 			$result['actions'] .= sprintf(
    425 				'<p><a href="%s">%s</a></p>',
    426 				esc_url( network_admin_url( 'plugins.php?plugin_status=upgrade' ) ),
    427 				__( 'Update your plugins' )
    428 			);
    429 		} else {
    430 			if ( 1 === $plugins_active ) {
    431 				$result['description'] .= sprintf(
    432 					'<p>%s</p>',
    433 					__( 'Your site has 1 active plugin, and it is up to date.' )
    434 				);
    435 			} else {
    436 				$result['description'] .= sprintf(
    437 					'<p>%s</p>',
    438 					sprintf(
    439 						/* translators: %d: The number of active plugins. */
    440 						_n(
    441 							'Your site has %d active plugin, and it is up to date.',
    442 							'Your site has %d active plugins, and they are all up to date.',
    443 							$plugins_active
    444 						),
    445 						$plugins_active
    446 					)
    447 				);
    448 			}
    449 		}
    450 
    451 		// Check if there are inactive plugins.
    452 		if ( $plugins_total > $plugins_active && ! is_multisite() ) {
    453 			$unused_plugins = $plugins_total - $plugins_active;
    454 
    455 			$result['status'] = 'recommended';
    456 
    457 			$result['label'] = __( 'You should remove inactive plugins' );
    458 
    459 			$result['description'] .= sprintf(
    460 				'<p>%s %s</p>',
    461 				sprintf(
    462 					/* translators: %d: The number of inactive plugins. */
    463 					_n(
    464 						'Your site has %d inactive plugin.',
    465 						'Your site has %d inactive plugins.',
    466 						$unused_plugins
    467 					),
    468 					$unused_plugins
    469 				),
    470 				__( 'Inactive plugins are tempting targets for attackers. If you&#8217;re not going to use a plugin, we recommend you remove it.' )
    471 			);
    472 
    473 			$result['actions'] .= sprintf(
    474 				'<p><a href="%s">%s</a></p>',
    475 				esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ),
    476 				__( 'Manage inactive plugins' )
    477 			);
    478 		}
    479 
    480 		return $result;
    481 	}
    482 
    483 	/**
    484 	 * Test if themes are outdated, or unnecessary.
    485 	 *
    486 	 * Сhecks if your site has a default theme (to fall back on if there is a need),
    487 	 * if your themes are up to date and, finally, encourages you to remove any themes
    488 	 * that are not needed.
    489 	 *
    490 	 * @since 5.2.0
    491 	 *
    492 	 * @return array The test results.
    493 	 */
    494 	public function get_test_theme_version() {
    495 		$result = array(
    496 			'label'       => __( 'Your themes are all up to date' ),
    497 			'status'      => 'good',
    498 			'badge'       => array(
    499 				'label' => __( 'Security' ),
    500 				'color' => 'blue',
    501 			),
    502 			'description' => sprintf(
    503 				'<p>%s</p>',
    504 				__( 'Themes add your site&#8217;s look and feel. It&#8217;s important to keep them up to date, to stay consistent with your brand and keep your site secure.' )
    505 			),
    506 			'actions'     => sprintf(
    507 				'<p><a href="%s">%s</a></p>',
    508 				esc_url( admin_url( 'themes.php' ) ),
    509 				__( 'Manage your themes' )
    510 			),
    511 			'test'        => 'theme_version',
    512 		);
    513 
    514 		$theme_updates = get_theme_updates();
    515 
    516 		$themes_total        = 0;
    517 		$themes_need_updates = 0;
    518 		$themes_inactive     = 0;
    519 
    520 		// This value is changed during processing to determine how many themes are considered a reasonable amount.
    521 		$allowed_theme_count = 1;
    522 
    523 		$has_default_theme   = false;
    524 		$has_unused_themes   = false;
    525 		$show_unused_themes  = true;
    526 		$using_default_theme = false;
    527 
    528 		// Populate a list of all themes available in the install.
    529 		$all_themes   = wp_get_themes();
    530 		$active_theme = wp_get_theme();
    531 
    532 		// If WP_DEFAULT_THEME doesn't exist, fall back to the latest core default theme.
    533 		$default_theme = wp_get_theme( WP_DEFAULT_THEME );
    534 		if ( ! $default_theme->exists() ) {
    535 			$default_theme = WP_Theme::get_core_default_theme();
    536 		}
    537 
    538 		if ( $default_theme ) {
    539 			$has_default_theme = true;
    540 
    541 			if (
    542 				$active_theme->get_stylesheet() === $default_theme->get_stylesheet()
    543 			||
    544 				is_child_theme() && $active_theme->get_template() === $default_theme->get_template()
    545 			) {
    546 				$using_default_theme = true;
    547 			}
    548 		}
    549 
    550 		foreach ( $all_themes as $theme_slug => $theme ) {
    551 			$themes_total++;
    552 
    553 			if ( array_key_exists( $theme_slug, $theme_updates ) ) {
    554 				$themes_need_updates++;
    555 			}
    556 		}
    557 
    558 		// If this is a child theme, increase the allowed theme count by one, to account for the parent.
    559 		if ( is_child_theme() ) {
    560 			$allowed_theme_count++;
    561 		}
    562 
    563 		// If there's a default theme installed and not in use, we count that as allowed as well.
    564 		if ( $has_default_theme && ! $using_default_theme ) {
    565 			$allowed_theme_count++;
    566 		}
    567 
    568 		if ( $themes_total > $allowed_theme_count ) {
    569 			$has_unused_themes = true;
    570 			$themes_inactive   = ( $themes_total - $allowed_theme_count );
    571 		}
    572 
    573 		// Check if any themes need to be updated.
    574 		if ( $themes_need_updates > 0 ) {
    575 			$result['status'] = 'critical';
    576 
    577 			$result['label'] = __( 'You have themes waiting to be updated' );
    578 
    579 			$result['description'] .= sprintf(
    580 				'<p>%s</p>',
    581 				sprintf(
    582 					/* translators: %d: The number of outdated themes. */
    583 					_n(
    584 						'Your site has %d theme waiting to be updated.',
    585 						'Your site has %d themes waiting to be updated.',
    586 						$themes_need_updates
    587 					),
    588 					$themes_need_updates
    589 				)
    590 			);
    591 		} else {
    592 			// Give positive feedback about the site being good about keeping things up to date.
    593 			if ( 1 === $themes_total ) {
    594 				$result['description'] .= sprintf(
    595 					'<p>%s</p>',
    596 					__( 'Your site has 1 installed theme, and it is up to date.' )
    597 				);
    598 			} else {
    599 				$result['description'] .= sprintf(
    600 					'<p>%s</p>',
    601 					sprintf(
    602 						/* translators: %d: The number of themes. */
    603 						_n(
    604 							'Your site has %d installed theme, and it is up to date.',
    605 							'Your site has %d installed themes, and they are all up to date.',
    606 							$themes_total
    607 						),
    608 						$themes_total
    609 					)
    610 				);
    611 			}
    612 		}
    613 
    614 		if ( $has_unused_themes && $show_unused_themes && ! is_multisite() ) {
    615 
    616 			// This is a child theme, so we want to be a bit more explicit in our messages.
    617 			if ( $active_theme->parent() ) {
    618 				// Recommend removing inactive themes, except a default theme, your current one, and the parent theme.
    619 				$result['status'] = 'recommended';
    620 
    621 				$result['label'] = __( 'You should remove inactive themes' );
    622 
    623 				if ( $using_default_theme ) {
    624 					$result['description'] .= sprintf(
    625 						'<p>%s %s</p>',
    626 						sprintf(
    627 							/* translators: %d: The number of inactive themes. */
    628 							_n(
    629 								'Your site has %d inactive theme.',
    630 								'Your site has %d inactive themes.',
    631 								$themes_inactive
    632 							),
    633 							$themes_inactive
    634 						),
    635 						sprintf(
    636 							/* translators: 1: The currently active theme. 2: The active theme's parent theme. */
    637 							__( 'To enhance your site&#8217;s security, we recommend you remove any themes you&#8217;re not using. You should keep your current theme, %1$s, and %2$s, its parent theme.' ),
    638 							$active_theme->name,
    639 							$active_theme->parent()->name
    640 						)
    641 					);
    642 				} else {
    643 					$result['description'] .= sprintf(
    644 						'<p>%s %s</p>',
    645 						sprintf(
    646 							/* translators: %d: The number of inactive themes. */
    647 							_n(
    648 								'Your site has %d inactive theme.',
    649 								'Your site has %d inactive themes.',
    650 								$themes_inactive
    651 							),
    652 							$themes_inactive
    653 						),
    654 						sprintf(
    655 							/* translators: 1: The default theme for WordPress. 2: The currently active theme. 3: The active theme's parent theme. */
    656 							__( 'To enhance your site&#8217;s security, we recommend you remove any themes you&#8217;re not using. You should keep %1$s, the default WordPress theme, %2$s, your current theme, and %3$s, its parent theme.' ),
    657 							$default_theme ? $default_theme->name : WP_DEFAULT_THEME,
    658 							$active_theme->name,
    659 							$active_theme->parent()->name
    660 						)
    661 					);
    662 				}
    663 			} else {
    664 				// Recommend removing all inactive themes.
    665 				$result['status'] = 'recommended';
    666 
    667 				$result['label'] = __( 'You should remove inactive themes' );
    668 
    669 				if ( $using_default_theme ) {
    670 					$result['description'] .= sprintf(
    671 						'<p>%s %s</p>',
    672 						sprintf(
    673 							/* translators: 1: The amount of inactive themes. 2: The currently active theme. */
    674 							_n(
    675 								'Your site has %1$d inactive theme, other than %2$s, your active theme.',
    676 								'Your site has %1$d inactive themes, other than %2$s, your active theme.',
    677 								$themes_inactive
    678 							),
    679 							$themes_inactive,
    680 							$active_theme->name
    681 						),
    682 						__( 'We recommend removing any unused themes to enhance your site&#8217;s security.' )
    683 					);
    684 				} else {
    685 					$result['description'] .= sprintf(
    686 						'<p>%s %s</p>',
    687 						sprintf(
    688 							/* translators: 1: The amount of inactive themes. 2: The default theme for WordPress. 3: The currently active theme. */
    689 							_n(
    690 								'Your site has %1$d inactive theme, other than %2$s, the default WordPress theme, and %3$s, your active theme.',
    691 								'Your site has %1$d inactive themes, other than %2$s, the default WordPress theme, and %3$s, your active theme.',
    692 								$themes_inactive
    693 							),
    694 							$themes_inactive,
    695 							$default_theme ? $default_theme->name : WP_DEFAULT_THEME,
    696 							$active_theme->name
    697 						),
    698 						__( 'We recommend removing any unused themes to enhance your site&#8217;s security.' )
    699 					);
    700 				}
    701 			}
    702 		}
    703 
    704 		// If no default Twenty* theme exists.
    705 		if ( ! $has_default_theme ) {
    706 			$result['status'] = 'recommended';
    707 
    708 			$result['label'] = __( 'Have a default theme available' );
    709 
    710 			$result['description'] .= sprintf(
    711 				'<p>%s</p>',
    712 				__( 'Your site does not have any default theme. Default themes are used by WordPress automatically if anything is wrong with your chosen theme.' )
    713 			);
    714 		}
    715 
    716 		return $result;
    717 	}
    718 
    719 	/**
    720 	 * Test if the supplied PHP version is supported.
    721 	 *
    722 	 * @since 5.2.0
    723 	 *
    724 	 * @return array The test results.
    725 	 */
    726 	public function get_test_php_version() {
    727 		$response = wp_check_php_version();
    728 
    729 		$result = array(
    730 			'label'       => sprintf(
    731 				/* translators: %s: The current PHP version. */
    732 				__( 'Your site is running the current version of PHP (%s)' ),
    733 				PHP_VERSION
    734 			),
    735 			'status'      => 'good',
    736 			'badge'       => array(
    737 				'label' => __( 'Performance' ),
    738 				'color' => 'blue',
    739 			),
    740 			'description' => sprintf(
    741 				'<p>%s</p>',
    742 				sprintf(
    743 					/* translators: %s: The minimum recommended PHP version. */
    744 					__( 'PHP is the programming language used to build and maintain WordPress. Newer versions of PHP are created with increased performance in mind, so you may see a positive effect on your site&#8217;s performance. The minimum recommended version of PHP is %s.' ),
    745 					$response ? $response['recommended_version'] : ''
    746 				)
    747 			),
    748 			'actions'     => sprintf(
    749 				'<p><a href="%s" target="_blank" rel="noopener">%s <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
    750 				esc_url( wp_get_update_php_url() ),
    751 				__( 'Learn more about updating PHP' ),
    752 				/* translators: Accessibility text. */
    753 				__( '(opens in a new tab)' )
    754 			),
    755 			'test'        => 'php_version',
    756 		);
    757 
    758 		// PHP is up to date.
    759 		if ( ! $response || version_compare( PHP_VERSION, $response['recommended_version'], '>=' ) ) {
    760 			return $result;
    761 		}
    762 
    763 		// The PHP version is older than the recommended version, but still receiving active support.
    764 		if ( $response['is_supported'] ) {
    765 			$result['label'] = sprintf(
    766 				/* translators: %s: The server PHP version. */
    767 				__( 'Your site is running an older version of PHP (%s)' ),
    768 				PHP_VERSION
    769 			);
    770 			$result['status'] = 'recommended';
    771 
    772 			return $result;
    773 		}
    774 
    775 		// The PHP version is only receiving security fixes.
    776 		if ( $response['is_secure'] ) {
    777 			$result['label'] = sprintf(
    778 				/* translators: %s: The server PHP version. */
    779 				__( 'Your site is running an older version of PHP (%s), which should be updated' ),
    780 				PHP_VERSION
    781 			);
    782 			$result['status'] = 'recommended';
    783 
    784 			return $result;
    785 		}
    786 
    787 		// Anything no longer secure must be updated.
    788 		$result['label'] = sprintf(
    789 			/* translators: %s: The server PHP version. */
    790 			__( 'Your site is running an outdated version of PHP (%s), which requires an update' ),
    791 			PHP_VERSION
    792 		);
    793 		$result['status']         = 'critical';
    794 		$result['badge']['label'] = __( 'Security' );
    795 
    796 		return $result;
    797 	}
    798 
    799 	/**
    800 	 * Check if the passed extension or function are available.
    801 	 *
    802 	 * Make the check for available PHP modules into a simple boolean operator for a cleaner test runner.
    803 	 *
    804 	 * @since 5.2.0
    805 	 * @since 5.3.0 The `$constant` and `$class` parameters were added.
    806 	 *
    807 	 * @param string $extension Optional. The extension name to test. Default null.
    808 	 * @param string $function  Optional. The function name to test. Default null.
    809 	 * @param string $constant  Optional. The constant name to test for. Default null.
    810 	 * @param string $class     Optional. The class name to test for. Default null.
    811 	 * @return bool Whether or not the extension and function are available.
    812 	 */
    813 	private function test_php_extension_availability( $extension = null, $function = null, $constant = null, $class = null ) {
    814 		// If no extension or function is passed, claim to fail testing, as we have nothing to test against.
    815 		if ( ! $extension && ! $function && ! $constant && ! $class ) {
    816 			return false;
    817 		}
    818 
    819 		if ( $extension && ! extension_loaded( $extension ) ) {
    820 			return false;
    821 		}
    822 		if ( $function && ! function_exists( $function ) ) {
    823 			return false;
    824 		}
    825 		if ( $constant && ! defined( $constant ) ) {
    826 			return false;
    827 		}
    828 		if ( $class && ! class_exists( $class ) ) {
    829 			return false;
    830 		}
    831 
    832 		return true;
    833 	}
    834 
    835 	/**
    836 	 * Test if required PHP modules are installed on the host.
    837 	 *
    838 	 * This test builds on the recommendations made by the WordPress Hosting Team
    839 	 * as seen at https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions
    840 	 *
    841 	 * @since 5.2.0
    842 	 *
    843 	 * @return array
    844 	 */
    845 	public function get_test_php_extensions() {
    846 		$result = array(
    847 			'label'       => __( 'Required and recommended modules are installed' ),
    848 			'status'      => 'good',
    849 			'badge'       => array(
    850 				'label' => __( 'Performance' ),
    851 				'color' => 'blue',
    852 			),
    853 			'description' => sprintf(
    854 				'<p>%s</p><p>%s</p>',
    855 				__( 'PHP modules perform most of the tasks on the server that make your site run. Any changes to these must be made by your server administrator.' ),
    856 				sprintf(
    857 					/* translators: 1: Link to the hosting group page about recommended PHP modules. 2: Additional link attributes. 3: Accessibility text. */
    858 					__( 'The WordPress Hosting Team maintains a list of those modules, both recommended and required, in <a href="%1$s" %2$s>the team handbook%3$s</a>.' ),
    859 					/* translators: Localized team handbook, if one exists. */
    860 					esc_url( __( 'https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions' ) ),
    861 					'target="_blank" rel="noopener"',
    862 					sprintf(
    863 						' <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span>',
    864 						/* translators: Accessibility text. */
    865 						__( '(opens in a new tab)' )
    866 					)
    867 				)
    868 			),
    869 			'actions'     => '',
    870 			'test'        => 'php_extensions',
    871 		);
    872 
    873 		$modules = array(
    874 			'curl'      => array(
    875 				'function' => 'curl_version',
    876 				'required' => false,
    877 			),
    878 			'dom'       => array(
    879 				'class'    => 'DOMNode',
    880 				'required' => false,
    881 			),
    882 			'exif'      => array(
    883 				'function' => 'exif_read_data',
    884 				'required' => false,
    885 			),
    886 			'fileinfo'  => array(
    887 				'function' => 'finfo_file',
    888 				'required' => false,
    889 			),
    890 			'hash'      => array(
    891 				'function' => 'hash',
    892 				'required' => false,
    893 			),
    894 			'json'      => array(
    895 				'function' => 'json_last_error',
    896 				'required' => true,
    897 			),
    898 			'mbstring'  => array(
    899 				'function' => 'mb_check_encoding',
    900 				'required' => false,
    901 			),
    902 			'mysqli'    => array(
    903 				'function' => 'mysqli_connect',
    904 				'required' => false,
    905 			),
    906 			'libsodium' => array(
    907 				'constant'            => 'SODIUM_LIBRARY_VERSION',
    908 				'required'            => false,
    909 				'php_bundled_version' => '7.2.0',
    910 			),
    911 			'openssl'   => array(
    912 				'function' => 'openssl_encrypt',
    913 				'required' => false,
    914 			),
    915 			'pcre'      => array(
    916 				'function' => 'preg_match',
    917 				'required' => false,
    918 			),
    919 			'imagick'   => array(
    920 				'extension' => 'imagick',
    921 				'required'  => false,
    922 			),
    923 			'mod_xml'   => array(
    924 				'extension' => 'libxml',
    925 				'required'  => false,
    926 			),
    927 			'zip'       => array(
    928 				'class'    => 'ZipArchive',
    929 				'required' => false,
    930 			),
    931 			'filter'    => array(
    932 				'function' => 'filter_list',
    933 				'required' => false,
    934 			),
    935 			'gd'        => array(
    936 				'extension'    => 'gd',
    937 				'required'     => false,
    938 				'fallback_for' => 'imagick',
    939 			),
    940 			'iconv'     => array(
    941 				'function' => 'iconv',
    942 				'required' => false,
    943 			),
    944 			'mcrypt'    => array(
    945 				'extension'    => 'mcrypt',
    946 				'required'     => false,
    947 				'fallback_for' => 'libsodium',
    948 			),
    949 			'simplexml' => array(
    950 				'extension'    => 'simplexml',
    951 				'required'     => false,
    952 				'fallback_for' => 'mod_xml',
    953 			),
    954 			'xmlreader' => array(
    955 				'extension'    => 'xmlreader',
    956 				'required'     => false,
    957 				'fallback_for' => 'mod_xml',
    958 			),
    959 			'zlib'      => array(
    960 				'extension'    => 'zlib',
    961 				'required'     => false,
    962 				'fallback_for' => 'zip',
    963 			),
    964 		);
    965 
    966 		/**
    967 		 * An array representing all the modules we wish to test for.
    968 		 *
    969 		 * @since 5.2.0
    970 		 * @since 5.3.0 The `$constant` and `$class` parameters were added.
    971 		 *
    972 		 * @param array $modules {
    973 		 *     An associative array of modules to test for.
    974 		 *
    975 		 *     @type array ...$0 {
    976 		 *         An associative array of module properties used during testing.
    977 		 *         One of either `$function` or `$extension` must be provided, or they will fail by default.
    978 		 *
    979 		 *         @type string $function     Optional. A function name to test for the existence of.
    980 		 *         @type string $extension    Optional. An extension to check if is loaded in PHP.
    981 		 *         @type string $constant     Optional. A constant name to check for to verify an extension exists.
    982 		 *         @type string $class        Optional. A class name to check for to verify an extension exists.
    983 		 *         @type bool   $required     Is this a required feature or not.
    984 		 *         @type string $fallback_for Optional. The module this module replaces as a fallback.
    985 		 *     }
    986 		 * }
    987 		 */
    988 		$modules = apply_filters( 'site_status_test_php_modules', $modules );
    989 
    990 		$failures = array();
    991 
    992 		foreach ( $modules as $library => $module ) {
    993 			$extension  = ( isset( $module['extension'] ) ? $module['extension'] : null );
    994 			$function   = ( isset( $module['function'] ) ? $module['function'] : null );
    995 			$constant   = ( isset( $module['constant'] ) ? $module['constant'] : null );
    996 			$class_name = ( isset( $module['class'] ) ? $module['class'] : null );
    997 
    998 			// If this module is a fallback for another function, check if that other function passed.
    999 			if ( isset( $module['fallback_for'] ) ) {
   1000 				/*
   1001 				 * If that other function has a failure, mark this module as required for usual operations.
   1002 				 * If that other function hasn't failed, skip this test as it's only a fallback.
   1003 				 */
   1004 				if ( isset( $failures[ $module['fallback_for'] ] ) ) {
   1005 					$module['required'] = true;
   1006 				} else {
   1007 					continue;
   1008 				}
   1009 			}
   1010 
   1011 			if ( ! $this->test_php_extension_availability( $extension, $function, $constant, $class_name ) && ( ! isset( $module['php_bundled_version'] ) || version_compare( PHP_VERSION, $module['php_bundled_version'], '<' ) ) ) {
   1012 				if ( $module['required'] ) {
   1013 					$result['status'] = 'critical';
   1014 
   1015 					$class         = 'error';
   1016 					$screen_reader = __( 'Error' );
   1017 					$message       = sprintf(
   1018 						/* translators: %s: The module name. */
   1019 						__( 'The required module, %s, is not installed, or has been disabled.' ),
   1020 						$library
   1021 					);
   1022 				} else {
   1023 					$class         = 'warning';
   1024 					$screen_reader = __( 'Warning' );
   1025 					$message       = sprintf(
   1026 						/* translators: %s: The module name. */
   1027 						__( 'The optional module, %s, is not installed, or has been disabled.' ),
   1028 						$library
   1029 					);
   1030 				}
   1031 
   1032 				if ( ! $module['required'] && 'good' === $result['status'] ) {
   1033 					$result['status'] = 'recommended';
   1034 				}
   1035 
   1036 				$failures[ $library ] = "<span class='dashicons $class'><span class='screen-reader-text'>$screen_reader</span></span> $message";
   1037 			}
   1038 		}
   1039 
   1040 		if ( ! empty( $failures ) ) {
   1041 			$output = '<ul>';
   1042 
   1043 			foreach ( $failures as $failure ) {
   1044 				$output .= sprintf(
   1045 					'<li>%s</li>',
   1046 					$failure
   1047 				);
   1048 			}
   1049 
   1050 			$output .= '</ul>';
   1051 		}
   1052 
   1053 		if ( 'good' !== $result['status'] ) {
   1054 			if ( 'recommended' === $result['status'] ) {
   1055 				$result['label'] = __( 'One or more recommended modules are missing' );
   1056 			}
   1057 			if ( 'critical' === $result['status'] ) {
   1058 				$result['label'] = __( 'One or more required modules are missing' );
   1059 			}
   1060 
   1061 			$result['description'] .= $output;
   1062 		}
   1063 
   1064 		return $result;
   1065 	}
   1066 
   1067 	/**
   1068 	 * Test if the PHP default timezone is set to UTC.
   1069 	 *
   1070 	 * @since 5.3.1
   1071 	 *
   1072 	 * @return array The test results.
   1073 	 */
   1074 	public function get_test_php_default_timezone() {
   1075 		$result = array(
   1076 			'label'       => __( 'PHP default timezone is valid' ),
   1077 			'status'      => 'good',
   1078 			'badge'       => array(
   1079 				'label' => __( 'Performance' ),
   1080 				'color' => 'blue',
   1081 			),
   1082 			'description' => sprintf(
   1083 				'<p>%s</p>',
   1084 				__( 'PHP default timezone was configured by WordPress on loading. This is necessary for correct calculations of dates and times.' )
   1085 			),
   1086 			'actions'     => '',
   1087 			'test'        => 'php_default_timezone',
   1088 		);
   1089 
   1090 		if ( 'UTC' !== date_default_timezone_get() ) {
   1091 			$result['status'] = 'critical';
   1092 
   1093 			$result['label'] = __( 'PHP default timezone is invalid' );
   1094 
   1095 			$result['description'] = sprintf(
   1096 				'<p>%s</p>',
   1097 				sprintf(
   1098 					/* translators: %s: date_default_timezone_set() */
   1099 					__( 'PHP default timezone was changed after WordPress loading by a %s function call. This interferes with correct calculations of dates and times.' ),
   1100 					'<code>date_default_timezone_set()</code>'
   1101 				)
   1102 			);
   1103 		}
   1104 
   1105 		return $result;
   1106 	}
   1107 
   1108 	/**
   1109 	 * Test if there's an active PHP session that can affect loopback requests.
   1110 	 *
   1111 	 * @since 5.5.0
   1112 	 *
   1113 	 * @return array The test results.
   1114 	 */
   1115 	public function get_test_php_sessions() {
   1116 		$result = array(
   1117 			'label'       => __( 'No PHP sessions detected' ),
   1118 			'status'      => 'good',
   1119 			'badge'       => array(
   1120 				'label' => __( 'Performance' ),
   1121 				'color' => 'blue',
   1122 			),
   1123 			'description' => sprintf(
   1124 				'<p>%s</p>',
   1125 				sprintf(
   1126 					/* translators: 1: session_start(), 2: session_write_close() */
   1127 					__( 'PHP sessions created by a %1$s function call may interfere with REST API and loopback requests. An active session should be closed by %2$s before making any HTTP requests.' ),
   1128 					'<code>session_start()</code>',
   1129 					'<code>session_write_close()</code>'
   1130 				)
   1131 			),
   1132 			'test'        => 'php_sessions',
   1133 		);
   1134 
   1135 		if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) {
   1136 			$result['status'] = 'critical';
   1137 
   1138 			$result['label'] = __( 'An active PHP session was detected' );
   1139 
   1140 			$result['description'] = sprintf(
   1141 				'<p>%s</p>',
   1142 				sprintf(
   1143 					/* translators: 1: session_start(), 2: session_write_close() */
   1144 					__( 'A PHP session was created by a %1$s function call. This interferes with REST API and loopback requests. The session should be closed by %2$s before making any HTTP requests.' ),
   1145 					'<code>session_start()</code>',
   1146 					'<code>session_write_close()</code>'
   1147 				)
   1148 			);
   1149 		}
   1150 
   1151 		return $result;
   1152 	}
   1153 
   1154 	/**
   1155 	 * Test if the SQL server is up to date.
   1156 	 *
   1157 	 * @since 5.2.0
   1158 	 *
   1159 	 * @return array The test results.
   1160 	 */
   1161 	public function get_test_sql_server() {
   1162 		if ( ! $this->mysql_server_version ) {
   1163 			$this->prepare_sql_data();
   1164 		}
   1165 
   1166 		$result = array(
   1167 			'label'       => __( 'SQL server is up to date' ),
   1168 			'status'      => 'good',
   1169 			'badge'       => array(
   1170 				'label' => __( 'Performance' ),
   1171 				'color' => 'blue',
   1172 			),
   1173 			'description' => sprintf(
   1174 				'<p>%s</p>',
   1175 				__( 'The SQL server is a required piece of software for the database WordPress uses to store all your site&#8217;s content and settings.' )
   1176 			),
   1177 			'actions'     => sprintf(
   1178 				'<p><a href="%s" target="_blank" rel="noopener">%s <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   1179 				/* translators: Localized version of WordPress requirements if one exists. */
   1180 				esc_url( __( 'https://wordpress.org/about/requirements/' ) ),
   1181 				__( 'Learn more about what WordPress requires to run.' ),
   1182 				/* translators: Accessibility text. */
   1183 				__( '(opens in a new tab)' )
   1184 			),
   1185 			'test'        => 'sql_server',
   1186 		);
   1187 
   1188 		$db_dropin = file_exists( WP_CONTENT_DIR . '/db.php' );
   1189 
   1190 		if ( ! $this->mysql_rec_version_check ) {
   1191 			$result['status'] = 'recommended';
   1192 
   1193 			$result['label'] = __( 'Outdated SQL server' );
   1194 
   1195 			$result['description'] .= sprintf(
   1196 				'<p>%s</p>',
   1197 				sprintf(
   1198 					/* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server recommended version number. */
   1199 					__( 'For optimal performance and security reasons, we recommend running %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
   1200 					( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
   1201 					$this->health_check_mysql_rec_version
   1202 				)
   1203 			);
   1204 		}
   1205 
   1206 		if ( ! $this->mysql_min_version_check ) {
   1207 			$result['status'] = 'critical';
   1208 
   1209 			$result['label']          = __( 'Severely outdated SQL server' );
   1210 			$result['badge']['label'] = __( 'Security' );
   1211 
   1212 			$result['description'] .= sprintf(
   1213 				'<p>%s</p>',
   1214 				sprintf(
   1215 					/* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server minimum version number. */
   1216 					__( 'WordPress requires %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
   1217 					( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
   1218 					$this->health_check_mysql_required_version
   1219 				)
   1220 			);
   1221 		}
   1222 
   1223 		if ( $db_dropin ) {
   1224 			$result['description'] .= sprintf(
   1225 				'<p>%s</p>',
   1226 				wp_kses(
   1227 					sprintf(
   1228 						/* translators: 1: The name of the drop-in. 2: The name of the database engine. */
   1229 						__( 'You are using a %1$s drop-in which might mean that a %2$s database is not being used.' ),
   1230 						'<code>wp-content/db.php</code>',
   1231 						( $this->is_mariadb ? 'MariaDB' : 'MySQL' )
   1232 					),
   1233 					array(
   1234 						'code' => true,
   1235 					)
   1236 				)
   1237 			);
   1238 		}
   1239 
   1240 		return $result;
   1241 	}
   1242 
   1243 	/**
   1244 	 * Test if the database server is capable of using utf8mb4.
   1245 	 *
   1246 	 * @since 5.2.0
   1247 	 *
   1248 	 * @return array The test results.
   1249 	 */
   1250 	public function get_test_utf8mb4_support() {
   1251 		global $wpdb;
   1252 
   1253 		if ( ! $this->mysql_server_version ) {
   1254 			$this->prepare_sql_data();
   1255 		}
   1256 
   1257 		$result = array(
   1258 			'label'       => __( 'UTF8MB4 is supported' ),
   1259 			'status'      => 'good',
   1260 			'badge'       => array(
   1261 				'label' => __( 'Performance' ),
   1262 				'color' => 'blue',
   1263 			),
   1264 			'description' => sprintf(
   1265 				'<p>%s</p>',
   1266 				__( 'UTF8MB4 is the character set WordPress prefers for database storage because it safely supports the widest set of characters and encodings, including Emoji, enabling better support for non-English languages.' )
   1267 			),
   1268 			'actions'     => '',
   1269 			'test'        => 'utf8mb4_support',
   1270 		);
   1271 
   1272 		if ( ! $this->is_mariadb ) {
   1273 			if ( version_compare( $this->mysql_server_version, '5.5.3', '<' ) ) {
   1274 				$result['status'] = 'recommended';
   1275 
   1276 				$result['label'] = __( 'utf8mb4 requires a MySQL update' );
   1277 
   1278 				$result['description'] .= sprintf(
   1279 					'<p>%s</p>',
   1280 					sprintf(
   1281 						/* translators: %s: Version number. */
   1282 						__( 'WordPress&#8217; utf8mb4 support requires MySQL version %s or greater. Please contact your server administrator.' ),
   1283 						'5.5.3'
   1284 					)
   1285 				);
   1286 			} else {
   1287 				$result['description'] .= sprintf(
   1288 					'<p>%s</p>',
   1289 					__( 'Your MySQL version supports utf8mb4.' )
   1290 				);
   1291 			}
   1292 		} else { // MariaDB introduced utf8mb4 support in 5.5.0.
   1293 			if ( version_compare( $this->mysql_server_version, '5.5.0', '<' ) ) {
   1294 				$result['status'] = 'recommended';
   1295 
   1296 				$result['label'] = __( 'utf8mb4 requires a MariaDB update' );
   1297 
   1298 				$result['description'] .= sprintf(
   1299 					'<p>%s</p>',
   1300 					sprintf(
   1301 						/* translators: %s: Version number. */
   1302 						__( 'WordPress&#8217; utf8mb4 support requires MariaDB version %s or greater. Please contact your server administrator.' ),
   1303 						'5.5.0'
   1304 					)
   1305 				);
   1306 			} else {
   1307 				$result['description'] .= sprintf(
   1308 					'<p>%s</p>',
   1309 					__( 'Your MariaDB version supports utf8mb4.' )
   1310 				);
   1311 			}
   1312 		}
   1313 
   1314 		if ( $wpdb->use_mysqli ) {
   1315 			// phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_client_info
   1316 			$mysql_client_version = mysqli_get_client_info();
   1317 		} else {
   1318 			// phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_client_info,PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
   1319 			$mysql_client_version = mysql_get_client_info();
   1320 		}
   1321 
   1322 		/*
   1323 		 * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
   1324 		 * mysqlnd has supported utf8mb4 since 5.0.9.
   1325 		 */
   1326 		if ( false !== strpos( $mysql_client_version, 'mysqlnd' ) ) {
   1327 			$mysql_client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $mysql_client_version );
   1328 			if ( version_compare( $mysql_client_version, '5.0.9', '<' ) ) {
   1329 				$result['status'] = 'recommended';
   1330 
   1331 				$result['label'] = __( 'utf8mb4 requires a newer client library' );
   1332 
   1333 				$result['description'] .= sprintf(
   1334 					'<p>%s</p>',
   1335 					sprintf(
   1336 						/* translators: 1: Name of the library, 2: Number of version. */
   1337 						__( 'WordPress&#8217; utf8mb4 support requires MySQL client library (%1$s) version %2$s or newer. Please contact your server administrator.' ),
   1338 						'mysqlnd',
   1339 						'5.0.9'
   1340 					)
   1341 				);
   1342 			}
   1343 		} else {
   1344 			if ( version_compare( $mysql_client_version, '5.5.3', '<' ) ) {
   1345 				$result['status'] = 'recommended';
   1346 
   1347 				$result['label'] = __( 'utf8mb4 requires a newer client library' );
   1348 
   1349 				$result['description'] .= sprintf(
   1350 					'<p>%s</p>',
   1351 					sprintf(
   1352 						/* translators: 1: Name of the library, 2: Number of version. */
   1353 						__( 'WordPress&#8217; utf8mb4 support requires MySQL client library (%1$s) version %2$s or newer. Please contact your server administrator.' ),
   1354 						'libmysql',
   1355 						'5.5.3'
   1356 					)
   1357 				);
   1358 			}
   1359 		}
   1360 
   1361 		return $result;
   1362 	}
   1363 
   1364 	/**
   1365 	 * Test if the site can communicate with WordPress.org.
   1366 	 *
   1367 	 * @since 5.2.0
   1368 	 *
   1369 	 * @return array The test results.
   1370 	 */
   1371 	public function get_test_dotorg_communication() {
   1372 		$result = array(
   1373 			'label'       => __( 'Can communicate with WordPress.org' ),
   1374 			'status'      => '',
   1375 			'badge'       => array(
   1376 				'label' => __( 'Security' ),
   1377 				'color' => 'blue',
   1378 			),
   1379 			'description' => sprintf(
   1380 				'<p>%s</p>',
   1381 				__( 'Communicating with the WordPress servers is used to check for new versions, and to both install and update WordPress core, themes or plugins.' )
   1382 			),
   1383 			'actions'     => '',
   1384 			'test'        => 'dotorg_communication',
   1385 		);
   1386 
   1387 		$wp_dotorg = wp_remote_get(
   1388 			'https://api.wordpress.org',
   1389 			array(
   1390 				'timeout' => 10,
   1391 			)
   1392 		);
   1393 		if ( ! is_wp_error( $wp_dotorg ) ) {
   1394 			$result['status'] = 'good';
   1395 		} else {
   1396 			$result['status'] = 'critical';
   1397 
   1398 			$result['label'] = __( 'Could not reach WordPress.org' );
   1399 
   1400 			$result['description'] .= sprintf(
   1401 				'<p>%s</p>',
   1402 				sprintf(
   1403 					'<span class="error"><span class="screen-reader-text">%s</span></span> %s',
   1404 					__( 'Error' ),
   1405 					sprintf(
   1406 						/* translators: 1: The IP address WordPress.org resolves to. 2: The error returned by the lookup. */
   1407 						__( 'Your site is unable to reach WordPress.org at %1$s, and returned the error: %2$s' ),
   1408 						gethostbyname( 'api.wordpress.org' ),
   1409 						$wp_dotorg->get_error_message()
   1410 					)
   1411 				)
   1412 			);
   1413 
   1414 			$result['actions'] = sprintf(
   1415 				'<p><a href="%s" target="_blank" rel="noopener">%s <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   1416 				/* translators: Localized Support reference. */
   1417 				esc_url( __( 'https://wordpress.org/support' ) ),
   1418 				__( 'Get help resolving this issue.' ),
   1419 				/* translators: Accessibility text. */
   1420 				__( '(opens in a new tab)' )
   1421 			);
   1422 		}
   1423 
   1424 		return $result;
   1425 	}
   1426 
   1427 	/**
   1428 	 * Test if debug information is enabled.
   1429 	 *
   1430 	 * When WP_DEBUG is enabled, errors and information may be disclosed to site visitors,
   1431 	 * or logged to a publicly accessible file.
   1432 	 *
   1433 	 * Debugging is also frequently left enabled after looking for errors on a site,
   1434 	 * as site owners do not understand the implications of this.
   1435 	 *
   1436 	 * @since 5.2.0
   1437 	 *
   1438 	 * @return array The test results.
   1439 	 */
   1440 	public function get_test_is_in_debug_mode() {
   1441 		$result = array(
   1442 			'label'       => __( 'Your site is not set to output debug information' ),
   1443 			'status'      => 'good',
   1444 			'badge'       => array(
   1445 				'label' => __( 'Security' ),
   1446 				'color' => 'blue',
   1447 			),
   1448 			'description' => sprintf(
   1449 				'<p>%s</p>',
   1450 				__( 'Debug mode is often enabled to gather more details about an error or site failure, but may contain sensitive information which should not be available on a publicly available website.' )
   1451 			),
   1452 			'actions'     => sprintf(
   1453 				'<p><a href="%s" target="_blank" rel="noopener">%s <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   1454 				/* translators: Documentation explaining debugging in WordPress. */
   1455 				esc_url( __( 'https://wordpress.org/support/article/debugging-in-wordpress/' ) ),
   1456 				__( 'Learn more about debugging in WordPress.' ),
   1457 				/* translators: Accessibility text. */
   1458 				__( '(opens in a new tab)' )
   1459 			),
   1460 			'test'        => 'is_in_debug_mode',
   1461 		);
   1462 
   1463 		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
   1464 			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
   1465 				$result['label'] = __( 'Your site is set to log errors to a potentially public file.' );
   1466 
   1467 				$result['status'] = ( 0 === strpos( ini_get( 'error_log' ), ABSPATH ) ) ? 'critical' : 'recommended';
   1468 
   1469 				$result['description'] .= sprintf(
   1470 					'<p>%s</p>',
   1471 					sprintf(
   1472 						/* translators: %s: WP_DEBUG_LOG */
   1473 						__( 'The value, %s, has been added to this website&#8217;s configuration file. This means any errors on the site will be written to a file which is potentially available to all users.' ),
   1474 						'<code>WP_DEBUG_LOG</code>'
   1475 					)
   1476 				);
   1477 			}
   1478 
   1479 			if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
   1480 				$result['label'] = __( 'Your site is set to display errors to site visitors' );
   1481 
   1482 				$result['status'] = 'critical';
   1483 
   1484 				// On development environments, set the status to recommended.
   1485 				if ( $this->is_development_environment() ) {
   1486 					$result['status'] = 'recommended';
   1487 				}
   1488 
   1489 				$result['description'] .= sprintf(
   1490 					'<p>%s</p>',
   1491 					sprintf(
   1492 						/* translators: 1: WP_DEBUG_DISPLAY, 2: WP_DEBUG */
   1493 						__( 'The value, %1$s, has either been enabled by %2$s or added to your configuration file. This will make errors display on the front end of your site.' ),
   1494 						'<code>WP_DEBUG_DISPLAY</code>',
   1495 						'<code>WP_DEBUG</code>'
   1496 					)
   1497 				);
   1498 			}
   1499 		}
   1500 
   1501 		return $result;
   1502 	}
   1503 
   1504 	/**
   1505 	 * Test if your site is serving content over HTTPS.
   1506 	 *
   1507 	 * Many sites have varying degrees of HTTPS support, the most common of which is sites that have it
   1508 	 * enabled, but only if you visit the right site address.
   1509 	 *
   1510 	 * @since 5.2.0
   1511 	 * @since 5.7.0 Updated to rely on {@see wp_is_using_https()} and {@see wp_is_https_supported()}.
   1512 	 *
   1513 	 * @return array The test results.
   1514 	 */
   1515 	public function get_test_https_status() {
   1516 		// Enforce fresh HTTPS detection results. This is normally invoked by using cron,
   1517 		// but for Site Health it should always rely on the latest results.
   1518 		wp_update_https_detection_errors();
   1519 
   1520 		$default_update_url = wp_get_default_update_https_url();
   1521 
   1522 		$result = array(
   1523 			'label'       => __( 'Your website is using an active HTTPS connection' ),
   1524 			'status'      => 'good',
   1525 			'badge'       => array(
   1526 				'label' => __( 'Security' ),
   1527 				'color' => 'blue',
   1528 			),
   1529 			'description' => sprintf(
   1530 				'<p>%s</p>',
   1531 				__( 'An HTTPS connection is a more secure way of browsing the web. Many services now have HTTPS as a requirement. HTTPS allows you to take advantage of new features that can increase site speed, improve search rankings, and gain the trust of your visitors by helping to protect their online privacy.' )
   1532 			),
   1533 			'actions'     => sprintf(
   1534 				'<p><a href="%s" target="_blank" rel="noopener">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   1535 				esc_url( $default_update_url ),
   1536 				__( 'Learn more about why you should use HTTPS' ),
   1537 				/* translators: Accessibility text. */
   1538 				__( '(opens in a new tab)' )
   1539 			),
   1540 			'test'        => 'https_status',
   1541 		);
   1542 
   1543 		if ( ! wp_is_using_https() ) {
   1544 			// If the website is not using HTTPS, provide more information
   1545 			// about whether it is supported and how it can be enabled.
   1546 			$result['status'] = 'recommended';
   1547 			$result['label']  = __( 'Your website does not use HTTPS' );
   1548 
   1549 			if ( wp_is_site_url_using_https() ) {
   1550 				if ( is_ssl() ) {
   1551 					$result['description'] = sprintf(
   1552 						'<p>%s</p>',
   1553 						sprintf(
   1554 							/* translators: %s: URL to Settings > General > Site Address. */
   1555 							__( 'You are accessing this website using HTTPS, but your <a href="%s">Site Address</a> is not set up to use HTTPS by default.' ),
   1556 							esc_url( admin_url( 'options-general.php' ) . '#home' )
   1557 						)
   1558 					);
   1559 				} else {
   1560 					$result['description'] = sprintf(
   1561 						'<p>%s</p>',
   1562 						sprintf(
   1563 							/* translators: %s: URL to Settings > General > Site Address. */
   1564 							__( 'Your <a href="%s">Site Address</a> is not set up to use HTTPS.' ),
   1565 							esc_url( admin_url( 'options-general.php' ) . '#home' )
   1566 						)
   1567 					);
   1568 				}
   1569 			} else {
   1570 				if ( is_ssl() ) {
   1571 					$result['description'] = sprintf(
   1572 						'<p>%s</p>',
   1573 						sprintf(
   1574 							/* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
   1575 							__( 'You are accessing this website using HTTPS, but your <a href="%1$s">WordPress Address</a> and <a href="%2$s">Site Address</a> are not set up to use HTTPS by default.' ),
   1576 							esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
   1577 							esc_url( admin_url( 'options-general.php' ) . '#home' )
   1578 						)
   1579 					);
   1580 				} else {
   1581 					$result['description'] = sprintf(
   1582 						'<p>%s</p>',
   1583 						sprintf(
   1584 							/* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
   1585 							__( 'Your <a href="%1$s">WordPress Address</a> and <a href="%2$s">Site Address</a> are not set up to use HTTPS.' ),
   1586 							esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
   1587 							esc_url( admin_url( 'options-general.php' ) . '#home' )
   1588 						)
   1589 					);
   1590 				}
   1591 			}
   1592 
   1593 			if ( wp_is_https_supported() ) {
   1594 				$result['description'] .= sprintf(
   1595 					'<p>%s</p>',
   1596 					__( 'HTTPS is already supported for your website.' )
   1597 				);
   1598 
   1599 				if ( defined( 'WP_HOME' ) || defined( 'WP_SITEURL' ) ) {
   1600 					$result['description'] .= sprintf(
   1601 						'<p>%s</p>',
   1602 						sprintf(
   1603 							/* translators: 1: wp-config.php, 2: WP_HOME, 3: WP_SITEURL */
   1604 							__( 'However, your WordPress Address is currently controlled by a PHP constant and therefore cannot be updated. You need to edit your %1$s and remove or update the definitions of %2$s and %3$s.' ),
   1605 							'<code>wp-config.php</code>',
   1606 							'<code>WP_HOME</code>',
   1607 							'<code>WP_SITEURL</code>'
   1608 						)
   1609 					);
   1610 				} elseif ( current_user_can( 'update_https' ) ) {
   1611 					$default_direct_update_url = add_query_arg( 'action', 'update_https', wp_nonce_url( admin_url( 'site-health.php' ), 'wp_update_https' ) );
   1612 					$direct_update_url         = wp_get_direct_update_https_url();
   1613 
   1614 					if ( ! empty( $direct_update_url ) ) {
   1615 						$result['actions'] = sprintf(
   1616 							'<p class="button-container"><a class="button button-primary" href="%1$s" target="_blank" rel="noopener">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   1617 							esc_url( $direct_update_url ),
   1618 							__( 'Update your site to use HTTPS' ),
   1619 							/* translators: Accessibility text. */
   1620 							__( '(opens in a new tab)' )
   1621 						);
   1622 					} else {
   1623 						$result['actions'] = sprintf(
   1624 							'<p class="button-container"><a class="button button-primary" href="%1$s">%2$s</a></p>',
   1625 							esc_url( $default_direct_update_url ),
   1626 							__( 'Update your site to use HTTPS' )
   1627 						);
   1628 					}
   1629 				}
   1630 			} else {
   1631 				// If host-specific "Update HTTPS" URL is provided, include a link.
   1632 				$update_url = wp_get_update_https_url();
   1633 				if ( $update_url !== $default_update_url ) {
   1634 					$result['description'] .= sprintf(
   1635 						'<p><a href="%s" target="_blank" rel="noopener">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   1636 						esc_url( $update_url ),
   1637 						__( 'Talk to your web host about supporting HTTPS for your website.' ),
   1638 						/* translators: Accessibility text. */
   1639 						__( '(opens in a new tab)' )
   1640 					);
   1641 				} else {
   1642 					$result['description'] .= sprintf(
   1643 						'<p>%s</p>',
   1644 						__( 'Talk to your web host about supporting HTTPS for your website.' )
   1645 					);
   1646 				}
   1647 			}
   1648 		}
   1649 
   1650 		return $result;
   1651 	}
   1652 
   1653 	/**
   1654 	 * Check if the HTTP API can handle SSL/TLS requests.
   1655 	 *
   1656 	 * @since 5.2.0
   1657 	 *
   1658 	 * @return array The test results.
   1659 	 */
   1660 	public function get_test_ssl_support() {
   1661 		$result = array(
   1662 			'label'       => '',
   1663 			'status'      => '',
   1664 			'badge'       => array(
   1665 				'label' => __( 'Security' ),
   1666 				'color' => 'blue',
   1667 			),
   1668 			'description' => sprintf(
   1669 				'<p>%s</p>',
   1670 				__( 'Securely communicating between servers are needed for transactions such as fetching files, conducting sales on store sites, and much more.' )
   1671 			),
   1672 			'actions'     => '',
   1673 			'test'        => 'ssl_support',
   1674 		);
   1675 
   1676 		$supports_https = wp_http_supports( array( 'ssl' ) );
   1677 
   1678 		if ( $supports_https ) {
   1679 			$result['status'] = 'good';
   1680 
   1681 			$result['label'] = __( 'Your site can communicate securely with other services' );
   1682 		} else {
   1683 			$result['status'] = 'critical';
   1684 
   1685 			$result['label'] = __( 'Your site is unable to communicate securely with other services' );
   1686 
   1687 			$result['description'] .= sprintf(
   1688 				'<p>%s</p>',
   1689 				__( 'Talk to your web host about OpenSSL support for PHP.' )
   1690 			);
   1691 		}
   1692 
   1693 		return $result;
   1694 	}
   1695 
   1696 	/**
   1697 	 * Test if scheduled events run as intended.
   1698 	 *
   1699 	 * If scheduled events are not running, this may indicate something with WP_Cron is not working
   1700 	 * as intended, or that there are orphaned events hanging around from older code.
   1701 	 *
   1702 	 * @since 5.2.0
   1703 	 *
   1704 	 * @return array The test results.
   1705 	 */
   1706 	public function get_test_scheduled_events() {
   1707 		$result = array(
   1708 			'label'       => __( 'Scheduled events are running' ),
   1709 			'status'      => 'good',
   1710 			'badge'       => array(
   1711 				'label' => __( 'Performance' ),
   1712 				'color' => 'blue',
   1713 			),
   1714 			'description' => sprintf(
   1715 				'<p>%s</p>',
   1716 				__( 'Scheduled events are what periodically looks for updates to plugins, themes and WordPress itself. It is also what makes sure scheduled posts are published on time. It may also be used by various plugins to make sure that planned actions are executed.' )
   1717 			),
   1718 			'actions'     => '',
   1719 			'test'        => 'scheduled_events',
   1720 		);
   1721 
   1722 		$this->wp_schedule_test_init();
   1723 
   1724 		if ( is_wp_error( $this->has_missed_cron() ) ) {
   1725 			$result['status'] = 'critical';
   1726 
   1727 			$result['label'] = __( 'It was not possible to check your scheduled events' );
   1728 
   1729 			$result['description'] = sprintf(
   1730 				'<p>%s</p>',
   1731 				sprintf(
   1732 					/* translators: %s: The error message returned while from the cron scheduler. */
   1733 					__( 'While trying to test your site&#8217;s scheduled events, the following error was returned: %s' ),
   1734 					$this->has_missed_cron()->get_error_message()
   1735 				)
   1736 			);
   1737 		} elseif ( $this->has_missed_cron() ) {
   1738 			$result['status'] = 'recommended';
   1739 
   1740 			$result['label'] = __( 'A scheduled event has failed' );
   1741 
   1742 			$result['description'] = sprintf(
   1743 				'<p>%s</p>',
   1744 				sprintf(
   1745 					/* translators: %s: The name of the failed cron event. */
   1746 					__( 'The scheduled event, %s, failed to run. Your site still works, but this may indicate that scheduling posts or automated updates may not work as intended.' ),
   1747 					$this->last_missed_cron
   1748 				)
   1749 			);
   1750 		} elseif ( $this->has_late_cron() ) {
   1751 			$result['status'] = 'recommended';
   1752 
   1753 			$result['label'] = __( 'A scheduled event is late' );
   1754 
   1755 			$result['description'] = sprintf(
   1756 				'<p>%s</p>',
   1757 				sprintf(
   1758 					/* translators: %s: The name of the late cron event. */
   1759 					__( 'The scheduled event, %s, is late to run. Your site still works, but this may indicate that scheduling posts or automated updates may not work as intended.' ),
   1760 					$this->last_late_cron
   1761 				)
   1762 			);
   1763 		}
   1764 
   1765 		return $result;
   1766 	}
   1767 
   1768 	/**
   1769 	 * Test if WordPress can run automated background updates.
   1770 	 *
   1771 	 * Background updates in WordPress are primarily used for minor releases and security updates.
   1772 	 * It's important to either have these working, or be aware that they are intentionally disabled
   1773 	 * for whatever reason.
   1774 	 *
   1775 	 * @since 5.2.0
   1776 	 *
   1777 	 * @return array The test results.
   1778 	 */
   1779 	public function get_test_background_updates() {
   1780 		$result = array(
   1781 			'label'       => __( 'Background updates are working' ),
   1782 			'status'      => 'good',
   1783 			'badge'       => array(
   1784 				'label' => __( 'Security' ),
   1785 				'color' => 'blue',
   1786 			),
   1787 			'description' => sprintf(
   1788 				'<p>%s</p>',
   1789 				__( 'Background updates ensure that WordPress can auto-update if a security update is released for the version you are currently using.' )
   1790 			),
   1791 			'actions'     => '',
   1792 			'test'        => 'background_updates',
   1793 		);
   1794 
   1795 		if ( ! class_exists( 'WP_Site_Health_Auto_Updates' ) ) {
   1796 			require_once ABSPATH . 'wp-admin/includes/class-wp-site-health-auto-updates.php';
   1797 		}
   1798 
   1799 		// Run the auto-update tests in a separate class,
   1800 		// as there are many considerations to be made.
   1801 		$automatic_updates = new WP_Site_Health_Auto_Updates();
   1802 		$tests             = $automatic_updates->run_tests();
   1803 
   1804 		$output = '<ul>';
   1805 
   1806 		foreach ( $tests as $test ) {
   1807 			$severity_string = __( 'Passed' );
   1808 
   1809 			if ( 'fail' === $test->severity ) {
   1810 				$result['label'] = __( 'Background updates are not working as expected' );
   1811 
   1812 				$result['status'] = 'critical';
   1813 
   1814 				$severity_string = __( 'Error' );
   1815 			}
   1816 
   1817 			if ( 'warning' === $test->severity && 'good' === $result['status'] ) {
   1818 				$result['label'] = __( 'Background updates may not be working properly' );
   1819 
   1820 				$result['status'] = 'recommended';
   1821 
   1822 				$severity_string = __( 'Warning' );
   1823 			}
   1824 
   1825 			$output .= sprintf(
   1826 				'<li><span class="dashicons %s"><span class="screen-reader-text">%s</span></span> %s</li>',
   1827 				esc_attr( $test->severity ),
   1828 				$severity_string,
   1829 				$test->description
   1830 			);
   1831 		}
   1832 
   1833 		$output .= '</ul>';
   1834 
   1835 		if ( 'good' !== $result['status'] ) {
   1836 			$result['description'] .= $output;
   1837 		}
   1838 
   1839 		return $result;
   1840 	}
   1841 
   1842 	/**
   1843 	 * Test if plugin and theme auto-updates appear to be configured correctly.
   1844 	 *
   1845 	 * @since 5.5.0
   1846 	 *
   1847 	 * @return array The test results.
   1848 	 */
   1849 	public function get_test_plugin_theme_auto_updates() {
   1850 		$result = array(
   1851 			'label'       => __( 'Plugin and theme auto-updates appear to be configured correctly' ),
   1852 			'status'      => 'good',
   1853 			'badge'       => array(
   1854 				'label' => __( 'Security' ),
   1855 				'color' => 'blue',
   1856 			),
   1857 			'description' => sprintf(
   1858 				'<p>%s</p>',
   1859 				__( 'Plugin and theme auto-updates ensure that the latest versions are always installed.' )
   1860 			),
   1861 			'actions'     => '',
   1862 			'test'        => 'plugin_theme_auto_updates',
   1863 		);
   1864 
   1865 		$check_plugin_theme_updates = $this->detect_plugin_theme_auto_update_issues();
   1866 
   1867 		$result['status'] = $check_plugin_theme_updates->status;
   1868 
   1869 		if ( 'good' !== $result['status'] ) {
   1870 			$result['label'] = __( 'Your site may have problems auto-updating plugins and themes' );
   1871 
   1872 			$result['description'] .= sprintf(
   1873 				'<p>%s</p>',
   1874 				$check_plugin_theme_updates->message
   1875 			);
   1876 		}
   1877 
   1878 		return $result;
   1879 	}
   1880 
   1881 	/**
   1882 	 * Test if loopbacks work as expected.
   1883 	 *
   1884 	 * A loopback is when WordPress queries itself, for example to start a new WP_Cron instance,
   1885 	 * or when editing a plugin or theme. This has shown itself to be a recurring issue,
   1886 	 * as code can very easily break this interaction.
   1887 	 *
   1888 	 * @since 5.2.0
   1889 	 *
   1890 	 * @return array The test results.
   1891 	 */
   1892 	public function get_test_loopback_requests() {
   1893 		$result = array(
   1894 			'label'       => __( 'Your site can perform loopback requests' ),
   1895 			'status'      => 'good',
   1896 			'badge'       => array(
   1897 				'label' => __( 'Performance' ),
   1898 				'color' => 'blue',
   1899 			),
   1900 			'description' => sprintf(
   1901 				'<p>%s</p>',
   1902 				__( 'Loopback requests are used to run scheduled events, and are also used by the built-in editors for themes and plugins to verify code stability.' )
   1903 			),
   1904 			'actions'     => '',
   1905 			'test'        => 'loopback_requests',
   1906 		);
   1907 
   1908 		$check_loopback = $this->can_perform_loopback();
   1909 
   1910 		$result['status'] = $check_loopback->status;
   1911 
   1912 		if ( 'good' !== $result['status'] ) {
   1913 			$result['label'] = __( 'Your site could not complete a loopback request' );
   1914 
   1915 			$result['description'] .= sprintf(
   1916 				'<p>%s</p>',
   1917 				$check_loopback->message
   1918 			);
   1919 		}
   1920 
   1921 		return $result;
   1922 	}
   1923 
   1924 	/**
   1925 	 * Test if HTTP requests are blocked.
   1926 	 *
   1927 	 * It's possible to block all outgoing communication (with the possibility of allowing certain
   1928 	 * hosts) via the HTTP API. This may create problems for users as many features are running as
   1929 	 * services these days.
   1930 	 *
   1931 	 * @since 5.2.0
   1932 	 *
   1933 	 * @return array The test results.
   1934 	 */
   1935 	public function get_test_http_requests() {
   1936 		$result = array(
   1937 			'label'       => __( 'HTTP requests seem to be working as expected' ),
   1938 			'status'      => 'good',
   1939 			'badge'       => array(
   1940 				'label' => __( 'Performance' ),
   1941 				'color' => 'blue',
   1942 			),
   1943 			'description' => sprintf(
   1944 				'<p>%s</p>',
   1945 				__( 'It is possible for site maintainers to block all, or some, communication to other sites and services. If set up incorrectly, this may prevent plugins and themes from working as intended.' )
   1946 			),
   1947 			'actions'     => '',
   1948 			'test'        => 'http_requests',
   1949 		);
   1950 
   1951 		$blocked = false;
   1952 		$hosts   = array();
   1953 
   1954 		if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) {
   1955 			$blocked = true;
   1956 		}
   1957 
   1958 		if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
   1959 			$hosts = explode( ',', WP_ACCESSIBLE_HOSTS );
   1960 		}
   1961 
   1962 		if ( $blocked && 0 === count( $hosts ) ) {
   1963 			$result['status'] = 'critical';
   1964 
   1965 			$result['label'] = __( 'HTTP requests are blocked' );
   1966 
   1967 			$result['description'] .= sprintf(
   1968 				'<p>%s</p>',
   1969 				sprintf(
   1970 					/* translators: %s: Name of the constant used. */
   1971 					__( 'HTTP requests have been blocked by the %s constant, with no allowed hosts.' ),
   1972 					'<code>WP_HTTP_BLOCK_EXTERNAL</code>'
   1973 				)
   1974 			);
   1975 		}
   1976 
   1977 		if ( $blocked && 0 < count( $hosts ) ) {
   1978 			$result['status'] = 'recommended';
   1979 
   1980 			$result['label'] = __( 'HTTP requests are partially blocked' );
   1981 
   1982 			$result['description'] .= sprintf(
   1983 				'<p>%s</p>',
   1984 				sprintf(
   1985 					/* translators: 1: Name of the constant used. 2: List of allowed hostnames. */
   1986 					__( 'HTTP requests have been blocked by the %1$s constant, with some allowed hosts: %2$s.' ),
   1987 					'<code>WP_HTTP_BLOCK_EXTERNAL</code>',
   1988 					implode( ',', $hosts )
   1989 				)
   1990 			);
   1991 		}
   1992 
   1993 		return $result;
   1994 	}
   1995 
   1996 	/**
   1997 	 * Test if the REST API is accessible.
   1998 	 *
   1999 	 * Various security measures may block the REST API from working, or it may have been disabled in general.
   2000 	 * This is required for the new block editor to work, so we explicitly test for this.
   2001 	 *
   2002 	 * @since 5.2.0
   2003 	 *
   2004 	 * @return array The test results.
   2005 	 */
   2006 	public function get_test_rest_availability() {
   2007 		$result = array(
   2008 			'label'       => __( 'The REST API is available' ),
   2009 			'status'      => 'good',
   2010 			'badge'       => array(
   2011 				'label' => __( 'Performance' ),
   2012 				'color' => 'blue',
   2013 			),
   2014 			'description' => sprintf(
   2015 				'<p>%s</p>',
   2016 				__( 'The REST API is one way WordPress, and other applications, communicate with the server. One example is the block editor screen, which relies on this to display, and save, your posts and pages.' )
   2017 			),
   2018 			'actions'     => '',
   2019 			'test'        => 'rest_availability',
   2020 		);
   2021 
   2022 		$cookies = wp_unslash( $_COOKIE );
   2023 		$timeout = 10;
   2024 		$headers = array(
   2025 			'Cache-Control' => 'no-cache',
   2026 			'X-WP-Nonce'    => wp_create_nonce( 'wp_rest' ),
   2027 		);
   2028 		/** This filter is documented in wp-includes/class-wp-http-streams.php */
   2029 		$sslverify = apply_filters( 'https_local_ssl_verify', false );
   2030 
   2031 		// Include Basic auth in loopback requests.
   2032 		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
   2033 			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
   2034 		}
   2035 
   2036 		$url = rest_url( 'wp/v2/types/post' );
   2037 
   2038 		// The context for this is editing with the new block editor.
   2039 		$url = add_query_arg(
   2040 			array(
   2041 				'context' => 'edit',
   2042 			),
   2043 			$url
   2044 		);
   2045 
   2046 		$r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
   2047 
   2048 		if ( is_wp_error( $r ) ) {
   2049 			$result['status'] = 'critical';
   2050 
   2051 			$result['label'] = __( 'The REST API encountered an error' );
   2052 
   2053 			$result['description'] .= sprintf(
   2054 				'<p>%s</p>',
   2055 				sprintf(
   2056 					'%s<br>%s',
   2057 					__( 'The REST API request failed due to an error.' ),
   2058 					sprintf(
   2059 						/* translators: 1: The WordPress error message. 2: The WordPress error code. */
   2060 						__( 'Error: %1$s (%2$s)' ),
   2061 						$r->get_error_message(),
   2062 						$r->get_error_code()
   2063 					)
   2064 				)
   2065 			);
   2066 		} elseif ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
   2067 			$result['status'] = 'recommended';
   2068 
   2069 			$result['label'] = __( 'The REST API encountered an unexpected result' );
   2070 
   2071 			$result['description'] .= sprintf(
   2072 				'<p>%s</p>',
   2073 				sprintf(
   2074 					/* translators: 1: The HTTP error code. 2: The HTTP error message. */
   2075 					__( 'The REST API call gave the following unexpected result: (%1$d) %2$s.' ),
   2076 					wp_remote_retrieve_response_code( $r ),
   2077 					esc_html( wp_remote_retrieve_body( $r ) )
   2078 				)
   2079 			);
   2080 		} else {
   2081 			$json = json_decode( wp_remote_retrieve_body( $r ), true );
   2082 
   2083 			if ( false !== $json && ! isset( $json['capabilities'] ) ) {
   2084 				$result['status'] = 'recommended';
   2085 
   2086 				$result['label'] = __( 'The REST API did not behave correctly' );
   2087 
   2088 				$result['description'] .= sprintf(
   2089 					'<p>%s</p>',
   2090 					sprintf(
   2091 						/* translators: %s: The name of the query parameter being tested. */
   2092 						__( 'The REST API did not process the %s query parameter correctly.' ),
   2093 						'<code>context</code>'
   2094 					)
   2095 				);
   2096 			}
   2097 		}
   2098 
   2099 		return $result;
   2100 	}
   2101 
   2102 	/**
   2103 	 * Test if 'file_uploads' directive in PHP.ini is turned off.
   2104 	 *
   2105 	 * @since 5.5.0
   2106 	 *
   2107 	 * @return array The test results.
   2108 	 */
   2109 	public function get_test_file_uploads() {
   2110 		$result = array(
   2111 			'label'       => __( 'Files can be uploaded.' ),
   2112 			'status'      => 'good',
   2113 			'badge'       => array(
   2114 				'label' => __( 'Performance' ),
   2115 				'color' => 'blue',
   2116 			),
   2117 			'description' => sprintf(
   2118 				'<p>%s</p>',
   2119 				sprintf(
   2120 					/* translators: 1: file_uploads, 2: php.ini */
   2121 					__( 'The %1$s directive in %2$s determines if uploading files is allowed on your site.' ),
   2122 					'<code>file_uploads</code>',
   2123 					'<code>php.ini</code>'
   2124 				)
   2125 			),
   2126 			'actions'     => '',
   2127 			'test'        => 'file_uploads',
   2128 		);
   2129 
   2130 		if ( ! function_exists( 'ini_get' ) ) {
   2131 			$result['status']       = 'critical';
   2132 			$result['description'] .= sprintf(
   2133 				/* translators: %s: ini_get() */
   2134 				__( 'The %s function has been disabled, some media settings are unavailable because of this.' ),
   2135 				'<code>ini_get()</code>'
   2136 			);
   2137 			return $result;
   2138 		}
   2139 
   2140 		if ( empty( ini_get( 'file_uploads' ) ) ) {
   2141 			$result['status']       = 'critical';
   2142 			$result['description'] .= sprintf(
   2143 				'<p>%s</p>',
   2144 				sprintf(
   2145 					/* translators: 1: file_uploads, 2: 0 */
   2146 					__( '%1$s is set to %2$s. You won\'t be able to upload files on your site.' ),
   2147 					'<code>file_uploads</code>',
   2148 					'<code>0</code>'
   2149 				)
   2150 			);
   2151 			return $result;
   2152 		}
   2153 
   2154 		$post_max_size       = ini_get( 'post_max_size' );
   2155 		$upload_max_filesize = ini_get( 'upload_max_filesize' );
   2156 
   2157 		if ( wp_convert_hr_to_bytes( $post_max_size ) < wp_convert_hr_to_bytes( $upload_max_filesize ) ) {
   2158 			$result['label'] = sprintf(
   2159 				/* translators: 1: post_max_size, 2: upload_max_filesize */
   2160 				__( 'The "%1$s" value is smaller than "%2$s".' ),
   2161 				'post_max_size',
   2162 				'upload_max_filesize'
   2163 			);
   2164 			$result['status'] = 'recommended';
   2165 
   2166 			if ( 0 === wp_convert_hr_to_bytes( $post_max_size ) ) {
   2167 				$result['description'] = sprintf(
   2168 					'<p>%s</p>',
   2169 					sprintf(
   2170 						/* translators: 1: post_max_size, 2: upload_max_filesize */
   2171 						__( 'The setting for %1$s is currently configured as 0, this could cause some problems when trying to upload files through plugin or theme features that rely on various upload methods. It is recommended to configure this setting to a fixed value, ideally matching the value of %2$s, as some upload methods read the value 0 as either unlimited, or disabled.' ),
   2172 						'<code>post_max_size</code>',
   2173 						'<code>upload_max_filesize</code>'
   2174 					)
   2175 				);
   2176 			} else {
   2177 				$result['description'] = sprintf(
   2178 					'<p>%s</p>',
   2179 					sprintf(
   2180 						/* translators: 1: post_max_size, 2: upload_max_filesize */
   2181 						__( 'The setting for %1$s is smaller than %2$s, this could cause some problems when trying to upload files.' ),
   2182 						'<code>post_max_size</code>',
   2183 						'<code>upload_max_filesize</code>'
   2184 					)
   2185 				);
   2186 			}
   2187 
   2188 			return $result;
   2189 		}
   2190 
   2191 		return $result;
   2192 	}
   2193 
   2194 	/**
   2195 	 * Tests if the Authorization header has the expected values.
   2196 	 *
   2197 	 * @since 5.6.0
   2198 	 *
   2199 	 * @return array
   2200 	 */
   2201 	public function get_test_authorization_header() {
   2202 		$result = array(
   2203 			'label'       => __( 'The Authorization header is working as expected.' ),
   2204 			'status'      => 'good',
   2205 			'badge'       => array(
   2206 				'label' => __( 'Security' ),
   2207 				'color' => 'blue',
   2208 			),
   2209 			'description' => sprintf(
   2210 				'<p>%s</p>',
   2211 				__( 'The Authorization header comes from the third-party applications you approve. Without it, those apps cannot connect to your site.' )
   2212 			),
   2213 			'actions'     => '',
   2214 			'test'        => 'authorization_header',
   2215 		);
   2216 
   2217 		if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) {
   2218 			$result['label'] = __( 'The authorization header is missing.' );
   2219 		} elseif ( 'user' !== $_SERVER['PHP_AUTH_USER'] || 'pwd' !== $_SERVER['PHP_AUTH_PW'] ) {
   2220 			$result['label'] = __( 'The authorization header is invalid.' );
   2221 		} else {
   2222 			return $result;
   2223 		}
   2224 
   2225 		$result['status'] = 'recommended';
   2226 
   2227 		if ( ! function_exists( 'got_mod_rewrite' ) ) {
   2228 			require_once ABSPATH . 'wp-admin/includes/misc.php';
   2229 		}
   2230 
   2231 		if ( got_mod_rewrite() ) {
   2232 			$result['actions'] .= sprintf(
   2233 				'<p><a href="%s">%s</a></p>',
   2234 				esc_url( admin_url( 'options-permalink.php' ) ),
   2235 				__( 'Flush permalinks' )
   2236 			);
   2237 		} else {
   2238 			$result['actions'] .= sprintf(
   2239 				'<p><a href="%s" target="_blank" rel="noopener">%s <span class="screen-reader-text">%s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
   2240 				__( 'https://developer.wordpress.org/rest-api/frequently-asked-questions/#why-is-authentication-not-working' ),
   2241 				__( 'Learn how to configure the Authorization header.' ),
   2242 				/* translators: Accessibility text. */
   2243 				__( '(opens in a new tab)' )
   2244 			);
   2245 		}
   2246 
   2247 		return $result;
   2248 	}
   2249 
   2250 	/**
   2251 	 * Return a set of tests that belong to the site status page.
   2252 	 *
   2253 	 * Each site status test is defined here, they may be `direct` tests, that run on page load, or `async` tests
   2254 	 * which will run later down the line via JavaScript calls to improve page performance and hopefully also user
   2255 	 * experiences.
   2256 	 *
   2257 	 * @since 5.2.0
   2258 	 * @since 5.6.0 Added support for `has_rest` and `permissions`.
   2259 	 *
   2260 	 * @return array The list of tests to run.
   2261 	 */
   2262 	public static function get_tests() {
   2263 		$tests = array(
   2264 			'direct' => array(
   2265 				'wordpress_version'         => array(
   2266 					'label' => __( 'WordPress Version' ),
   2267 					'test'  => 'wordpress_version',
   2268 				),
   2269 				'plugin_version'            => array(
   2270 					'label' => __( 'Plugin Versions' ),
   2271 					'test'  => 'plugin_version',
   2272 				),
   2273 				'theme_version'             => array(
   2274 					'label' => __( 'Theme Versions' ),
   2275 					'test'  => 'theme_version',
   2276 				),
   2277 				'php_version'               => array(
   2278 					'label' => __( 'PHP Version' ),
   2279 					'test'  => 'php_version',
   2280 				),
   2281 				'php_extensions'            => array(
   2282 					'label' => __( 'PHP Extensions' ),
   2283 					'test'  => 'php_extensions',
   2284 				),
   2285 				'php_default_timezone'      => array(
   2286 					'label' => __( 'PHP Default Timezone' ),
   2287 					'test'  => 'php_default_timezone',
   2288 				),
   2289 				'php_sessions'              => array(
   2290 					'label' => __( 'PHP Sessions' ),
   2291 					'test'  => 'php_sessions',
   2292 				),
   2293 				'sql_server'                => array(
   2294 					'label' => __( 'Database Server version' ),
   2295 					'test'  => 'sql_server',
   2296 				),
   2297 				'utf8mb4_support'           => array(
   2298 					'label' => __( 'MySQL utf8mb4 support' ),
   2299 					'test'  => 'utf8mb4_support',
   2300 				),
   2301 				'ssl_support'               => array(
   2302 					'label' => __( 'Secure communication' ),
   2303 					'test'  => 'ssl_support',
   2304 				),
   2305 				'scheduled_events'          => array(
   2306 					'label' => __( 'Scheduled events' ),
   2307 					'test'  => 'scheduled_events',
   2308 				),
   2309 				'http_requests'             => array(
   2310 					'label' => __( 'HTTP Requests' ),
   2311 					'test'  => 'http_requests',
   2312 				),
   2313 				'rest_availability'         => array(
   2314 					'label'     => __( 'REST API availability' ),
   2315 					'test'      => 'rest_availability',
   2316 					'skip_cron' => true,
   2317 				),
   2318 				'debug_enabled'             => array(
   2319 					'label' => __( 'Debugging enabled' ),
   2320 					'test'  => 'is_in_debug_mode',
   2321 				),
   2322 				'file_uploads'              => array(
   2323 					'label' => __( 'File uploads' ),
   2324 					'test'  => 'file_uploads',
   2325 				),
   2326 				'plugin_theme_auto_updates' => array(
   2327 					'label' => __( 'Plugin and theme auto-updates' ),
   2328 					'test'  => 'plugin_theme_auto_updates',
   2329 				),
   2330 			),
   2331 			'async'  => array(
   2332 				'dotorg_communication' => array(
   2333 					'label'             => __( 'Communication with WordPress.org' ),
   2334 					'test'              => rest_url( 'wp-site-health/v1/tests/dotorg-communication' ),
   2335 					'has_rest'          => true,
   2336 					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_dotorg_communication' ),
   2337 				),
   2338 				'background_updates'   => array(
   2339 					'label'             => __( 'Background updates' ),
   2340 					'test'              => rest_url( 'wp-site-health/v1/tests/background-updates' ),
   2341 					'has_rest'          => true,
   2342 					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_background_updates' ),
   2343 				),
   2344 				'loopback_requests'    => array(
   2345 					'label'             => __( 'Loopback request' ),
   2346 					'test'              => rest_url( 'wp-site-health/v1/tests/loopback-requests' ),
   2347 					'has_rest'          => true,
   2348 					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_loopback_requests' ),
   2349 				),
   2350 				'https_status'         => array(
   2351 					'label'             => __( 'HTTPS status' ),
   2352 					'test'              => rest_url( 'wp-site-health/v1/tests/https-status' ),
   2353 					'has_rest'          => true,
   2354 					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_https_status' ),
   2355 				),
   2356 			),
   2357 		);
   2358 
   2359 		// Conditionally include Authorization header test if the site isn't protected by Basic Auth.
   2360 		if ( ! wp_is_site_protected_by_basic_auth() ) {
   2361 			$tests['async']['authorization_header'] = array(
   2362 				'label'     => __( 'Authorization header' ),
   2363 				'test'      => rest_url( 'wp-site-health/v1/tests/authorization-header' ),
   2364 				'has_rest'  => true,
   2365 				'headers'   => array( 'Authorization' => 'Basic ' . base64_encode( 'user:pwd' ) ),
   2366 				'skip_cron' => true,
   2367 			);
   2368 		}
   2369 
   2370 		/**
   2371 		 * Add or modify which site status tests are run on a site.
   2372 		 *
   2373 		 * The site health is determined by a set of tests based on best practices from
   2374 		 * both the WordPress Hosting Team, but also web standards in general.
   2375 		 *
   2376 		 * Some sites may not have the same requirements, for example the automatic update
   2377 		 * checks may be handled by a host, and are therefore disabled in core.
   2378 		 * Or maybe you want to introduce a new test, is caching enabled/disabled/stale for example.
   2379 		 *
   2380 		 * Tests may be added either as direct, or asynchronous ones. Any test that may require some time
   2381 		 * to complete should run asynchronously, to avoid extended loading periods within wp-admin.
   2382 		 *
   2383 		 * @since 5.2.0
   2384 		 * @since 5.6.0 Added the `async_direct_test` array key.
   2385 		 *              Added the `skip_cron` array key.
   2386 		 *
   2387 		 * @param array $test_type {
   2388 		 *     An associative array, where the `$test_type` is either `direct` or
   2389 		 *     `async`, to declare if the test should run via Ajax calls after page load.
   2390 		 *
   2391 		 *     @type array $identifier {
   2392 		 *         `$identifier` should be a unique identifier for the test that should run.
   2393 		 *         Plugins and themes are encouraged to prefix test identifiers with their slug
   2394 		 *         to avoid any collisions between tests.
   2395 		 *
   2396 		 *         @type string   $label             A friendly label for your test to identify it by.
   2397 		 *         @type mixed    $test              A callable to perform a direct test, or a string AJAX action
   2398 		 *                                           to be called to perform an async test.
   2399 		 *         @type boolean  $has_rest          Optional. Denote if `$test` has a REST API endpoint.
   2400 		 *         @type boolean  $skip_cron         Whether to skip this test when running as cron.
   2401 		 *         @type callable $async_direct_test A manner of directly calling the test marked as asynchronous,
   2402 		 *                                           as the scheduled event can not authenticate, and endpoints
   2403 		 *                                           may require authentication.
   2404 		 *     }
   2405 		 * }
   2406 		 */
   2407 		$tests = apply_filters( 'site_status_tests', $tests );
   2408 
   2409 		// Ensure that the filtered tests contain the required array keys.
   2410 		$tests = array_merge(
   2411 			array(
   2412 				'direct' => array(),
   2413 				'async'  => array(),
   2414 			),
   2415 			$tests
   2416 		);
   2417 
   2418 		return $tests;
   2419 	}
   2420 
   2421 	/**
   2422 	 * Add a class to the body HTML tag.
   2423 	 *
   2424 	 * Filters the body class string for admin pages and adds our own class for easier styling.
   2425 	 *
   2426 	 * @since 5.2.0
   2427 	 *
   2428 	 * @param string $body_class The body class string.
   2429 	 * @return string The modified body class string.
   2430 	 */
   2431 	public function admin_body_class( $body_class ) {
   2432 		$screen = get_current_screen();
   2433 		if ( 'site-health' !== $screen->id ) {
   2434 			return $body_class;
   2435 		}
   2436 
   2437 		$body_class .= ' site-health';
   2438 
   2439 		return $body_class;
   2440 	}
   2441 
   2442 	/**
   2443 	 * Initiate the WP_Cron schedule test cases.
   2444 	 *
   2445 	 * @since 5.2.0
   2446 	 */
   2447 	private function wp_schedule_test_init() {
   2448 		$this->schedules = wp_get_schedules();
   2449 		$this->get_cron_tasks();
   2450 	}
   2451 
   2452 	/**
   2453 	 * Populate our list of cron events and store them to a class-wide variable.
   2454 	 *
   2455 	 * @since 5.2.0
   2456 	 */
   2457 	private function get_cron_tasks() {
   2458 		$cron_tasks = _get_cron_array();
   2459 
   2460 		if ( empty( $cron_tasks ) ) {
   2461 			$this->crons = new WP_Error( 'no_tasks', __( 'No scheduled events exist on this site.' ) );
   2462 			return;
   2463 		}
   2464 
   2465 		$this->crons = array();
   2466 
   2467 		foreach ( $cron_tasks as $time => $cron ) {
   2468 			foreach ( $cron as $hook => $dings ) {
   2469 				foreach ( $dings as $sig => $data ) {
   2470 
   2471 					$this->crons[ "$hook-$sig-$time" ] = (object) array(
   2472 						'hook'     => $hook,
   2473 						'time'     => $time,
   2474 						'sig'      => $sig,
   2475 						'args'     => $data['args'],
   2476 						'schedule' => $data['schedule'],
   2477 						'interval' => isset( $data['interval'] ) ? $data['interval'] : null,
   2478 					);
   2479 
   2480 				}
   2481 			}
   2482 		}
   2483 	}
   2484 
   2485 	/**
   2486 	 * Check if any scheduled tasks have been missed.
   2487 	 *
   2488 	 * Returns a boolean value of `true` if a scheduled task has been missed and ends processing.
   2489 	 *
   2490 	 * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
   2491 	 *
   2492 	 * @since 5.2.0
   2493 	 *
   2494 	 * @return bool|WP_Error True if a cron was missed, false if not. WP_Error if the cron is set to that.
   2495 	 */
   2496 	public function has_missed_cron() {
   2497 		if ( is_wp_error( $this->crons ) ) {
   2498 			return $this->crons;
   2499 		}
   2500 
   2501 		foreach ( $this->crons as $id => $cron ) {
   2502 			if ( ( $cron->time - time() ) < $this->timeout_missed_cron ) {
   2503 				$this->last_missed_cron = $cron->hook;
   2504 				return true;
   2505 			}
   2506 		}
   2507 
   2508 		return false;
   2509 	}
   2510 
   2511 	/**
   2512 	 * Check if any scheduled tasks are late.
   2513 	 *
   2514 	 * Returns a boolean value of `true` if a scheduled task is late and ends processing.
   2515 	 *
   2516 	 * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
   2517 	 *
   2518 	 * @since 5.3.0
   2519 	 *
   2520 	 * @return bool|WP_Error True if a cron is late, false if not. WP_Error if the cron is set to that.
   2521 	 */
   2522 	public function has_late_cron() {
   2523 		if ( is_wp_error( $this->crons ) ) {
   2524 			return $this->crons;
   2525 		}
   2526 
   2527 		foreach ( $this->crons as $id => $cron ) {
   2528 			$cron_offset = $cron->time - time();
   2529 			if (
   2530 				$cron_offset >= $this->timeout_missed_cron &&
   2531 				$cron_offset < $this->timeout_late_cron
   2532 			) {
   2533 				$this->last_late_cron = $cron->hook;
   2534 				return true;
   2535 			}
   2536 		}
   2537 
   2538 		return false;
   2539 	}
   2540 
   2541 	/**
   2542 	 * Check for potential issues with plugin and theme auto-updates.
   2543 	 *
   2544 	 * Though there is no way to 100% determine if plugin and theme auto-updates are configured
   2545 	 * correctly, a few educated guesses could be made to flag any conditions that would
   2546 	 * potentially cause unexpected behaviors.
   2547 	 *
   2548 	 * @since 5.5.0
   2549 	 *
   2550 	 * @return object The test results.
   2551 	 */
   2552 	function detect_plugin_theme_auto_update_issues() {
   2553 		$mock_plugin = (object) array(
   2554 			'id'            => 'w.org/plugins/a-fake-plugin',
   2555 			'slug'          => 'a-fake-plugin',
   2556 			'plugin'        => 'a-fake-plugin/a-fake-plugin.php',
   2557 			'new_version'   => '9.9',
   2558 			'url'           => 'https://wordpress.org/plugins/a-fake-plugin/',
   2559 			'package'       => 'https://downloads.wordpress.org/plugin/a-fake-plugin.9.9.zip',
   2560 			'icons'         => array(
   2561 				'2x' => 'https://ps.w.org/a-fake-plugin/assets/icon-256x256.png',
   2562 				'1x' => 'https://ps.w.org/a-fake-plugin/assets/icon-128x128.png',
   2563 			),
   2564 			'banners'       => array(
   2565 				'2x' => 'https://ps.w.org/a-fake-plugin/assets/banner-1544x500.png',
   2566 				'1x' => 'https://ps.w.org/a-fake-plugin/assets/banner-772x250.png',
   2567 			),
   2568 			'banners_rtl'   => array(),
   2569 			'tested'        => '5.5.0',
   2570 			'requires_php'  => '5.6.20',
   2571 			'compatibility' => new stdClass(),
   2572 		);
   2573 
   2574 		$mock_theme = (object) array(
   2575 			'theme'        => 'a-fake-theme',
   2576 			'new_version'  => '9.9',
   2577 			'url'          => 'https://wordpress.org/themes/a-fake-theme/',
   2578 			'package'      => 'https://downloads.wordpress.org/theme/a-fake-theme.9.9.zip',
   2579 			'requires'     => '5.0.0',
   2580 			'requires_php' => '5.6.20',
   2581 		);
   2582 
   2583 		$test_plugins_enabled = wp_is_auto_update_forced_for_item( 'plugin', true, $mock_plugin );
   2584 		$test_themes_enabled  = wp_is_auto_update_forced_for_item( 'theme', true, $mock_theme );
   2585 
   2586 		$ui_enabled_for_plugins = wp_is_auto_update_enabled_for_type( 'plugin' );
   2587 		$ui_enabled_for_themes  = wp_is_auto_update_enabled_for_type( 'theme' );
   2588 		$plugin_filter_present  = has_filter( 'auto_update_plugin' );
   2589 		$theme_filter_present   = has_filter( 'auto_update_theme' );
   2590 
   2591 		if ( ( ! $test_plugins_enabled && $ui_enabled_for_plugins )
   2592 			|| ( ! $test_themes_enabled && $ui_enabled_for_themes )
   2593 		) {
   2594 			return (object) array(
   2595 				'status'  => 'critical',
   2596 				'message' => __( 'Auto-updates for plugins and/or themes appear to be disabled, but settings are still set to be displayed. This could cause auto-updates to not work as expected.' ),
   2597 			);
   2598 		}
   2599 
   2600 		if ( ( ! $test_plugins_enabled && $plugin_filter_present )
   2601 			&& ( ! $test_themes_enabled && $theme_filter_present )
   2602 		) {
   2603 			return (object) array(
   2604 				'status'  => 'recommended',
   2605 				'message' => __( 'Auto-updates for plugins and themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
   2606 			);
   2607 		} elseif ( ! $test_plugins_enabled && $plugin_filter_present ) {
   2608 			return (object) array(
   2609 				'status'  => 'recommended',
   2610 				'message' => __( 'Auto-updates for plugins appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
   2611 			);
   2612 		} elseif ( ! $test_themes_enabled && $theme_filter_present ) {
   2613 			return (object) array(
   2614 				'status'  => 'recommended',
   2615 				'message' => __( 'Auto-updates for themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
   2616 			);
   2617 		}
   2618 
   2619 		return (object) array(
   2620 			'status'  => 'good',
   2621 			'message' => __( 'There appear to be no issues with plugin and theme auto-updates.' ),
   2622 		);
   2623 	}
   2624 
   2625 	/**
   2626 	 * Run a loopback test on our site.
   2627 	 *
   2628 	 * Loopbacks are what WordPress uses to communicate with itself to start up WP_Cron, scheduled posts,
   2629 	 * make sure plugin or theme edits don't cause site failures and similar.
   2630 	 *
   2631 	 * @since 5.2.0
   2632 	 *
   2633 	 * @return object The test results.
   2634 	 */
   2635 	function can_perform_loopback() {
   2636 		$body    = array( 'site-health' => 'loopback-test' );
   2637 		$cookies = wp_unslash( $_COOKIE );
   2638 		$timeout = 10;
   2639 		$headers = array(
   2640 			'Cache-Control' => 'no-cache',
   2641 		);
   2642 		/** This filter is documented in wp-includes/class-wp-http-streams.php */
   2643 		$sslverify = apply_filters( 'https_local_ssl_verify', false );
   2644 
   2645 		// Include Basic auth in loopback requests.
   2646 		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
   2647 			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
   2648 		}
   2649 
   2650 		$url = site_url( 'wp-cron.php' );
   2651 
   2652 		/*
   2653 		 * A post request is used for the wp-cron.php loopback test to cause the file
   2654 		 * to finish early without triggering cron jobs. This has two benefits:
   2655 		 * - cron jobs are not triggered a second time on the site health page,
   2656 		 * - the loopback request finishes sooner providing a quicker result.
   2657 		 *
   2658 		 * Using a POST request causes the loopback to differ slightly to the standard
   2659 		 * GET request WordPress uses for wp-cron.php loopback requests but is close
   2660 		 * enough. See https://core.trac.wordpress.org/ticket/52547
   2661 		 */
   2662 		$r = wp_remote_post( $url, compact( 'body', 'cookies', 'headers', 'timeout', 'sslverify' ) );
   2663 
   2664 		if ( is_wp_error( $r ) ) {
   2665 			return (object) array(
   2666 				'status'  => 'critical',
   2667 				'message' => sprintf(
   2668 					'%s<br>%s',
   2669 					__( 'The loopback request to your site failed, this means features relying on them are not currently working as expected.' ),
   2670 					sprintf(
   2671 						/* translators: 1: The WordPress error message. 2: The WordPress error code. */
   2672 						__( 'Error: %1$s (%2$s)' ),
   2673 						$r->get_error_message(),
   2674 						$r->get_error_code()
   2675 					)
   2676 				),
   2677 			);
   2678 		}
   2679 
   2680 		if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
   2681 			return (object) array(
   2682 				'status'  => 'recommended',
   2683 				'message' => sprintf(
   2684 					/* translators: %d: The HTTP response code returned. */
   2685 					__( 'The loopback request returned an unexpected http status code, %d, it was not possible to determine if this will prevent features from working as expected.' ),
   2686 					wp_remote_retrieve_response_code( $r )
   2687 				),
   2688 			);
   2689 		}
   2690 
   2691 		return (object) array(
   2692 			'status'  => 'good',
   2693 			'message' => __( 'The loopback request to your site completed successfully.' ),
   2694 		);
   2695 	}
   2696 
   2697 	/**
   2698 	 * Create a weekly cron event, if one does not already exist.
   2699 	 *
   2700 	 * @since 5.4.0
   2701 	 */
   2702 	public function maybe_create_scheduled_event() {
   2703 		if ( ! wp_next_scheduled( 'wp_site_health_scheduled_check' ) && ! wp_installing() ) {
   2704 			wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'wp_site_health_scheduled_check' );
   2705 		}
   2706 	}
   2707 
   2708 	/**
   2709 	 * Run our scheduled event to check and update the latest site health status for the website.
   2710 	 *
   2711 	 * @since 5.4.0
   2712 	 */
   2713 	public function wp_cron_scheduled_check() {
   2714 		// Bootstrap wp-admin, as WP_Cron doesn't do this for us.
   2715 		require_once trailingslashit( ABSPATH ) . 'wp-admin/includes/admin.php';
   2716 
   2717 		$tests = WP_Site_Health::get_tests();
   2718 
   2719 		$results = array();
   2720 
   2721 		$site_status = array(
   2722 			'good'        => 0,
   2723 			'recommended' => 0,
   2724 			'critical'    => 0,
   2725 		);
   2726 
   2727 		// Don't run https test on development environments.
   2728 		if ( $this->is_development_environment() ) {
   2729 			unset( $tests['async']['https_status'] );
   2730 		}
   2731 
   2732 		foreach ( $tests['direct'] as $test ) {
   2733 			if ( ! empty( $test['skip_cron'] ) ) {
   2734 				continue;
   2735 			}
   2736 
   2737 			if ( is_string( $test['test'] ) ) {
   2738 				$test_function = sprintf(
   2739 					'get_test_%s',
   2740 					$test['test']
   2741 				);
   2742 
   2743 				if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
   2744 					$results[] = $this->perform_test( array( $this, $test_function ) );
   2745 					continue;
   2746 				}
   2747 			}
   2748 
   2749 			if ( is_callable( $test['test'] ) ) {
   2750 				$results[] = $this->perform_test( $test['test'] );
   2751 			}
   2752 		}
   2753 
   2754 		foreach ( $tests['async'] as $test ) {
   2755 			if ( ! empty( $test['skip_cron'] ) ) {
   2756 				continue;
   2757 			}
   2758 
   2759 			// Local endpoints may require authentication, so asynchronous tests can pass a direct test runner as well.
   2760 			if ( ! empty( $test['async_direct_test'] ) && is_callable( $test['async_direct_test'] ) ) {
   2761 				// This test is callable, do so and continue to the next asynchronous check.
   2762 				$results[] = $this->perform_test( $test['async_direct_test'] );
   2763 				continue;
   2764 			}
   2765 
   2766 			if ( is_string( $test['test'] ) ) {
   2767 				// Check if this test has a REST API endpoint.
   2768 				if ( isset( $test['has_rest'] ) && $test['has_rest'] ) {
   2769 					$result_fetch = wp_remote_get(
   2770 						$test['test'],
   2771 						array(
   2772 							'body' => array(
   2773 								'_wpnonce' => wp_create_nonce( 'wp_rest' ),
   2774 							),
   2775 						)
   2776 					);
   2777 				} else {
   2778 					$result_fetch = wp_remote_post(
   2779 						admin_url( 'admin-ajax.php' ),
   2780 						array(
   2781 							'body' => array(
   2782 								'action'   => $test['test'],
   2783 								'_wpnonce' => wp_create_nonce( 'health-check-site-status' ),
   2784 							),
   2785 						)
   2786 					);
   2787 				}
   2788 
   2789 				if ( ! is_wp_error( $result_fetch ) && 200 === wp_remote_retrieve_response_code( $result_fetch ) ) {
   2790 					$result = json_decode( wp_remote_retrieve_body( $result_fetch ), true );
   2791 				} else {
   2792 					$result = false;
   2793 				}
   2794 
   2795 				if ( is_array( $result ) ) {
   2796 					$results[] = $result;
   2797 				} else {
   2798 					$results[] = array(
   2799 						'status' => 'recommended',
   2800 						'label'  => __( 'A test is unavailable' ),
   2801 					);
   2802 				}
   2803 			}
   2804 		}
   2805 
   2806 		foreach ( $results as $result ) {
   2807 			if ( 'critical' === $result['status'] ) {
   2808 				$site_status['critical']++;
   2809 			} elseif ( 'recommended' === $result['status'] ) {
   2810 				$site_status['recommended']++;
   2811 			} else {
   2812 				$site_status['good']++;
   2813 			}
   2814 		}
   2815 
   2816 		set_transient( 'health-check-site-status-result', wp_json_encode( $site_status ) );
   2817 	}
   2818 
   2819 	/**
   2820 	 * Checks if the current environment type is set to 'development' or 'local'.
   2821 	 *
   2822 	 * @since 5.6.0
   2823 	 *
   2824 	 * @return bool True if it is a development environment, false if not.
   2825 	 */
   2826 	public function is_development_environment() {
   2827 		return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
   2828 	}
   2829 
   2830 }