[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-admin/includes/ -> class-wp-site-health.php (source)

   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              'imagick'   => array(
 895                  'extension' => 'imagick',
 896                  'required'  => false,
 897              ),
 898              'json'      => array(
 899                  'function' => 'json_last_error',
 900                  'required' => true,
 901              ),
 902              'mbstring'  => array(
 903                  'function' => 'mb_check_encoding',
 904                  'required' => false,
 905              ),
 906              'mysqli'    => array(
 907                  'function' => 'mysqli_connect',
 908                  'required' => false,
 909              ),
 910              'libsodium' => array(
 911                  'constant'            => 'SODIUM_LIBRARY_VERSION',
 912                  'required'            => false,
 913                  'php_bundled_version' => '7.2.0',
 914              ),
 915              'openssl'   => array(
 916                  'function' => 'openssl_encrypt',
 917                  'required' => false,
 918              ),
 919              'pcre'      => array(
 920                  'function' => 'preg_match',
 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              'intl'      => array(
 945                  'extension' => 'intl',
 946                  'required'  => false,
 947              ),
 948              'mcrypt'    => array(
 949                  'extension'    => 'mcrypt',
 950                  'required'     => false,
 951                  'fallback_for' => 'libsodium',
 952              ),
 953              'simplexml' => array(
 954                  'extension'    => 'simplexml',
 955                  'required'     => false,
 956                  'fallback_for' => 'mod_xml',
 957              ),
 958              'xmlreader' => array(
 959                  'extension'    => 'xmlreader',
 960                  'required'     => false,
 961                  'fallback_for' => 'mod_xml',
 962              ),
 963              'zlib'      => array(
 964                  'extension'    => 'zlib',
 965                  'required'     => false,
 966                  'fallback_for' => 'zip',
 967              ),
 968          );
 969  
 970          /**
 971           * An array representing all the modules we wish to test for.
 972           *
 973           * @since 5.2.0
 974           * @since 5.3.0 The `$constant` and `$class` parameters were added.
 975           *
 976           * @param array $modules {
 977           *     An associative array of modules to test for.
 978           *
 979           *     @type array ...$0 {
 980           *         An associative array of module properties used during testing.
 981           *         One of either `$function` or `$extension` must be provided, or they will fail by default.
 982           *
 983           *         @type string $function     Optional. A function name to test for the existence of.
 984           *         @type string $extension    Optional. An extension to check if is loaded in PHP.
 985           *         @type string $constant     Optional. A constant name to check for to verify an extension exists.
 986           *         @type string $class        Optional. A class name to check for to verify an extension exists.
 987           *         @type bool   $required     Is this a required feature or not.
 988           *         @type string $fallback_for Optional. The module this module replaces as a fallback.
 989           *     }
 990           * }
 991           */
 992          $modules = apply_filters( 'site_status_test_php_modules', $modules );
 993  
 994          $failures = array();
 995  
 996          foreach ( $modules as $library => $module ) {
 997              $extension  = ( isset( $module['extension'] ) ? $module['extension'] : null );
 998              $function   = ( isset( $module['function'] ) ? $module['function'] : null );
 999              $constant   = ( isset( $module['constant'] ) ? $module['constant'] : null );
1000              $class_name = ( isset( $module['class'] ) ? $module['class'] : null );
1001  
1002              // If this module is a fallback for another function, check if that other function passed.
1003              if ( isset( $module['fallback_for'] ) ) {
1004                  /*
1005                   * If that other function has a failure, mark this module as required for usual operations.
1006                   * If that other function hasn't failed, skip this test as it's only a fallback.
1007                   */
1008                  if ( isset( $failures[ $module['fallback_for'] ] ) ) {
1009                      $module['required'] = true;
1010                  } else {
1011                      continue;
1012                  }
1013              }
1014  
1015              if ( ! $this->test_php_extension_availability( $extension, $function, $constant, $class_name ) && ( ! isset( $module['php_bundled_version'] ) || version_compare( PHP_VERSION, $module['php_bundled_version'], '<' ) ) ) {
1016                  if ( $module['required'] ) {
1017                      $result['status'] = 'critical';
1018  
1019                      $class         = 'error';
1020                      $screen_reader = __( 'Error' );
1021                      $message       = sprintf(
1022                          /* translators: %s: The module name. */
1023                          __( 'The required module, %s, is not installed, or has been disabled.' ),
1024                          $library
1025                      );
1026                  } else {
1027                      $class         = 'warning';
1028                      $screen_reader = __( 'Warning' );
1029                      $message       = sprintf(
1030                          /* translators: %s: The module name. */
1031                          __( 'The optional module, %s, is not installed, or has been disabled.' ),
1032                          $library
1033                      );
1034                  }
1035  
1036                  if ( ! $module['required'] && 'good' === $result['status'] ) {
1037                      $result['status'] = 'recommended';
1038                  }
1039  
1040                  $failures[ $library ] = "<span class='dashicons $class'><span class='screen-reader-text'>$screen_reader</span></span> $message";
1041              }
1042          }
1043  
1044          if ( ! empty( $failures ) ) {
1045              $output = '<ul>';
1046  
1047              foreach ( $failures as $failure ) {
1048                  $output .= sprintf(
1049                      '<li>%s</li>',
1050                      $failure
1051                  );
1052              }
1053  
1054              $output .= '</ul>';
1055          }
1056  
1057          if ( 'good' !== $result['status'] ) {
1058              if ( 'recommended' === $result['status'] ) {
1059                  $result['label'] = __( 'One or more recommended modules are missing' );
1060              }
1061              if ( 'critical' === $result['status'] ) {
1062                  $result['label'] = __( 'One or more required modules are missing' );
1063              }
1064  
1065              $result['description'] .= $output;
1066          }
1067  
1068          return $result;
1069      }
1070  
1071      /**
1072       * Test if the PHP default timezone is set to UTC.
1073       *
1074       * @since 5.3.1
1075       *
1076       * @return array The test results.
1077       */
1078  	public function get_test_php_default_timezone() {
1079          $result = array(
1080              'label'       => __( 'PHP default timezone is valid' ),
1081              'status'      => 'good',
1082              'badge'       => array(
1083                  'label' => __( 'Performance' ),
1084                  'color' => 'blue',
1085              ),
1086              'description' => sprintf(
1087                  '<p>%s</p>',
1088                  __( 'PHP default timezone was configured by WordPress on loading. This is necessary for correct calculations of dates and times.' )
1089              ),
1090              'actions'     => '',
1091              'test'        => 'php_default_timezone',
1092          );
1093  
1094          if ( 'UTC' !== date_default_timezone_get() ) {
1095              $result['status'] = 'critical';
1096  
1097              $result['label'] = __( 'PHP default timezone is invalid' );
1098  
1099              $result['description'] = sprintf(
1100                  '<p>%s</p>',
1101                  sprintf(
1102                      /* translators: %s: date_default_timezone_set() */
1103                      __( 'PHP default timezone was changed after WordPress loading by a %s function call. This interferes with correct calculations of dates and times.' ),
1104                      '<code>date_default_timezone_set()</code>'
1105                  )
1106              );
1107          }
1108  
1109          return $result;
1110      }
1111  
1112      /**
1113       * Test if there's an active PHP session that can affect loopback requests.
1114       *
1115       * @since 5.5.0
1116       *
1117       * @return array The test results.
1118       */
1119  	public function get_test_php_sessions() {
1120          $result = array(
1121              'label'       => __( 'No PHP sessions detected' ),
1122              'status'      => 'good',
1123              'badge'       => array(
1124                  'label' => __( 'Performance' ),
1125                  'color' => 'blue',
1126              ),
1127              'description' => sprintf(
1128                  '<p>%s</p>',
1129                  sprintf(
1130                      /* translators: 1: session_start(), 2: session_write_close() */
1131                      __( '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.' ),
1132                      '<code>session_start()</code>',
1133                      '<code>session_write_close()</code>'
1134                  )
1135              ),
1136              'test'        => 'php_sessions',
1137          );
1138  
1139          if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) {
1140              $result['status'] = 'critical';
1141  
1142              $result['label'] = __( 'An active PHP session was detected' );
1143  
1144              $result['description'] = sprintf(
1145                  '<p>%s</p>',
1146                  sprintf(
1147                      /* translators: 1: session_start(), 2: session_write_close() */
1148                      __( '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.' ),
1149                      '<code>session_start()</code>',
1150                      '<code>session_write_close()</code>'
1151                  )
1152              );
1153          }
1154  
1155          return $result;
1156      }
1157  
1158      /**
1159       * Test if the SQL server is up to date.
1160       *
1161       * @since 5.2.0
1162       *
1163       * @return array The test results.
1164       */
1165  	public function get_test_sql_server() {
1166          if ( ! $this->mysql_server_version ) {
1167              $this->prepare_sql_data();
1168          }
1169  
1170          $result = array(
1171              'label'       => __( 'SQL server is up to date' ),
1172              'status'      => 'good',
1173              'badge'       => array(
1174                  'label' => __( 'Performance' ),
1175                  'color' => 'blue',
1176              ),
1177              'description' => sprintf(
1178                  '<p>%s</p>',
1179                  __( 'The SQL server is a required piece of software for the database WordPress uses to store all your site&#8217;s content and settings.' )
1180              ),
1181              'actions'     => sprintf(
1182                  '<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>',
1183                  /* translators: Localized version of WordPress requirements if one exists. */
1184                  esc_url( __( 'https://wordpress.org/about/requirements/' ) ),
1185                  __( 'Learn more about what WordPress requires to run.' ),
1186                  /* translators: Accessibility text. */
1187                  __( '(opens in a new tab)' )
1188              ),
1189              'test'        => 'sql_server',
1190          );
1191  
1192          $db_dropin = file_exists( WP_CONTENT_DIR . '/db.php' );
1193  
1194          if ( ! $this->mysql_rec_version_check ) {
1195              $result['status'] = 'recommended';
1196  
1197              $result['label'] = __( 'Outdated SQL server' );
1198  
1199              $result['description'] .= sprintf(
1200                  '<p>%s</p>',
1201                  sprintf(
1202                      /* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server recommended version number. */
1203                      __( 'For optimal performance and security reasons, we recommend running %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
1204                      ( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
1205                      $this->health_check_mysql_rec_version
1206                  )
1207              );
1208          }
1209  
1210          if ( ! $this->mysql_min_version_check ) {
1211              $result['status'] = 'critical';
1212  
1213              $result['label']          = __( 'Severely outdated SQL server' );
1214              $result['badge']['label'] = __( 'Security' );
1215  
1216              $result['description'] .= sprintf(
1217                  '<p>%s</p>',
1218                  sprintf(
1219                      /* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server minimum version number. */
1220                      __( 'WordPress requires %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
1221                      ( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
1222                      $this->health_check_mysql_required_version
1223                  )
1224              );
1225          }
1226  
1227          if ( $db_dropin ) {
1228              $result['description'] .= sprintf(
1229                  '<p>%s</p>',
1230                  wp_kses(
1231                      sprintf(
1232                          /* translators: 1: The name of the drop-in. 2: The name of the database engine. */
1233                          __( 'You are using a %1$s drop-in which might mean that a %2$s database is not being used.' ),
1234                          '<code>wp-content/db.php</code>',
1235                          ( $this->is_mariadb ? 'MariaDB' : 'MySQL' )
1236                      ),
1237                      array(
1238                          'code' => true,
1239                      )
1240                  )
1241              );
1242          }
1243  
1244          return $result;
1245      }
1246  
1247      /**
1248       * Test if the database server is capable of using utf8mb4.
1249       *
1250       * @since 5.2.0
1251       *
1252       * @return array The test results.
1253       */
1254  	public function get_test_utf8mb4_support() {
1255          global $wpdb;
1256  
1257          if ( ! $this->mysql_server_version ) {
1258              $this->prepare_sql_data();
1259          }
1260  
1261          $result = array(
1262              'label'       => __( 'UTF8MB4 is supported' ),
1263              'status'      => 'good',
1264              'badge'       => array(
1265                  'label' => __( 'Performance' ),
1266                  'color' => 'blue',
1267              ),
1268              'description' => sprintf(
1269                  '<p>%s</p>',
1270                  __( '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.' )
1271              ),
1272              'actions'     => '',
1273              'test'        => 'utf8mb4_support',
1274          );
1275  
1276          if ( ! $this->is_mariadb ) {
1277              if ( version_compare( $this->mysql_server_version, '5.5.3', '<' ) ) {
1278                  $result['status'] = 'recommended';
1279  
1280                  $result['label'] = __( 'utf8mb4 requires a MySQL update' );
1281  
1282                  $result['description'] .= sprintf(
1283                      '<p>%s</p>',
1284                      sprintf(
1285                          /* translators: %s: Version number. */
1286                          __( 'WordPress&#8217; utf8mb4 support requires MySQL version %s or greater. Please contact your server administrator.' ),
1287                          '5.5.3'
1288                      )
1289                  );
1290              } else {
1291                  $result['description'] .= sprintf(
1292                      '<p>%s</p>',
1293                      __( 'Your MySQL version supports utf8mb4.' )
1294                  );
1295              }
1296          } else { // MariaDB introduced utf8mb4 support in 5.5.0.
1297              if ( version_compare( $this->mysql_server_version, '5.5.0', '<' ) ) {
1298                  $result['status'] = 'recommended';
1299  
1300                  $result['label'] = __( 'utf8mb4 requires a MariaDB update' );
1301  
1302                  $result['description'] .= sprintf(
1303                      '<p>%s</p>',
1304                      sprintf(
1305                          /* translators: %s: Version number. */
1306                          __( 'WordPress&#8217; utf8mb4 support requires MariaDB version %s or greater. Please contact your server administrator.' ),
1307                          '5.5.0'
1308                      )
1309                  );
1310              } else {
1311                  $result['description'] .= sprintf(
1312                      '<p>%s</p>',
1313                      __( 'Your MariaDB version supports utf8mb4.' )
1314                  );
1315              }
1316          }
1317  
1318          if ( $wpdb->use_mysqli ) {
1319              // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_client_info
1320              $mysql_client_version = mysqli_get_client_info();
1321          } else {
1322              // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_get_client_info,PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
1323              $mysql_client_version = mysql_get_client_info();
1324          }
1325  
1326          /*
1327           * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
1328           * mysqlnd has supported utf8mb4 since 5.0.9.
1329           */
1330          if ( false !== strpos( $mysql_client_version, 'mysqlnd' ) ) {
1331              $mysql_client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $mysql_client_version );
1332              if ( version_compare( $mysql_client_version, '5.0.9', '<' ) ) {
1333                  $result['status'] = 'recommended';
1334  
1335                  $result['label'] = __( 'utf8mb4 requires a newer client library' );
1336  
1337                  $result['description'] .= sprintf(
1338                      '<p>%s</p>',
1339                      sprintf(
1340                          /* translators: 1: Name of the library, 2: Number of version. */
1341                          __( 'WordPress&#8217; utf8mb4 support requires MySQL client library (%1$s) version %2$s or newer. Please contact your server administrator.' ),
1342                          'mysqlnd',
1343                          '5.0.9'
1344                      )
1345                  );
1346              }
1347          } else {
1348              if ( version_compare( $mysql_client_version, '5.5.3', '<' ) ) {
1349                  $result['status'] = 'recommended';
1350  
1351                  $result['label'] = __( 'utf8mb4 requires a newer client library' );
1352  
1353                  $result['description'] .= sprintf(
1354                      '<p>%s</p>',
1355                      sprintf(
1356                          /* translators: 1: Name of the library, 2: Number of version. */
1357                          __( 'WordPress&#8217; utf8mb4 support requires MySQL client library (%1$s) version %2$s or newer. Please contact your server administrator.' ),
1358                          'libmysql',
1359                          '5.5.3'
1360                      )
1361                  );
1362              }
1363          }
1364  
1365          return $result;
1366      }
1367  
1368      /**
1369       * Test if the site can communicate with WordPress.org.
1370       *
1371       * @since 5.2.0
1372       *
1373       * @return array The test results.
1374       */
1375  	public function get_test_dotorg_communication() {
1376          $result = array(
1377              'label'       => __( 'Can communicate with WordPress.org' ),
1378              'status'      => '',
1379              'badge'       => array(
1380                  'label' => __( 'Security' ),
1381                  'color' => 'blue',
1382              ),
1383              'description' => sprintf(
1384                  '<p>%s</p>',
1385                  __( 'Communicating with the WordPress servers is used to check for new versions, and to both install and update WordPress core, themes or plugins.' )
1386              ),
1387              'actions'     => '',
1388              'test'        => 'dotorg_communication',
1389          );
1390  
1391          $wp_dotorg = wp_remote_get(
1392              'https://api.wordpress.org',
1393              array(
1394                  'timeout' => 10,
1395              )
1396          );
1397          if ( ! is_wp_error( $wp_dotorg ) ) {
1398              $result['status'] = 'good';
1399          } else {
1400              $result['status'] = 'critical';
1401  
1402              $result['label'] = __( 'Could not reach WordPress.org' );
1403  
1404              $result['description'] .= sprintf(
1405                  '<p>%s</p>',
1406                  sprintf(
1407                      '<span class="error"><span class="screen-reader-text">%s</span></span> %s',
1408                      __( 'Error' ),
1409                      sprintf(
1410                          /* translators: 1: The IP address WordPress.org resolves to. 2: The error returned by the lookup. */
1411                          __( 'Your site is unable to reach WordPress.org at %1$s, and returned the error: %2$s' ),
1412                          gethostbyname( 'api.wordpress.org' ),
1413                          $wp_dotorg->get_error_message()
1414                      )
1415                  )
1416              );
1417  
1418              $result['actions'] = sprintf(
1419                  '<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>',
1420                  /* translators: Localized Support reference. */
1421                  esc_url( __( 'https://wordpress.org/support' ) ),
1422                  __( 'Get help resolving this issue.' ),
1423                  /* translators: Accessibility text. */
1424                  __( '(opens in a new tab)' )
1425              );
1426          }
1427  
1428          return $result;
1429      }
1430  
1431      /**
1432       * Test if debug information is enabled.
1433       *
1434       * When WP_DEBUG is enabled, errors and information may be disclosed to site visitors,
1435       * or logged to a publicly accessible file.
1436       *
1437       * Debugging is also frequently left enabled after looking for errors on a site,
1438       * as site owners do not understand the implications of this.
1439       *
1440       * @since 5.2.0
1441       *
1442       * @return array The test results.
1443       */
1444  	public function get_test_is_in_debug_mode() {
1445          $result = array(
1446              'label'       => __( 'Your site is not set to output debug information' ),
1447              'status'      => 'good',
1448              'badge'       => array(
1449                  'label' => __( 'Security' ),
1450                  'color' => 'blue',
1451              ),
1452              'description' => sprintf(
1453                  '<p>%s</p>',
1454                  __( '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.' )
1455              ),
1456              'actions'     => sprintf(
1457                  '<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>',
1458                  /* translators: Documentation explaining debugging in WordPress. */
1459                  esc_url( __( 'https://wordpress.org/support/article/debugging-in-wordpress/' ) ),
1460                  __( 'Learn more about debugging in WordPress.' ),
1461                  /* translators: Accessibility text. */
1462                  __( '(opens in a new tab)' )
1463              ),
1464              'test'        => 'is_in_debug_mode',
1465          );
1466  
1467          if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
1468              if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
1469                  $result['label'] = __( 'Your site is set to log errors to a potentially public file' );
1470  
1471                  $result['status'] = ( 0 === strpos( ini_get( 'error_log' ), ABSPATH ) ) ? 'critical' : 'recommended';
1472  
1473                  $result['description'] .= sprintf(
1474                      '<p>%s</p>',
1475                      sprintf(
1476                          /* translators: %s: WP_DEBUG_LOG */
1477                          __( '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.' ),
1478                          '<code>WP_DEBUG_LOG</code>'
1479                      )
1480                  );
1481              }
1482  
1483              if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
1484                  $result['label'] = __( 'Your site is set to display errors to site visitors' );
1485  
1486                  $result['status'] = 'critical';
1487  
1488                  // On development environments, set the status to recommended.
1489                  if ( $this->is_development_environment() ) {
1490                      $result['status'] = 'recommended';
1491                  }
1492  
1493                  $result['description'] .= sprintf(
1494                      '<p>%s</p>',
1495                      sprintf(
1496                          /* translators: 1: WP_DEBUG_DISPLAY, 2: WP_DEBUG */
1497                          __( '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.' ),
1498                          '<code>WP_DEBUG_DISPLAY</code>',
1499                          '<code>WP_DEBUG</code>'
1500                      )
1501                  );
1502              }
1503          }
1504  
1505          return $result;
1506      }
1507  
1508      /**
1509       * Test if your site is serving content over HTTPS.
1510       *
1511       * Many sites have varying degrees of HTTPS support, the most common of which is sites that have it
1512       * enabled, but only if you visit the right site address.
1513       *
1514       * @since 5.2.0
1515       * @since 5.7.0 Updated to rely on {@see wp_is_using_https()} and {@see wp_is_https_supported()}.
1516       *
1517       * @return array The test results.
1518       */
1519  	public function get_test_https_status() {
1520          // Enforce fresh HTTPS detection results. This is normally invoked by using cron,
1521          // but for Site Health it should always rely on the latest results.
1522          wp_update_https_detection_errors();
1523  
1524          $default_update_url = wp_get_default_update_https_url();
1525  
1526          $result = array(
1527              'label'       => __( 'Your website is using an active HTTPS connection' ),
1528              'status'      => 'good',
1529              'badge'       => array(
1530                  'label' => __( 'Security' ),
1531                  'color' => 'blue',
1532              ),
1533              'description' => sprintf(
1534                  '<p>%s</p>',
1535                  __( '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.' )
1536              ),
1537              'actions'     => sprintf(
1538                  '<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>',
1539                  esc_url( $default_update_url ),
1540                  __( 'Learn more about why you should use HTTPS' ),
1541                  /* translators: Accessibility text. */
1542                  __( '(opens in a new tab)' )
1543              ),
1544              'test'        => 'https_status',
1545          );
1546  
1547          if ( ! wp_is_using_https() ) {
1548              // If the website is not using HTTPS, provide more information
1549              // about whether it is supported and how it can be enabled.
1550              $result['status'] = 'recommended';
1551              $result['label']  = __( 'Your website does not use HTTPS' );
1552  
1553              if ( wp_is_site_url_using_https() ) {
1554                  if ( is_ssl() ) {
1555                      $result['description'] = sprintf(
1556                          '<p>%s</p>',
1557                          sprintf(
1558                              /* translators: %s: URL to Settings > General > Site Address. */
1559                              __( 'You are accessing this website using HTTPS, but your <a href="%s">Site Address</a> is not set up to use HTTPS by default.' ),
1560                              esc_url( admin_url( 'options-general.php' ) . '#home' )
1561                          )
1562                      );
1563                  } else {
1564                      $result['description'] = sprintf(
1565                          '<p>%s</p>',
1566                          sprintf(
1567                              /* translators: %s: URL to Settings > General > Site Address. */
1568                              __( 'Your <a href="%s">Site Address</a> is not set up to use HTTPS.' ),
1569                              esc_url( admin_url( 'options-general.php' ) . '#home' )
1570                          )
1571                      );
1572                  }
1573              } else {
1574                  if ( is_ssl() ) {
1575                      $result['description'] = sprintf(
1576                          '<p>%s</p>',
1577                          sprintf(
1578                              /* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
1579                              __( '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.' ),
1580                              esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
1581                              esc_url( admin_url( 'options-general.php' ) . '#home' )
1582                          )
1583                      );
1584                  } else {
1585                      $result['description'] = sprintf(
1586                          '<p>%s</p>',
1587                          sprintf(
1588                              /* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
1589                              __( 'Your <a href="%1$s">WordPress Address</a> and <a href="%2$s">Site Address</a> are not set up to use HTTPS.' ),
1590                              esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
1591                              esc_url( admin_url( 'options-general.php' ) . '#home' )
1592                          )
1593                      );
1594                  }
1595              }
1596  
1597              if ( wp_is_https_supported() ) {
1598                  $result['description'] .= sprintf(
1599                      '<p>%s</p>',
1600                      __( 'HTTPS is already supported for your website.' )
1601                  );
1602  
1603                  if ( defined( 'WP_HOME' ) || defined( 'WP_SITEURL' ) ) {
1604                      $result['description'] .= sprintf(
1605                          '<p>%s</p>',
1606                          sprintf(
1607                              /* translators: 1: wp-config.php, 2: WP_HOME, 3: WP_SITEURL */
1608                              __( '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.' ),
1609                              '<code>wp-config.php</code>',
1610                              '<code>WP_HOME</code>',
1611                              '<code>WP_SITEURL</code>'
1612                          )
1613                      );
1614                  } elseif ( current_user_can( 'update_https' ) ) {
1615                      $default_direct_update_url = add_query_arg( 'action', 'update_https', wp_nonce_url( admin_url( 'site-health.php' ), 'wp_update_https' ) );
1616                      $direct_update_url         = wp_get_direct_update_https_url();
1617  
1618                      if ( ! empty( $direct_update_url ) ) {
1619                          $result['actions'] = sprintf(
1620                              '<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>',
1621                              esc_url( $direct_update_url ),
1622                              __( 'Update your site to use HTTPS' ),
1623                              /* translators: Accessibility text. */
1624                              __( '(opens in a new tab)' )
1625                          );
1626                      } else {
1627                          $result['actions'] = sprintf(
1628                              '<p class="button-container"><a class="button button-primary" href="%1$s">%2$s</a></p>',
1629                              esc_url( $default_direct_update_url ),
1630                              __( 'Update your site to use HTTPS' )
1631                          );
1632                      }
1633                  }
1634              } else {
1635                  // If host-specific "Update HTTPS" URL is provided, include a link.
1636                  $update_url = wp_get_update_https_url();
1637                  if ( $update_url !== $default_update_url ) {
1638                      $result['description'] .= sprintf(
1639                          '<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>',
1640                          esc_url( $update_url ),
1641                          __( 'Talk to your web host about supporting HTTPS for your website.' ),
1642                          /* translators: Accessibility text. */
1643                          __( '(opens in a new tab)' )
1644                      );
1645                  } else {
1646                      $result['description'] .= sprintf(
1647                          '<p>%s</p>',
1648                          __( 'Talk to your web host about supporting HTTPS for your website.' )
1649                      );
1650                  }
1651              }
1652          }
1653  
1654          return $result;
1655      }
1656  
1657      /**
1658       * Check if the HTTP API can handle SSL/TLS requests.
1659       *
1660       * @since 5.2.0
1661       *
1662       * @return array The test results.
1663       */
1664  	public function get_test_ssl_support() {
1665          $result = array(
1666              'label'       => '',
1667              'status'      => '',
1668              'badge'       => array(
1669                  'label' => __( 'Security' ),
1670                  'color' => 'blue',
1671              ),
1672              'description' => sprintf(
1673                  '<p>%s</p>',
1674                  __( 'Securely communicating between servers are needed for transactions such as fetching files, conducting sales on store sites, and much more.' )
1675              ),
1676              'actions'     => '',
1677              'test'        => 'ssl_support',
1678          );
1679  
1680          $supports_https = wp_http_supports( array( 'ssl' ) );
1681  
1682          if ( $supports_https ) {
1683              $result['status'] = 'good';
1684  
1685              $result['label'] = __( 'Your site can communicate securely with other services' );
1686          } else {
1687              $result['status'] = 'critical';
1688  
1689              $result['label'] = __( 'Your site is unable to communicate securely with other services' );
1690  
1691              $result['description'] .= sprintf(
1692                  '<p>%s</p>',
1693                  __( 'Talk to your web host about OpenSSL support for PHP.' )
1694              );
1695          }
1696  
1697          return $result;
1698      }
1699  
1700      /**
1701       * Test if scheduled events run as intended.
1702       *
1703       * If scheduled events are not running, this may indicate something with WP_Cron is not working
1704       * as intended, or that there are orphaned events hanging around from older code.
1705       *
1706       * @since 5.2.0
1707       *
1708       * @return array The test results.
1709       */
1710  	public function get_test_scheduled_events() {
1711          $result = array(
1712              'label'       => __( 'Scheduled events are running' ),
1713              'status'      => 'good',
1714              'badge'       => array(
1715                  'label' => __( 'Performance' ),
1716                  'color' => 'blue',
1717              ),
1718              'description' => sprintf(
1719                  '<p>%s</p>',
1720                  __( '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.' )
1721              ),
1722              'actions'     => '',
1723              'test'        => 'scheduled_events',
1724          );
1725  
1726          $this->wp_schedule_test_init();
1727  
1728          if ( is_wp_error( $this->has_missed_cron() ) ) {
1729              $result['status'] = 'critical';
1730  
1731              $result['label'] = __( 'It was not possible to check your scheduled events' );
1732  
1733              $result['description'] = sprintf(
1734                  '<p>%s</p>',
1735                  sprintf(
1736                      /* translators: %s: The error message returned while from the cron scheduler. */
1737                      __( 'While trying to test your site&#8217;s scheduled events, the following error was returned: %s' ),
1738                      $this->has_missed_cron()->get_error_message()
1739                  )
1740              );
1741          } elseif ( $this->has_missed_cron() ) {
1742              $result['status'] = 'recommended';
1743  
1744              $result['label'] = __( 'A scheduled event has failed' );
1745  
1746              $result['description'] = sprintf(
1747                  '<p>%s</p>',
1748                  sprintf(
1749                      /* translators: %s: The name of the failed cron event. */
1750                      __( '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.' ),
1751                      $this->last_missed_cron
1752                  )
1753              );
1754          } elseif ( $this->has_late_cron() ) {
1755              $result['status'] = 'recommended';
1756  
1757              $result['label'] = __( 'A scheduled event is late' );
1758  
1759              $result['description'] = sprintf(
1760                  '<p>%s</p>',
1761                  sprintf(
1762                      /* translators: %s: The name of the late cron event. */
1763                      __( '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.' ),
1764                      $this->last_late_cron
1765                  )
1766              );
1767          }
1768  
1769          return $result;
1770      }
1771  
1772      /**
1773       * Test if WordPress can run automated background updates.
1774       *
1775       * Background updates in WordPress are primarily used for minor releases and security updates.
1776       * It's important to either have these working, or be aware that they are intentionally disabled
1777       * for whatever reason.
1778       *
1779       * @since 5.2.0
1780       *
1781       * @return array The test results.
1782       */
1783  	public function get_test_background_updates() {
1784          $result = array(
1785              'label'       => __( 'Background updates are working' ),
1786              'status'      => 'good',
1787              'badge'       => array(
1788                  'label' => __( 'Security' ),
1789                  'color' => 'blue',
1790              ),
1791              'description' => sprintf(
1792                  '<p>%s</p>',
1793                  __( 'Background updates ensure that WordPress can auto-update if a security update is released for the version you are currently using.' )
1794              ),
1795              'actions'     => '',
1796              'test'        => 'background_updates',
1797          );
1798  
1799          if ( ! class_exists( 'WP_Site_Health_Auto_Updates' ) ) {
1800              require_once ABSPATH . 'wp-admin/includes/class-wp-site-health-auto-updates.php';
1801          }
1802  
1803          // Run the auto-update tests in a separate class,
1804          // as there are many considerations to be made.
1805          $automatic_updates = new WP_Site_Health_Auto_Updates();
1806          $tests             = $automatic_updates->run_tests();
1807  
1808          $output = '<ul>';
1809  
1810          foreach ( $tests as $test ) {
1811              $severity_string = __( 'Passed' );
1812  
1813              if ( 'fail' === $test->severity ) {
1814                  $result['label'] = __( 'Background updates are not working as expected' );
1815  
1816                  $result['status'] = 'critical';
1817  
1818                  $severity_string = __( 'Error' );
1819              }
1820  
1821              if ( 'warning' === $test->severity && 'good' === $result['status'] ) {
1822                  $result['label'] = __( 'Background updates may not be working properly' );
1823  
1824                  $result['status'] = 'recommended';
1825  
1826                  $severity_string = __( 'Warning' );
1827              }
1828  
1829              $output .= sprintf(
1830                  '<li><span class="dashicons %s"><span class="screen-reader-text">%s</span></span> %s</li>',
1831                  esc_attr( $test->severity ),
1832                  $severity_string,
1833                  $test->description
1834              );
1835          }
1836  
1837          $output .= '</ul>';
1838  
1839          if ( 'good' !== $result['status'] ) {
1840              $result['description'] .= $output;
1841          }
1842  
1843          return $result;
1844      }
1845  
1846      /**
1847       * Test if plugin and theme auto-updates appear to be configured correctly.
1848       *
1849       * @since 5.5.0
1850       *
1851       * @return array The test results.
1852       */
1853  	public function get_test_plugin_theme_auto_updates() {
1854          $result = array(
1855              'label'       => __( 'Plugin and theme auto-updates appear to be configured correctly' ),
1856              'status'      => 'good',
1857              'badge'       => array(
1858                  'label' => __( 'Security' ),
1859                  'color' => 'blue',
1860              ),
1861              'description' => sprintf(
1862                  '<p>%s</p>',
1863                  __( 'Plugin and theme auto-updates ensure that the latest versions are always installed.' )
1864              ),
1865              'actions'     => '',
1866              'test'        => 'plugin_theme_auto_updates',
1867          );
1868  
1869          $check_plugin_theme_updates = $this->detect_plugin_theme_auto_update_issues();
1870  
1871          $result['status'] = $check_plugin_theme_updates->status;
1872  
1873          if ( 'good' !== $result['status'] ) {
1874              $result['label'] = __( 'Your site may have problems auto-updating plugins and themes' );
1875  
1876              $result['description'] .= sprintf(
1877                  '<p>%s</p>',
1878                  $check_plugin_theme_updates->message
1879              );
1880          }
1881  
1882          return $result;
1883      }
1884  
1885      /**
1886       * Test available disk space for updates.
1887       *
1888       * @since 5.9.0
1889       *
1890       * @return array The test results.
1891       */
1892  	public function get_test_available_updates_disk_space() {
1893          $available_space       = function_exists( 'disk_free_space' ) ? (int) @disk_free_space( WP_CONTENT_DIR . '/upgrade/' ) : 0;
1894          $available_space_in_mb = $available_space / MB_IN_BYTES;
1895  
1896          $result = array(
1897              'label'       => __( 'Disk space available to safely perform updates' ),
1898              'status'      => 'good',
1899              'badge'       => array(
1900                  'label' => __( 'Security' ),
1901                  'color' => 'blue',
1902              ),
1903              'description' => sprintf(
1904                  /* translators: %s: Available disk space in MB or GB. */
1905                  '<p>' . __( '%s available disk space was detected, update routines can be performed safely.' ),
1906                  size_format( $available_space )
1907              ),
1908              'actions'     => '',
1909              'test'        => 'available_updates_disk_space',
1910          );
1911  
1912          if ( $available_space_in_mb < 100 ) {
1913              $result['description'] = __( 'Available disk space is low, less than 100 MB available.' );
1914              $result['status']      = 'recommended';
1915          }
1916  
1917          if ( $available_space_in_mb < 20 ) {
1918              $result['description'] = __( 'Available disk space is critically low, less than 20 MB available. Proceed with caution, updates may fail.' );
1919              $result['status']      = 'critical';
1920          }
1921  
1922          if ( ! $available_space ) {
1923              $result['description'] = __( 'Could not determine available disk space for updates.' );
1924              $result['status']      = 'recommended';
1925          }
1926  
1927          return $result;
1928      }
1929  
1930      /**
1931       * Test if plugin and theme updates temp-backup directories are writable or can be created.
1932       *
1933       * @since 5.9.0
1934       *
1935       * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
1936       *
1937       * @return array The test results.
1938       */
1939  	public function get_test_update_temp_backup_writable() {
1940          global $wp_filesystem;
1941  
1942          $result = array(
1943              'label'       => sprintf(
1944                  /* translators: %s: temp-backup */
1945                  __( 'Plugin and theme update %s directory is writable' ),
1946                  'temp-backup'
1947              ),
1948              'status'      => 'good',
1949              'badge'       => array(
1950                  'label' => __( 'Security' ),
1951                  'color' => 'blue',
1952              ),
1953              'description' => sprintf(
1954                  /* translators: %s: wp-content/upgrade/temp-backup */
1955                  '<p>' . __( 'The %s directory used to improve the stability of plugin and theme updates is writable.' ),
1956                  '<code>wp-content/upgrade/temp-backup</code>'
1957              ),
1958              'actions'     => '',
1959              'test'        => 'update_temp_backup_writable',
1960          );
1961  
1962          if ( ! $wp_filesystem ) {
1963              if ( ! function_exists( 'WP_Filesystem' ) ) {
1964                  require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
1965              }
1966              WP_Filesystem();
1967          }
1968          $wp_content = $wp_filesystem->wp_content_dir();
1969  
1970          $upgrade_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade" );
1971          $upgrade_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade" );
1972          $backup_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup" );
1973          $backup_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup" );
1974  
1975          $plugins_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup/plugins" );
1976          $plugins_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/plugins" );
1977          $themes_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade/temp-backup/themes" );
1978          $themes_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade/temp-backup/themes" );
1979  
1980          if ( $plugins_dir_exists && ! $plugins_dir_is_writable && $themes_dir_exists && ! $themes_dir_is_writable ) {
1981              $result['status']      = 'critical';
1982              $result['label']       = sprintf(
1983                  /* translators: %s: temp-backup */
1984                  __( 'Plugins and themes %s directories exist but are not writable' ),
1985                  'temp-backup'
1986              );
1987              $result['description'] = sprintf(
1988                  /* translators: 1: wp-content/upgrade/temp-backup/plugins, 2: wp-content/upgrade/temp-backup/themes. */
1989                  '<p>' . __( 'The %1$s and %2$s directories exist but are not writable. These directories are used to improve the stability of plugin updates. Please make sure the server has write permissions to these directories.' ) . '</p>',
1990                  '<code>wp-content/upgrade/temp-backup/plugins</code>',
1991                  '<code>wp-content/upgrade/temp-backup/themes</code>'
1992              );
1993              return $result;
1994          }
1995  
1996          if ( $plugins_dir_exists && ! $plugins_dir_is_writable ) {
1997              $result['status']      = 'critical';
1998              $result['label']       = sprintf(
1999                  /* translators: %s: temp-backup */
2000                  __( 'Plugins %s directory exists but is not writable' ),
2001                  'temp-backup'
2002              );
2003              $result['description'] = sprintf(
2004                  /* translators: %s: wp-content/upgrade/temp-backup/plugins */
2005                  '<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
2006                  '<code>wp-content/upgrade/temp-backup/plugins</code>'
2007              );
2008              return $result;
2009          }
2010  
2011          if ( $themes_dir_exists && ! $themes_dir_is_writable ) {
2012              $result['status']      = 'critical';
2013              $result['label']       = sprintf(
2014                  /* translators: %s: temp-backup */
2015                  __( 'Themes %s directory exists but is not writable' ),
2016                  'temp-backup'
2017              );
2018              $result['description'] = sprintf(
2019                  /* translators: %s: wp-content/upgrade/temp-backup/themes */
2020                  '<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
2021                  '<code>wp-content/upgrade/temp-backup/themes</code>'
2022              );
2023              return $result;
2024          }
2025  
2026          if ( ( ! $plugins_dir_exists || ! $themes_dir_exists ) && $backup_dir_exists && ! $backup_dir_is_writable ) {
2027              $result['status']      = 'critical';
2028              $result['label']       = sprintf(
2029                  /* translators: %s: temp-backup */
2030                  __( 'The %s directory exists but is not writable' ),
2031                  'temp-backup'
2032              );
2033              $result['description'] = sprintf(
2034                  /* translators: %s: wp-content/upgrade/temp-backup */
2035                  '<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
2036                  '<code>wp-content/upgrade/temp-backup</code>'
2037              );
2038              return $result;
2039          }
2040  
2041          if ( ! $backup_dir_exists && $upgrade_dir_exists && ! $upgrade_dir_is_writable ) {
2042              $result['status']      = 'critical';
2043              $result['label']       = sprintf(
2044                  /* translators: %s: upgrade */
2045                  __( 'The %s directory exists but is not writable' ),
2046                  'upgrade'
2047              );
2048              $result['description'] = sprintf(
2049                  /* translators: %s: wp-content/upgrade */
2050                  '<p>' . __( 'The %s directory exists but is not writable. This directory is used for plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
2051                  '<code>wp-content/upgrade</code>'
2052              );
2053              return $result;
2054          }
2055  
2056          if ( ! $upgrade_dir_exists && ! $wp_filesystem->is_writable( $wp_content ) ) {
2057              $result['status']      = 'critical';
2058              $result['label']       = sprintf(
2059                  /* translators: %s: upgrade */
2060                  __( 'The %s directory cannot be created' ),
2061                  'upgrade'
2062              );
2063              $result['description'] = sprintf(
2064                  /* translators: 1: wp-content/upgrade, 2: wp-content. */
2065                  '<p>' . __( 'The %1$s directory does not exist, and the server does not have write permissions in %2$s to create it. This directory is used for plugin and theme updates. Please make sure the server has write permissions in %2$s.' ) . '</p>',
2066                  '<code>wp-content/upgrade</code>',
2067                  '<code>wp-content</code>'
2068              );
2069              return $result;
2070          }
2071  
2072          return $result;
2073      }
2074  
2075      /**
2076       * Test if loopbacks work as expected.
2077       *
2078       * A loopback is when WordPress queries itself, for example to start a new WP_Cron instance,
2079       * or when editing a plugin or theme. This has shown itself to be a recurring issue,
2080       * as code can very easily break this interaction.
2081       *
2082       * @since 5.2.0
2083       *
2084       * @return array The test results.
2085       */
2086  	public function get_test_loopback_requests() {
2087          $result = array(
2088              'label'       => __( 'Your site can perform loopback requests' ),
2089              'status'      => 'good',
2090              'badge'       => array(
2091                  'label' => __( 'Performance' ),
2092                  'color' => 'blue',
2093              ),
2094              'description' => sprintf(
2095                  '<p>%s</p>',
2096                  __( '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.' )
2097              ),
2098              'actions'     => '',
2099              'test'        => 'loopback_requests',
2100          );
2101  
2102          $check_loopback = $this->can_perform_loopback();
2103  
2104          $result['status'] = $check_loopback->status;
2105  
2106          if ( 'good' !== $result['status'] ) {
2107              $result['label'] = __( 'Your site could not complete a loopback request' );
2108  
2109              $result['description'] .= sprintf(
2110                  '<p>%s</p>',
2111                  $check_loopback->message
2112              );
2113          }
2114  
2115          return $result;
2116      }
2117  
2118      /**
2119       * Test if HTTP requests are blocked.
2120       *
2121       * It's possible to block all outgoing communication (with the possibility of allowing certain
2122       * hosts) via the HTTP API. This may create problems for users as many features are running as
2123       * services these days.
2124       *
2125       * @since 5.2.0
2126       *
2127       * @return array The test results.
2128       */
2129  	public function get_test_http_requests() {
2130          $result = array(
2131              'label'       => __( 'HTTP requests seem to be working as expected' ),
2132              'status'      => 'good',
2133              'badge'       => array(
2134                  'label' => __( 'Performance' ),
2135                  'color' => 'blue',
2136              ),
2137              'description' => sprintf(
2138                  '<p>%s</p>',
2139                  __( '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.' )
2140              ),
2141              'actions'     => '',
2142              'test'        => 'http_requests',
2143          );
2144  
2145          $blocked = false;
2146          $hosts   = array();
2147  
2148          if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) {
2149              $blocked = true;
2150          }
2151  
2152          if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
2153              $hosts = explode( ',', WP_ACCESSIBLE_HOSTS );
2154          }
2155  
2156          if ( $blocked && 0 === count( $hosts ) ) {
2157              $result['status'] = 'critical';
2158  
2159              $result['label'] = __( 'HTTP requests are blocked' );
2160  
2161              $result['description'] .= sprintf(
2162                  '<p>%s</p>',
2163                  sprintf(
2164                      /* translators: %s: Name of the constant used. */
2165                      __( 'HTTP requests have been blocked by the %s constant, with no allowed hosts.' ),
2166                      '<code>WP_HTTP_BLOCK_EXTERNAL</code>'
2167                  )
2168              );
2169          }
2170  
2171          if ( $blocked && 0 < count( $hosts ) ) {
2172              $result['status'] = 'recommended';
2173  
2174              $result['label'] = __( 'HTTP requests are partially blocked' );
2175  
2176              $result['description'] .= sprintf(
2177                  '<p>%s</p>',
2178                  sprintf(
2179                      /* translators: 1: Name of the constant used. 2: List of allowed hostnames. */
2180                      __( 'HTTP requests have been blocked by the %1$s constant, with some allowed hosts: %2$s.' ),
2181                      '<code>WP_HTTP_BLOCK_EXTERNAL</code>',
2182                      implode( ',', $hosts )
2183                  )
2184              );
2185          }
2186  
2187          return $result;
2188      }
2189  
2190      /**
2191       * Test if the REST API is accessible.
2192       *
2193       * Various security measures may block the REST API from working, or it may have been disabled in general.
2194       * This is required for the new block editor to work, so we explicitly test for this.
2195       *
2196       * @since 5.2.0
2197       *
2198       * @return array The test results.
2199       */
2200  	public function get_test_rest_availability() {
2201          $result = array(
2202              'label'       => __( 'The REST API is available' ),
2203              'status'      => 'good',
2204              'badge'       => array(
2205                  'label' => __( 'Performance' ),
2206                  'color' => 'blue',
2207              ),
2208              'description' => sprintf(
2209                  '<p>%s</p>',
2210                  __( '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.' )
2211              ),
2212              'actions'     => '',
2213              'test'        => 'rest_availability',
2214          );
2215  
2216          $cookies = wp_unslash( $_COOKIE );
2217          $timeout = 10;
2218          $headers = array(
2219              'Cache-Control' => 'no-cache',
2220              'X-WP-Nonce'    => wp_create_nonce( 'wp_rest' ),
2221          );
2222          /** This filter is documented in wp-includes/class-wp-http-streams.php */
2223          $sslverify = apply_filters( 'https_local_ssl_verify', false );
2224  
2225          // Include Basic auth in loopback requests.
2226          if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
2227              $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
2228          }
2229  
2230          $url = rest_url( 'wp/v2/types/post' );
2231  
2232          // The context for this is editing with the new block editor.
2233          $url = add_query_arg(
2234              array(
2235                  'context' => 'edit',
2236              ),
2237              $url
2238          );
2239  
2240          $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
2241  
2242          if ( is_wp_error( $r ) ) {
2243              $result['status'] = 'critical';
2244  
2245              $result['label'] = __( 'The REST API encountered an error' );
2246  
2247              $result['description'] .= sprintf(
2248                  '<p>%s</p>',
2249                  sprintf(
2250                      '%s<br>%s',
2251                      __( 'The REST API request failed due to an error.' ),
2252                      sprintf(
2253                          /* translators: 1: The WordPress error message. 2: The WordPress error code. */
2254                          __( 'Error: %1$s (%2$s)' ),
2255                          $r->get_error_message(),
2256                          $r->get_error_code()
2257                      )
2258                  )
2259              );
2260          } elseif ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
2261              $result['status'] = 'recommended';
2262  
2263              $result['label'] = __( 'The REST API encountered an unexpected result' );
2264  
2265              $result['description'] .= sprintf(
2266                  '<p>%s</p>',
2267                  sprintf(
2268                      /* translators: 1: The HTTP error code. 2: The HTTP error message. */
2269                      __( 'The REST API call gave the following unexpected result: (%1$d) %2$s.' ),
2270                      wp_remote_retrieve_response_code( $r ),
2271                      esc_html( wp_remote_retrieve_body( $r ) )
2272                  )
2273              );
2274          } else {
2275              $json = json_decode( wp_remote_retrieve_body( $r ), true );
2276  
2277              if ( false !== $json && ! isset( $json['capabilities'] ) ) {
2278                  $result['status'] = 'recommended';
2279  
2280                  $result['label'] = __( 'The REST API did not behave correctly' );
2281  
2282                  $result['description'] .= sprintf(
2283                      '<p>%s</p>',
2284                      sprintf(
2285                          /* translators: %s: The name of the query parameter being tested. */
2286                          __( 'The REST API did not process the %s query parameter correctly.' ),
2287                          '<code>context</code>'
2288                      )
2289                  );
2290              }
2291          }
2292  
2293          return $result;
2294      }
2295  
2296      /**
2297       * Test if 'file_uploads' directive in PHP.ini is turned off.
2298       *
2299       * @since 5.5.0
2300       *
2301       * @return array The test results.
2302       */
2303  	public function get_test_file_uploads() {
2304          $result = array(
2305              'label'       => __( 'Files can be uploaded' ),
2306              'status'      => 'good',
2307              'badge'       => array(
2308                  'label' => __( 'Performance' ),
2309                  'color' => 'blue',
2310              ),
2311              'description' => sprintf(
2312                  '<p>%s</p>',
2313                  sprintf(
2314                      /* translators: 1: file_uploads, 2: php.ini */
2315                      __( 'The %1$s directive in %2$s determines if uploading files is allowed on your site.' ),
2316                      '<code>file_uploads</code>',
2317                      '<code>php.ini</code>'
2318                  )
2319              ),
2320              'actions'     => '',
2321              'test'        => 'file_uploads',
2322          );
2323  
2324          if ( ! function_exists( 'ini_get' ) ) {
2325              $result['status']       = 'critical';
2326              $result['description'] .= sprintf(
2327                  /* translators: %s: ini_get() */
2328                  __( 'The %s function has been disabled, some media settings are unavailable because of this.' ),
2329                  '<code>ini_get()</code>'
2330              );
2331              return $result;
2332          }
2333  
2334          if ( empty( ini_get( 'file_uploads' ) ) ) {
2335              $result['status']       = 'critical';
2336              $result['description'] .= sprintf(
2337                  '<p>%s</p>',
2338                  sprintf(
2339                      /* translators: 1: file_uploads, 2: 0 */
2340                      __( '%1$s is set to %2$s. You won\'t be able to upload files on your site.' ),
2341                      '<code>file_uploads</code>',
2342                      '<code>0</code>'
2343                  )
2344              );
2345              return $result;
2346          }
2347  
2348          $post_max_size       = ini_get( 'post_max_size' );
2349          $upload_max_filesize = ini_get( 'upload_max_filesize' );
2350  
2351          if ( wp_convert_hr_to_bytes( $post_max_size ) < wp_convert_hr_to_bytes( $upload_max_filesize ) ) {
2352              $result['label'] = sprintf(
2353                  /* translators: 1: post_max_size, 2: upload_max_filesize */
2354                  __( 'The "%1$s" value is smaller than "%2$s"' ),
2355                  'post_max_size',
2356                  'upload_max_filesize'
2357              );
2358              $result['status'] = 'recommended';
2359  
2360              if ( 0 === wp_convert_hr_to_bytes( $post_max_size ) ) {
2361                  $result['description'] = sprintf(
2362                      '<p>%s</p>',
2363                      sprintf(
2364                          /* translators: 1: post_max_size, 2: upload_max_filesize */
2365                          __( '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.' ),
2366                          '<code>post_max_size</code>',
2367                          '<code>upload_max_filesize</code>'
2368                      )
2369                  );
2370              } else {
2371                  $result['description'] = sprintf(
2372                      '<p>%s</p>',
2373                      sprintf(
2374                          /* translators: 1: post_max_size, 2: upload_max_filesize */
2375                          __( 'The setting for %1$s is smaller than %2$s, this could cause some problems when trying to upload files.' ),
2376                          '<code>post_max_size</code>',
2377                          '<code>upload_max_filesize</code>'
2378                      )
2379                  );
2380              }
2381  
2382              return $result;
2383          }
2384  
2385          return $result;
2386      }
2387  
2388      /**
2389       * Tests if the Authorization header has the expected values.
2390       *
2391       * @since 5.6.0
2392       *
2393       * @return array
2394       */
2395  	public function get_test_authorization_header() {
2396          $result = array(
2397              'label'       => __( 'The Authorization header is working as expected' ),
2398              'status'      => 'good',
2399              'badge'       => array(
2400                  'label' => __( 'Security' ),
2401                  'color' => 'blue',
2402              ),
2403              'description' => sprintf(
2404                  '<p>%s</p>',
2405                  __( 'The Authorization header comes from the third-party applications you approve. Without it, those apps cannot connect to your site.' )
2406              ),
2407              'actions'     => '',
2408              'test'        => 'authorization_header',
2409          );
2410  
2411          if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) {
2412              $result['label'] = __( 'The authorization header is missing' );
2413          } elseif ( 'user' !== $_SERVER['PHP_AUTH_USER'] || 'pwd' !== $_SERVER['PHP_AUTH_PW'] ) {
2414              $result['label'] = __( 'The authorization header is invalid' );
2415          } else {
2416              return $result;
2417          }
2418  
2419          $result['status'] = 'recommended';
2420  
2421          if ( ! function_exists( 'got_mod_rewrite' ) ) {
2422              require_once ABSPATH . 'wp-admin/includes/misc.php';
2423          }
2424  
2425          if ( got_mod_rewrite() ) {
2426              $result['actions'] .= sprintf(
2427                  '<p><a href="%s">%s</a></p>',
2428                  esc_url( admin_url( 'options-permalink.php' ) ),
2429                  __( 'Flush permalinks' )
2430              );
2431          } else {
2432              $result['actions'] .= sprintf(
2433                  '<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>',
2434                  __( 'https://developer.wordpress.org/rest-api/frequently-asked-questions/#why-is-authentication-not-working' ),
2435                  __( 'Learn how to configure the Authorization header.' ),
2436                  /* translators: Accessibility text. */
2437                  __( '(opens in a new tab)' )
2438              );
2439          }
2440  
2441          return $result;
2442      }
2443  
2444      /**
2445       * Return a set of tests that belong to the site status page.
2446       *
2447       * Each site status test is defined here, they may be `direct` tests, that run on page load, or `async` tests
2448       * which will run later down the line via JavaScript calls to improve page performance and hopefully also user
2449       * experiences.
2450       *
2451       * @since 5.2.0
2452       * @since 5.6.0 Added support for `has_rest` and `permissions`.
2453       *
2454       * @return array The list of tests to run.
2455       */
2456  	public static function get_tests() {
2457          $tests = array(
2458              'direct' => array(
2459                  'wordpress_version'            => array(
2460                      'label' => __( 'WordPress Version' ),
2461                      'test'  => 'wordpress_version',
2462                  ),
2463                  'plugin_version'               => array(
2464                      'label' => __( 'Plugin Versions' ),
2465                      'test'  => 'plugin_version',
2466                  ),
2467                  'theme_version'                => array(
2468                      'label' => __( 'Theme Versions' ),
2469                      'test'  => 'theme_version',
2470                  ),
2471                  'php_version'                  => array(
2472                      'label' => __( 'PHP Version' ),
2473                      'test'  => 'php_version',
2474                  ),
2475                  'php_extensions'               => array(
2476                      'label' => __( 'PHP Extensions' ),
2477                      'test'  => 'php_extensions',
2478                  ),
2479                  'php_default_timezone'         => array(
2480                      'label' => __( 'PHP Default Timezone' ),
2481                      'test'  => 'php_default_timezone',
2482                  ),
2483                  'php_sessions'                 => array(
2484                      'label' => __( 'PHP Sessions' ),
2485                      'test'  => 'php_sessions',
2486                  ),
2487                  'sql_server'                   => array(
2488                      'label' => __( 'Database Server version' ),
2489                      'test'  => 'sql_server',
2490                  ),
2491                  'utf8mb4_support'              => array(
2492                      'label' => __( 'MySQL utf8mb4 support' ),
2493                      'test'  => 'utf8mb4_support',
2494                  ),
2495                  'ssl_support'                  => array(
2496                      'label' => __( 'Secure communication' ),
2497                      'test'  => 'ssl_support',
2498                  ),
2499                  'scheduled_events'             => array(
2500                      'label' => __( 'Scheduled events' ),
2501                      'test'  => 'scheduled_events',
2502                  ),
2503                  'http_requests'                => array(
2504                      'label' => __( 'HTTP Requests' ),
2505                      'test'  => 'http_requests',
2506                  ),
2507                  'rest_availability'            => array(
2508                      'label'     => __( 'REST API availability' ),
2509                      'test'      => 'rest_availability',
2510                      'skip_cron' => true,
2511                  ),
2512                  'debug_enabled'                => array(
2513                      'label' => __( 'Debugging enabled' ),
2514                      'test'  => 'is_in_debug_mode',
2515                  ),
2516                  'file_uploads'                 => array(
2517                      'label' => __( 'File uploads' ),
2518                      'test'  => 'file_uploads',
2519                  ),
2520                  'plugin_theme_auto_updates'    => array(
2521                      'label' => __( 'Plugin and theme auto-updates' ),
2522                      'test'  => 'plugin_theme_auto_updates',
2523                  ),
2524                  'update_temp_backup_writable'  => array(
2525                      /* translators: %s: temp-backup */
2526                      'label' => sprintf( __( 'Updates %s directory access' ), 'temp-backup' ),
2527                      'test'  => 'update_temp_backup_writable',
2528                  ),
2529                  'available_updates_disk_space' => array(
2530                      'label' => __( 'Available disk space' ),
2531                      'test'  => 'available_updates_disk_space',
2532                  ),
2533              ),
2534              'async'  => array(
2535                  'dotorg_communication' => array(
2536                      'label'             => __( 'Communication with WordPress.org' ),
2537                      'test'              => rest_url( 'wp-site-health/v1/tests/dotorg-communication' ),
2538                      'has_rest'          => true,
2539                      'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_dotorg_communication' ),
2540                  ),
2541                  'background_updates'   => array(
2542                      'label'             => __( 'Background updates' ),
2543                      'test'              => rest_url( 'wp-site-health/v1/tests/background-updates' ),
2544                      'has_rest'          => true,
2545                      'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_background_updates' ),
2546                  ),
2547                  'loopback_requests'    => array(
2548                      'label'             => __( 'Loopback request' ),
2549                      'test'              => rest_url( 'wp-site-health/v1/tests/loopback-requests' ),
2550                      'has_rest'          => true,
2551                      'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_loopback_requests' ),
2552                  ),
2553                  'https_status'         => array(
2554                      'label'             => __( 'HTTPS status' ),
2555                      'test'              => rest_url( 'wp-site-health/v1/tests/https-status' ),
2556                      'has_rest'          => true,
2557                      'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_https_status' ),
2558                  ),
2559              ),
2560          );
2561  
2562          // Conditionally include Authorization header test if the site isn't protected by Basic Auth.
2563          if ( ! wp_is_site_protected_by_basic_auth() ) {
2564              $tests['async']['authorization_header'] = array(
2565                  'label'     => __( 'Authorization header' ),
2566                  'test'      => rest_url( 'wp-site-health/v1/tests/authorization-header' ),
2567                  'has_rest'  => true,
2568                  'headers'   => array( 'Authorization' => 'Basic ' . base64_encode( 'user:pwd' ) ),
2569                  'skip_cron' => true,
2570              );
2571          }
2572  
2573          /**
2574           * Add or modify which site status tests are run on a site.
2575           *
2576           * The site health is determined by a set of tests based on best practices from
2577           * both the WordPress Hosting Team and web standards in general.
2578           *
2579           * Some sites may not have the same requirements, for example the automatic update
2580           * checks may be handled by a host, and are therefore disabled in core.
2581           * Or maybe you want to introduce a new test, is caching enabled/disabled/stale for example.
2582           *
2583           * Tests may be added either as direct, or asynchronous ones. Any test that may require some time
2584           * to complete should run asynchronously, to avoid extended loading periods within wp-admin.
2585           *
2586           * @since 5.2.0
2587           * @since 5.6.0 Added the `async_direct_test` array key for asynchronous tests.
2588           *              Added the `skip_cron` array key for all tests.
2589           *
2590           * @param array[] $tests {
2591           *     An associative array of direct and asynchronous tests.
2592           *
2593           *     @type array[] $direct {
2594           *         An array of direct tests.
2595           *
2596           *         @type array ...$identifier {
2597           *             `$identifier` should be a unique identifier for the test. Plugins and themes are encouraged to
2598           *             prefix test identifiers with their slug to avoid collisions between tests.
2599           *
2600           *             @type string   $label     The friendly label to identify the test.
2601           *             @type callable $test      The callback function that runs the test and returns its result.
2602           *             @type bool     $skip_cron Whether to skip this test when running as cron.
2603           *         }
2604           *     }
2605           *     @type array[] $async {
2606           *         An array of asynchronous tests.
2607           *
2608           *         @type array ...$identifier {
2609           *             `$identifier` should be a unique identifier for the test. Plugins and themes are encouraged to
2610           *             prefix test identifiers with their slug to avoid collisions between tests.
2611           *
2612           *             @type string   $label             The friendly label to identify the test.
2613           *             @type string   $test              An admin-ajax.php action to be called to perform the test, or
2614           *                                               if `$has_rest` is true, a URL to a REST API endpoint to perform
2615           *                                               the test.
2616           *             @type bool     $has_rest          Whether the `$test` property points to a REST API endpoint.
2617           *             @type bool     $skip_cron         Whether to skip this test when running as cron.
2618           *             @type callable $async_direct_test A manner of directly calling the test marked as asynchronous,
2619           *                                               as the scheduled event can not authenticate, and endpoints
2620           *                                               may require authentication.
2621           *         }
2622           *     }
2623           * }
2624           */
2625          $tests = apply_filters( 'site_status_tests', $tests );
2626  
2627          // Ensure that the filtered tests contain the required array keys.
2628          $tests = array_merge(
2629              array(
2630                  'direct' => array(),
2631                  'async'  => array(),
2632              ),
2633              $tests
2634          );
2635  
2636          return $tests;
2637      }
2638  
2639      /**
2640       * Add a class to the body HTML tag.
2641       *
2642       * Filters the body class string for admin pages and adds our own class for easier styling.
2643       *
2644       * @since 5.2.0
2645       *
2646       * @param string $body_class The body class string.
2647       * @return string The modified body class string.
2648       */
2649  	public function admin_body_class( $body_class ) {
2650          $screen = get_current_screen();
2651          if ( 'site-health' !== $screen->id ) {
2652              return $body_class;
2653          }
2654  
2655          $body_class .= ' site-health';
2656  
2657          return $body_class;
2658      }
2659  
2660      /**
2661       * Initiate the WP_Cron schedule test cases.
2662       *
2663       * @since 5.2.0
2664       */
2665  	private function wp_schedule_test_init() {
2666          $this->schedules = wp_get_schedules();
2667          $this->get_cron_tasks();
2668      }
2669  
2670      /**
2671       * Populate our list of cron events and store them to a class-wide variable.
2672       *
2673       * @since 5.2.0
2674       */
2675  	private function get_cron_tasks() {
2676          $cron_tasks = _get_cron_array();
2677  
2678          if ( empty( $cron_tasks ) ) {
2679              $this->crons = new WP_Error( 'no_tasks', __( 'No scheduled events exist on this site.' ) );
2680              return;
2681          }
2682  
2683          $this->crons = array();
2684  
2685          foreach ( $cron_tasks as $time => $cron ) {
2686              foreach ( $cron as $hook => $dings ) {
2687                  foreach ( $dings as $sig => $data ) {
2688  
2689                      $this->crons[ "$hook-$sig-$time" ] = (object) array(
2690                          'hook'     => $hook,
2691                          'time'     => $time,
2692                          'sig'      => $sig,
2693                          'args'     => $data['args'],
2694                          'schedule' => $data['schedule'],
2695                          'interval' => isset( $data['interval'] ) ? $data['interval'] : null,
2696                      );
2697  
2698                  }
2699              }
2700          }
2701      }
2702  
2703      /**
2704       * Check if any scheduled tasks have been missed.
2705       *
2706       * Returns a boolean value of `true` if a scheduled task has been missed and ends processing.
2707       *
2708       * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
2709       *
2710       * @since 5.2.0
2711       *
2712       * @return bool|WP_Error True if a cron was missed, false if not. WP_Error if the cron is set to that.
2713       */
2714  	public function has_missed_cron() {
2715          if ( is_wp_error( $this->crons ) ) {
2716              return $this->crons;
2717          }
2718  
2719          foreach ( $this->crons as $id => $cron ) {
2720              if ( ( $cron->time - time() ) < $this->timeout_missed_cron ) {
2721                  $this->last_missed_cron = $cron->hook;
2722                  return true;
2723              }
2724          }
2725  
2726          return false;
2727      }
2728  
2729      /**
2730       * Check if any scheduled tasks are late.
2731       *
2732       * Returns a boolean value of `true` if a scheduled task is late and ends processing.
2733       *
2734       * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
2735       *
2736       * @since 5.3.0
2737       *
2738       * @return bool|WP_Error True if a cron is late, false if not. WP_Error if the cron is set to that.
2739       */
2740  	public function has_late_cron() {
2741          if ( is_wp_error( $this->crons ) ) {
2742              return $this->crons;
2743          }
2744  
2745          foreach ( $this->crons as $id => $cron ) {
2746              $cron_offset = $cron->time - time();
2747              if (
2748                  $cron_offset >= $this->timeout_missed_cron &&
2749                  $cron_offset < $this->timeout_late_cron
2750              ) {
2751                  $this->last_late_cron = $cron->hook;
2752                  return true;
2753              }
2754          }
2755  
2756          return false;
2757      }
2758  
2759      /**
2760       * Check for potential issues with plugin and theme auto-updates.
2761       *
2762       * Though there is no way to 100% determine if plugin and theme auto-updates are configured
2763       * correctly, a few educated guesses could be made to flag any conditions that would
2764       * potentially cause unexpected behaviors.
2765       *
2766       * @since 5.5.0
2767       *
2768       * @return object The test results.
2769       */
2770  	public function detect_plugin_theme_auto_update_issues() {
2771          $mock_plugin = (object) array(
2772              'id'            => 'w.org/plugins/a-fake-plugin',
2773              'slug'          => 'a-fake-plugin',
2774              'plugin'        => 'a-fake-plugin/a-fake-plugin.php',
2775              'new_version'   => '9.9',
2776              'url'           => 'https://wordpress.org/plugins/a-fake-plugin/',
2777              'package'       => 'https://downloads.wordpress.org/plugin/a-fake-plugin.9.9.zip',
2778              'icons'         => array(
2779                  '2x' => 'https://ps.w.org/a-fake-plugin/assets/icon-256x256.png',
2780                  '1x' => 'https://ps.w.org/a-fake-plugin/assets/icon-128x128.png',
2781              ),
2782              'banners'       => array(
2783                  '2x' => 'https://ps.w.org/a-fake-plugin/assets/banner-1544x500.png',
2784                  '1x' => 'https://ps.w.org/a-fake-plugin/assets/banner-772x250.png',
2785              ),
2786              'banners_rtl'   => array(),
2787              'tested'        => '5.5.0',
2788              'requires_php'  => '5.6.20',
2789              'compatibility' => new stdClass(),
2790          );
2791  
2792          $mock_theme = (object) array(
2793              'theme'        => 'a-fake-theme',
2794              'new_version'  => '9.9',
2795              'url'          => 'https://wordpress.org/themes/a-fake-theme/',
2796              'package'      => 'https://downloads.wordpress.org/theme/a-fake-theme.9.9.zip',
2797              'requires'     => '5.0.0',
2798              'requires_php' => '5.6.20',
2799          );
2800  
2801          $test_plugins_enabled = wp_is_auto_update_forced_for_item( 'plugin', true, $mock_plugin );
2802          $test_themes_enabled  = wp_is_auto_update_forced_for_item( 'theme', true, $mock_theme );
2803  
2804          $ui_enabled_for_plugins = wp_is_auto_update_enabled_for_type( 'plugin' );
2805          $ui_enabled_for_themes  = wp_is_auto_update_enabled_for_type( 'theme' );
2806          $plugin_filter_present  = has_filter( 'auto_update_plugin' );
2807          $theme_filter_present   = has_filter( 'auto_update_theme' );
2808  
2809          if ( ( ! $test_plugins_enabled && $ui_enabled_for_plugins )
2810              || ( ! $test_themes_enabled && $ui_enabled_for_themes )
2811          ) {
2812              return (object) array(
2813                  'status'  => 'critical',
2814                  '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.' ),
2815              );
2816          }
2817  
2818          if ( ( ! $test_plugins_enabled && $plugin_filter_present )
2819              && ( ! $test_themes_enabled && $theme_filter_present )
2820          ) {
2821              return (object) array(
2822                  'status'  => 'recommended',
2823                  'message' => __( 'Auto-updates for plugins and themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
2824              );
2825          } elseif ( ! $test_plugins_enabled && $plugin_filter_present ) {
2826              return (object) array(
2827                  'status'  => 'recommended',
2828                  'message' => __( 'Auto-updates for plugins appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
2829              );
2830          } elseif ( ! $test_themes_enabled && $theme_filter_present ) {
2831              return (object) array(
2832                  'status'  => 'recommended',
2833                  'message' => __( 'Auto-updates for themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
2834              );
2835          }
2836  
2837          return (object) array(
2838              'status'  => 'good',
2839              'message' => __( 'There appear to be no issues with plugin and theme auto-updates.' ),
2840          );
2841      }
2842  
2843      /**
2844       * Run a loopback test on our site.
2845       *
2846       * Loopbacks are what WordPress uses to communicate with itself to start up WP_Cron, scheduled posts,
2847       * make sure plugin or theme edits don't cause site failures and similar.
2848       *
2849       * @since 5.2.0
2850       *
2851       * @return object The test results.
2852       */
2853  	public function can_perform_loopback() {
2854          $body    = array( 'site-health' => 'loopback-test' );
2855          $cookies = wp_unslash( $_COOKIE );
2856          $timeout = 10;
2857          $headers = array(
2858              'Cache-Control' => 'no-cache',
2859          );
2860          /** This filter is documented in wp-includes/class-wp-http-streams.php */
2861          $sslverify = apply_filters( 'https_local_ssl_verify', false );
2862  
2863          // Include Basic auth in loopback requests.
2864          if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
2865              $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
2866          }
2867  
2868          $url = site_url( 'wp-cron.php' );
2869  
2870          /*
2871           * A post request is used for the wp-cron.php loopback test to cause the file
2872           * to finish early without triggering cron jobs. This has two benefits:
2873           * - cron jobs are not triggered a second time on the site health page,
2874           * - the loopback request finishes sooner providing a quicker result.
2875           *
2876           * Using a POST request causes the loopback to differ slightly to the standard
2877           * GET request WordPress uses for wp-cron.php loopback requests but is close
2878           * enough. See https://core.trac.wordpress.org/ticket/52547
2879           */
2880          $r = wp_remote_post( $url, compact( 'body', 'cookies', 'headers', 'timeout', 'sslverify' ) );
2881  
2882          if ( is_wp_error( $r ) ) {
2883              return (object) array(
2884                  'status'  => 'critical',
2885                  'message' => sprintf(
2886                      '%s<br>%s',
2887                      __( 'The loopback request to your site failed, this means features relying on them are not currently working as expected.' ),
2888                      sprintf(
2889                          /* translators: 1: The WordPress error message. 2: The WordPress error code. */
2890                          __( 'Error: %1$s (%2$s)' ),
2891                          $r->get_error_message(),
2892                          $r->get_error_code()
2893                      )
2894                  ),
2895              );
2896          }
2897  
2898          if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
2899              return (object) array(
2900                  'status'  => 'recommended',
2901                  'message' => sprintf(
2902                      /* translators: %d: The HTTP response code returned. */
2903                      __( '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.' ),
2904                      wp_remote_retrieve_response_code( $r )
2905                  ),
2906              );
2907          }
2908  
2909          return (object) array(
2910              'status'  => 'good',
2911              'message' => __( 'The loopback request to your site completed successfully.' ),
2912          );
2913      }
2914  
2915      /**
2916       * Create a weekly cron event, if one does not already exist.
2917       *
2918       * @since 5.4.0
2919       */
2920  	public function maybe_create_scheduled_event() {
2921          if ( ! wp_next_scheduled( 'wp_site_health_scheduled_check' ) && ! wp_installing() ) {
2922              wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'wp_site_health_scheduled_check' );
2923          }
2924      }
2925  
2926      /**
2927       * Run our scheduled event to check and update the latest site health status for the website.
2928       *
2929       * @since 5.4.0
2930       */
2931  	public function wp_cron_scheduled_check() {
2932          // Bootstrap wp-admin, as WP_Cron doesn't do this for us.
2933          require_once trailingslashit( ABSPATH ) . 'wp-admin/includes/admin.php';
2934  
2935          $tests = WP_Site_Health::get_tests();
2936  
2937          $results = array();
2938  
2939          $site_status = array(
2940              'good'        => 0,
2941              'recommended' => 0,
2942              'critical'    => 0,
2943          );
2944  
2945          // Don't run https test on development environments.
2946          if ( $this->is_development_environment() ) {
2947              unset( $tests['async']['https_status'] );
2948          }
2949  
2950          foreach ( $tests['direct'] as $test ) {
2951              if ( ! empty( $test['skip_cron'] ) ) {
2952                  continue;
2953              }
2954  
2955              if ( is_string( $test['test'] ) ) {
2956                  $test_function = sprintf(
2957                      'get_test_%s',
2958                      $test['test']
2959                  );
2960  
2961                  if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
2962                      $results[] = $this->perform_test( array( $this, $test_function ) );
2963                      continue;
2964                  }
2965              }
2966  
2967              if ( is_callable( $test['test'] ) ) {
2968                  $results[] = $this->perform_test( $test['test'] );
2969              }
2970          }
2971  
2972          foreach ( $tests['async'] as $test ) {
2973              if ( ! empty( $test['skip_cron'] ) ) {
2974                  continue;
2975              }
2976  
2977              // Local endpoints may require authentication, so asynchronous tests can pass a direct test runner as well.
2978              if ( ! empty( $test['async_direct_test'] ) && is_callable( $test['async_direct_test'] ) ) {
2979                  // This test is callable, do so and continue to the next asynchronous check.
2980                  $results[] = $this->perform_test( $test['async_direct_test'] );
2981                  continue;
2982              }
2983  
2984              if ( is_string( $test['test'] ) ) {
2985                  // Check if this test has a REST API endpoint.
2986                  if ( isset( $test['has_rest'] ) && $test['has_rest'] ) {
2987                      $result_fetch = wp_remote_get(
2988                          $test['test'],
2989                          array(
2990                              'body' => array(
2991                                  '_wpnonce' => wp_create_nonce( 'wp_rest' ),
2992                              ),
2993                          )
2994                      );
2995                  } else {
2996                      $result_fetch = wp_remote_post(
2997                          admin_url( 'admin-ajax.php' ),
2998                          array(
2999                              'body' => array(
3000                                  'action'   => $test['test'],
3001                                  '_wpnonce' => wp_create_nonce( 'health-check-site-status' ),
3002                              ),
3003                          )
3004                      );
3005                  }
3006  
3007                  if ( ! is_wp_error( $result_fetch ) && 200 === wp_remote_retrieve_response_code( $result_fetch ) ) {
3008                      $result = json_decode( wp_remote_retrieve_body( $result_fetch ), true );
3009                  } else {
3010                      $result = false;
3011                  }
3012  
3013                  if ( is_array( $result ) ) {
3014                      $results[] = $result;
3015                  } else {
3016                      $results[] = array(
3017                          'status' => 'recommended',
3018                          'label'  => __( 'A test is unavailable' ),
3019                      );
3020                  }
3021              }
3022          }
3023  
3024          foreach ( $results as $result ) {
3025              if ( 'critical' === $result['status'] ) {
3026                  $site_status['critical']++;
3027              } elseif ( 'recommended' === $result['status'] ) {
3028                  $site_status['recommended']++;
3029              } else {
3030                  $site_status['good']++;
3031              }
3032          }
3033  
3034          set_transient( 'health-check-site-status-result', wp_json_encode( $site_status ) );
3035      }
3036  
3037      /**
3038       * Checks if the current environment type is set to 'development' or 'local'.
3039       *
3040       * @since 5.6.0
3041       *
3042       * @return bool True if it is a development environment, false if not.
3043       */
3044  	public function is_development_environment() {
3045          return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
3046      }
3047  
3048  }


Generated: Tue Oct 26 01:00:02 2021 Cross-referenced by PHPXref 0.7.1