Make WordPress Core

Ticket #64370: 64370.patch

File 64370.patch, 262.8 KB (added by solankisoftware, 8 months ago)

FIX :: Response header detection in page cache test for Site Health should be more robust

  • new file wp-admin/includes/class-wp-site-health.php

    diff --git a/wp-admin/includes/class-wp-site-health.php b/wp-admin/includes/class-wp-site-health.php
    new file mode 100644
    index 0000000..c659c0f
    - +  
     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#[AllowDynamicProperties]
     11class WP_Site_Health {
     12        private static $instance = null;
     13
     14        private $is_acceptable_mysql_version;
     15        private $is_recommended_mysql_version;
     16
     17        public $is_mariadb                   = false;
     18        private $mysql_server_version        = '';
     19        private $mysql_required_version      = '5.5';
     20        private $mysql_recommended_version   = '8.0';
     21        private $mariadb_recommended_version = '10.6';
     22
     23        public $php_memory_limit;
     24
     25        public $schedules;
     26        public $crons;
     27        public $last_missed_cron     = null;
     28        public $last_late_cron       = null;
     29        private $timeout_missed_cron = null;
     30        private $timeout_late_cron   = null;
     31
     32        /**
     33         * WP_Site_Health constructor.
     34         *
     35         * @since 5.2.0
     36         */
     37        public function __construct() {
     38                $this->maybe_create_scheduled_event();
     39
     40                // Save memory limit before it's affected by wp_raise_memory_limit( 'admin' ).
     41                $this->php_memory_limit = ini_get( 'memory_limit' );
     42
     43                $this->timeout_late_cron   = 0;
     44                $this->timeout_missed_cron = - 5 * MINUTE_IN_SECONDS;
     45
     46                if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
     47                        $this->timeout_late_cron   = - 15 * MINUTE_IN_SECONDS;
     48                        $this->timeout_missed_cron = - 1 * HOUR_IN_SECONDS;
     49                }
     50
     51                add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
     52
     53                add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
     54                add_action( 'wp_site_health_scheduled_check', array( $this, 'wp_cron_scheduled_check' ) );
     55
     56                add_action( 'site_health_tab_content', array( $this, 'show_site_health_tab' ) );
     57        }
     58
     59        /**
     60         * Outputs the content of a tab in the Site Health screen.
     61         *
     62         * @since 5.8.0
     63         *
     64         * @param string $tab Slug of the current tab being displayed.
     65         */
     66        public function show_site_health_tab( $tab ) {
     67                if ( 'debug' === $tab ) {
     68                        require_once ABSPATH . 'wp-admin/site-health-info.php';
     69                }
     70        }
     71
     72        /**
     73         * Returns an instance of the WP_Site_Health class, or create one if none exist yet.
     74         *
     75         * @since 5.4.0
     76         *
     77         * @return WP_Site_Health|null
     78         */
     79        public static function get_instance() {
     80                if ( null === self::$instance ) {
     81                        self::$instance = new WP_Site_Health();
     82                }
     83
     84                return self::$instance;
     85        }
     86
     87        /**
     88         * Enqueues the site health scripts.
     89         *
     90         * @since 5.2.0
     91         */
     92        public function enqueue_scripts() {
     93                $screen = get_current_screen();
     94                if ( 'site-health' !== $screen->id && 'dashboard' !== $screen->id ) {
     95                        return;
     96                }
     97
     98                $health_check_js_variables = array(
     99                        'screen'      => $screen->id,
     100                        'nonce'       => array(
     101                                'site_status'        => wp_create_nonce( 'health-check-site-status' ),
     102                                'site_status_result' => wp_create_nonce( 'health-check-site-status-result' ),
     103                        ),
     104                        'site_status' => array(
     105                                'direct' => array(),
     106                                'async'  => array(),
     107                                'issues' => array(
     108                                        'good'        => 0,
     109                                        'recommended' => 0,
     110                                        'critical'    => 0,
     111                                ),
     112                        ),
     113                );
     114
     115                $issue_counts = get_transient( 'health-check-site-status-result' );
     116
     117                if ( false !== $issue_counts ) {
     118                        $issue_counts = json_decode( $issue_counts );
     119
     120                        $health_check_js_variables['site_status']['issues'] = $issue_counts;
     121                }
     122
     123                if ( 'site-health' === $screen->id && ( ! isset( $_GET['tab'] ) || empty( $_GET['tab'] ) ) ) {
     124                        $tests = WP_Site_Health::get_tests();
     125
     126                        // Don't run https test on development environments.
     127                        if ( $this->is_development_environment() ) {
     128                                unset( $tests['async']['https_status'] );
     129                        }
     130
     131                        foreach ( $tests['direct'] as $test ) {
     132                                if ( is_string( $test['test'] ) ) {
     133                                        $test_function = sprintf(
     134                                                'get_test_%s',
     135                                                $test['test']
     136                                        );
     137
     138                                        if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
     139                                                $health_check_js_variables['site_status']['direct'][] = $this->perform_test( array( $this, $test_function ) );
     140                                                continue;
     141                                        }
     142                                }
     143
     144                                if ( is_callable( $test['test'] ) ) {
     145                                        $health_check_js_variables['site_status']['direct'][] = $this->perform_test( $test['test'] );
     146                                }
     147                        }
     148
     149                        foreach ( $tests['async'] as $test ) {
     150                                if ( is_string( $test['test'] ) ) {
     151                                        $health_check_js_variables['site_status']['async'][] = array(
     152                                                'test'      => $test['test'],
     153                                                'has_rest'  => ( isset( $test['has_rest'] ) ? $test['has_rest'] : false ),
     154                                                'completed' => false,
     155                                                'headers'   => isset( $test['headers'] ) ? $test['headers'] : array(),
     156                                        );
     157                                }
     158                        }
     159                }
     160
     161                wp_localize_script( 'site-health', 'SiteHealth', $health_check_js_variables );
     162        }
     163
     164        /**
     165         * Runs a Site Health test directly.
     166         *
     167         * @since 5.4.0
     168         *
     169         * @param callable $callback
     170         * @return mixed|void
     171         */
     172        private function perform_test( $callback ) {
     173                /**
     174                 * Filters the output of a finished Site Health test.
     175                 *
     176                 * @since 5.3.0
     177                 *
     178                 * @param array $test_result {
     179                 *     An associative array of test result data.
     180                 *
     181                 *     @type string $label       A label describing the test, and is used as a header in the output.
     182                 *     @type string $status      The status of the test, which can be a value of `good`, `recommended` or `critical`.
     183                 *     @type array  $badge {
     184                 *         Tests are put into categories which have an associated badge shown, these can be modified and assigned here.
     185                 *
     186                 *         @type string $label The test label, for example `Performance`.
     187                 *         @type string $color Default `blue`. A string representing a color to use for the label.
     188                 *     }
     189                 *     @type string $description A more descriptive explanation of what the test looks for, and why it is important for the end user.
     190                 *     @type string $actions     An action to direct the user to where they can resolve the issue, if one exists.
     191                 *     @type string $test        The name of the test being ran, used as a reference point.
     192                 * }
     193                 */
     194                return apply_filters( 'site_status_test_result', call_user_func( $callback ) );
     195        }
     196
     197        /**
     198         * Detect cache status from headers.
     199         *
     200         * @param array $headers Response headers.
     201         * @return bool Whether cache was detected.
     202         */
     203        private function detect_cache_headers( $headers ) {
     204
     205            // Normalize header names & values.
     206            $normalized = array();
     207            foreach ( $headers as $key => $value ) {
     208                $normalized[ strtolower( $key ) ] = strtolower( $value );
     209            }
     210
     211            // Common server / CDN / proxy cache headers.
     212            $cache_headers = array(
     213                'x-cache',
     214                'x-cache-status',
     215                'cf-cache-status',
     216                'x-varnish',
     217                'x-pass',
     218                'x-proxy-cache',
     219                'x-litespeed-cache',
     220            );
     221
     222            /**
     223             * Allow hosts / plugins to add additional cache headers.
     224             */
     225            $cache_headers = apply_filters( 'site_health_cache_headers', $cache_headers );
     226
     227            // Valid cache status values.
     228            $valid_values = array(
     229                'hit',
     230                'miss',
     231                'pass',
     232                'skip',
     233                'expired',
     234                'revalidated',
     235                'reload',
     236                'refresh',
     237                'dynamic',
     238                'bypass',
     239            );
     240
     241            foreach ( $cache_headers as $header ) {
     242                if ( isset( $normalized[ $header ] ) ) {
     243
     244                    $value = trim( $normalized[ $header ] );
     245
     246                    // Exact match.
     247                    if ( in_array( $value, $valid_values, true ) ) {
     248                        return true;
     249                    }
     250
     251                    // Match phrases: "HIT from varnish", etc.
     252                    if ( preg_match( '/\b(' . implode( '|', $valid_values ) . ')\b/i', $value ) ) {
     253                        return true;
     254                    }
     255                }
     256            }
     257
     258            return false;
     259        }
     260
     261        /**
     262         * Runs the SQL version checks.
     263         *
     264         * These values are used in later tests, but the part of preparing them is more easily managed
     265         * early in the class for ease of access and discovery.
     266         *
     267         * @since 5.2.0
     268         *
     269         * @global wpdb $wpdb WordPress database abstraction object.
     270         */
     271        private function prepare_sql_data() {
     272                global $wpdb;
     273
     274                $mysql_server_type = $wpdb->db_server_info();
     275
     276                $this->mysql_server_version = $wpdb->get_var( 'SELECT VERSION()' );
     277
     278                if ( stristr( $mysql_server_type, 'mariadb' ) ) {
     279                        $this->is_mariadb                = true;
     280                        $this->mysql_recommended_version = $this->mariadb_recommended_version;
     281                }
     282
     283                $this->is_acceptable_mysql_version  = version_compare( $this->mysql_required_version, $this->mysql_server_version, '<=' );
     284                $this->is_recommended_mysql_version = version_compare( $this->mysql_recommended_version, $this->mysql_server_version, '<=' );
     285        }
     286
     287        /**
     288         * Tests whether `wp_version_check` is blocked.
     289         *
     290         * It's possible to block updates with the `wp_version_check` filter, but this can't be checked
     291         * during an Ajax call, as the filter is never introduced then.
     292         *
     293         * This filter overrides a standard page request if it's made by an admin through the Ajax call
     294         * with the right query argument to check for this.
     295         *
     296         * @since 5.2.0
     297         */
     298        public function check_wp_version_check_exists() {
     299                if ( ! is_admin() || ! is_user_logged_in() || ! current_user_can( 'update_core' ) || ! isset( $_GET['health-check-test-wp_version_check'] ) ) {
     300                        return;
     301                }
     302
     303                echo ( has_filter( 'wp_version_check', 'wp_version_check' ) ? 'yes' : 'no' );
     304
     305                die();
     306        }
     307
     308        /**
     309         * Tests for WordPress version and outputs it.
     310         *
     311         * Gives various results depending on what kind of updates are available, if any, to encourage
     312         * the user to install security updates as a priority.
     313         *
     314         * @since 5.2.0
     315         *
     316         * @return array The test result.
     317         */
     318        public function get_test_wordpress_version() {
     319                $result = array(
     320                        'label'       => '',
     321                        'status'      => '',
     322                        'badge'       => array(
     323                                'label' => __( 'Performance' ),
     324                                'color' => 'blue',
     325                        ),
     326                        'description' => '',
     327                        'actions'     => '',
     328                        'test'        => 'wordpress_version',
     329                );
     330
     331                $core_current_version = wp_get_wp_version();
     332                $core_updates         = get_core_updates();
     333
     334                if ( ! is_array( $core_updates ) ) {
     335                        $result['status'] = 'recommended';
     336
     337                        $result['label'] = sprintf(
     338                                /* translators: %s: Your current version of WordPress. */
     339                                __( 'WordPress version %s' ),
     340                                $core_current_version
     341                        );
     342
     343                        $result['description'] = sprintf(
     344                                '<p>%s</p>',
     345                                __( 'Unable to check if any new versions of WordPress are available.' )
     346                        );
     347
     348                        $result['actions'] = sprintf(
     349                                '<a href="%s">%s</a>',
     350                                esc_url( admin_url( 'update-core.php?force-check=1' ) ),
     351                                __( 'Check for updates manually' )
     352                        );
     353                } else {
     354                        foreach ( $core_updates as $core => $update ) {
     355                                if ( 'upgrade' === $update->response ) {
     356                                        $current_version = explode( '.', $core_current_version );
     357                                        $new_version     = explode( '.', $update->version );
     358
     359                                        $current_major = $current_version[0] . '.' . $current_version[1];
     360                                        $new_major     = $new_version[0] . '.' . $new_version[1];
     361
     362                                        $result['label'] = sprintf(
     363                                                /* translators: %s: The latest version of WordPress available. */
     364                                                __( 'WordPress update available (%s)' ),
     365                                                $update->version
     366                                        );
     367
     368                                        $result['actions'] = sprintf(
     369                                                '<a href="%s">%s</a>',
     370                                                esc_url( admin_url( 'update-core.php' ) ),
     371                                                __( 'Install the latest version of WordPress' )
     372                                        );
     373
     374                                        if ( $current_major !== $new_major ) {
     375                                                // This is a major version mismatch.
     376                                                $result['status']      = 'recommended';
     377                                                $result['description'] = sprintf(
     378                                                        '<p>%s</p>',
     379                                                        __( 'A new version of WordPress is available.' )
     380                                                );
     381                                        } else {
     382                                                // This is a minor version, sometimes considered more critical.
     383                                                $result['status']         = 'critical';
     384                                                $result['badge']['label'] = __( 'Security' );
     385                                                $result['description']    = sprintf(
     386                                                        '<p>%s</p>',
     387                                                        __( 'A new minor update is available for your site. Because minor updates often address security, it&#8217;s important to install them.' )
     388                                                );
     389                                        }
     390                                } else {
     391                                        $result['status'] = 'good';
     392                                        $result['label']  = sprintf(
     393                                                /* translators: %s: The current version of WordPress installed on this site. */
     394                                                __( 'Your version of WordPress (%s) is up to date' ),
     395                                                $core_current_version
     396                                        );
     397
     398                                        $result['description'] = sprintf(
     399                                                '<p>%s</p>',
     400                                                __( 'You are currently running the latest version of WordPress available, keep it up!' )
     401                                        );
     402                                }
     403                        }
     404                }
     405
     406                return $result;
     407        }
     408
     409        /**
     410         * Tests if plugins are outdated, or unnecessary.
     411         *
     412         * The test checks if your plugins are up to date, and encourages you to remove any
     413         * that are not in use.
     414         *
     415         * @since 5.2.0
     416         *
     417         * @return array The test result.
     418         */
     419        public function get_test_plugin_version() {
     420                $result = array(
     421                        'label'       => __( 'Your plugins are all up to date' ),
     422                        'status'      => 'good',
     423                        'badge'       => array(
     424                                'label' => __( 'Security' ),
     425                                'color' => 'blue',
     426                        ),
     427                        'description' => sprintf(
     428                                '<p>%s</p>',
     429                                __( '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.' )
     430                        ),
     431                        'actions'     => sprintf(
     432                                '<p><a href="%s">%s</a></p>',
     433                                esc_url( admin_url( 'plugins.php' ) ),
     434                                __( 'Manage your plugins' )
     435                        ),
     436                        'test'        => 'plugin_version',
     437                );
     438
     439                $plugins        = get_plugins();
     440                $plugin_updates = get_plugin_updates();
     441
     442                $plugins_active      = 0;
     443                $plugins_total       = 0;
     444                $plugins_need_update = 0;
     445
     446                // Loop over the available plugins and check their versions and active state.
     447                foreach ( $plugins as $plugin_path => $plugin ) {
     448                        ++$plugins_total;
     449
     450                        if ( is_plugin_active( $plugin_path ) ) {
     451                                ++$plugins_active;
     452                        }
     453
     454                        if ( array_key_exists( $plugin_path, $plugin_updates ) ) {
     455                                ++$plugins_need_update;
     456                        }
     457                }
     458
     459                // Add a notice if there are outdated plugins.
     460                if ( $plugins_need_update > 0 ) {
     461                        $result['status'] = 'critical';
     462
     463                        $result['label'] = __( 'You have plugins waiting to be updated' );
     464
     465                        $result['description'] .= sprintf(
     466                                '<p>%s</p>',
     467                                sprintf(
     468                                        /* translators: %d: The number of outdated plugins. */
     469                                        _n(
     470                                                'Your site has %d plugin waiting to be updated.',
     471                                                'Your site has %d plugins waiting to be updated.',
     472                                                $plugins_need_update
     473                                        ),
     474                                        $plugins_need_update
     475                                )
     476                        );
     477
     478                        $result['actions'] .= sprintf(
     479                                '<p><a href="%s">%s</a></p>',
     480                                esc_url( network_admin_url( 'plugins.php?plugin_status=upgrade' ) ),
     481                                __( 'Update your plugins' )
     482                        );
     483                } else {
     484                        if ( 1 === $plugins_active ) {
     485                                $result['description'] .= sprintf(
     486                                        '<p>%s</p>',
     487                                        __( 'Your site has 1 active plugin, and it is up to date.' )
     488                                );
     489                        } elseif ( $plugins_active > 0 ) {
     490                                $result['description'] .= sprintf(
     491                                        '<p>%s</p>',
     492                                        sprintf(
     493                                                /* translators: %d: The number of active plugins. */
     494                                                _n(
     495                                                        'Your site has %d active plugin, and it is up to date.',
     496                                                        'Your site has %d active plugins, and they are all up to date.',
     497                                                        $plugins_active
     498                                                ),
     499                                                $plugins_active
     500                                        )
     501                                );
     502                        } else {
     503                                $result['description'] .= sprintf(
     504                                        '<p>%s</p>',
     505                                        __( 'Your site does not have any active plugins.' )
     506                                );
     507                        }
     508                }
     509
     510                // Check if there are inactive plugins.
     511                if ( $plugins_total > $plugins_active && ! is_multisite() ) {
     512                        $unused_plugins = $plugins_total - $plugins_active;
     513
     514                        $result['status'] = 'recommended';
     515
     516                        $result['label'] = __( 'You should remove inactive plugins' );
     517
     518                        $result['description'] .= sprintf(
     519                                '<p>%s %s</p>',
     520                                sprintf(
     521                                        /* translators: %d: The number of inactive plugins. */
     522                                        _n(
     523                                                'Your site has %d inactive plugin.',
     524                                                'Your site has %d inactive plugins.',
     525                                                $unused_plugins
     526                                        ),
     527                                        $unused_plugins
     528                                ),
     529                                __( 'Inactive plugins are tempting targets for attackers. If you are not going to use a plugin, you should consider removing it.' )
     530                        );
     531
     532                        $result['actions'] .= sprintf(
     533                                '<p><a href="%s">%s</a></p>',
     534                                esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ),
     535                                __( 'Manage inactive plugins' )
     536                        );
     537                }
     538
     539                return $result;
     540        }
     541
     542        /**
     543         * Tests if themes are outdated, or unnecessary.
     544         *
     545         * Checks if your site has a default theme (to fall back on if there is a need),
     546         * if your themes are up to date and, finally, encourages you to remove any themes
     547         * that are not needed.
     548         *
     549         * @since 5.2.0
     550         *
     551         * @return array The test results.
     552         */
     553        public function get_test_theme_version() {
     554                $result = array(
     555                        'label'       => __( 'Your themes are all up to date' ),
     556                        'status'      => 'good',
     557                        'badge'       => array(
     558                                'label' => __( 'Security' ),
     559                                'color' => 'blue',
     560                        ),
     561                        'description' => sprintf(
     562                                '<p>%s</p>',
     563                                __( '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.' )
     564                        ),
     565                        'actions'     => sprintf(
     566                                '<p><a href="%s">%s</a></p>',
     567                                esc_url( admin_url( 'themes.php' ) ),
     568                                __( 'Manage your themes' )
     569                        ),
     570                        'test'        => 'theme_version',
     571                );
     572
     573                $theme_updates = get_theme_updates();
     574
     575                $themes_total        = 0;
     576                $themes_need_updates = 0;
     577                $themes_inactive     = 0;
     578
     579                // This value is changed during processing to determine how many themes are considered a reasonable amount.
     580                $allowed_theme_count = 1;
     581
     582                $has_default_theme   = false;
     583                $has_unused_themes   = false;
     584                $show_unused_themes  = true;
     585                $using_default_theme = false;
     586
     587                // Populate a list of all themes available in the install.
     588                $all_themes   = wp_get_themes();
     589                $active_theme = wp_get_theme();
     590
     591                // If WP_DEFAULT_THEME doesn't exist, fall back to the latest core default theme.
     592                $default_theme = wp_get_theme( WP_DEFAULT_THEME );
     593                if ( ! $default_theme->exists() ) {
     594                        $default_theme = WP_Theme::get_core_default_theme();
     595                }
     596
     597                if ( $default_theme ) {
     598                        $has_default_theme = true;
     599
     600                        if (
     601                                $active_theme->get_stylesheet() === $default_theme->get_stylesheet()
     602                        ||
     603                                is_child_theme() && $active_theme->get_template() === $default_theme->get_template()
     604                        ) {
     605                                $using_default_theme = true;
     606                        }
     607                }
     608
     609                foreach ( $all_themes as $theme_slug => $theme ) {
     610                        ++$themes_total;
     611
     612                        if ( array_key_exists( $theme_slug, $theme_updates ) ) {
     613                                ++$themes_need_updates;
     614                        }
     615                }
     616
     617                // If this is a child theme, increase the allowed theme count by one, to account for the parent.
     618                if ( is_child_theme() ) {
     619                        ++$allowed_theme_count;
     620                }
     621
     622                // If there's a default theme installed and not in use, we count that as allowed as well.
     623                if ( $has_default_theme && ! $using_default_theme ) {
     624                        ++$allowed_theme_count;
     625                }
     626
     627                if ( $themes_total > $allowed_theme_count ) {
     628                        $has_unused_themes = true;
     629                        $themes_inactive   = ( $themes_total - $allowed_theme_count );
     630                }
     631
     632                // Check if any themes need to be updated.
     633                if ( $themes_need_updates > 0 ) {
     634                        $result['status'] = 'critical';
     635
     636                        $result['label'] = __( 'You have themes waiting to be updated' );
     637
     638                        $result['description'] .= sprintf(
     639                                '<p>%s</p>',
     640                                sprintf(
     641                                        /* translators: %d: The number of outdated themes. */
     642                                        _n(
     643                                                'Your site has %d theme waiting to be updated.',
     644                                                'Your site has %d themes waiting to be updated.',
     645                                                $themes_need_updates
     646                                        ),
     647                                        $themes_need_updates
     648                                )
     649                        );
     650                } else {
     651                        // Give positive feedback about the site being good about keeping things up to date.
     652                        if ( 1 === $themes_total ) {
     653                                $result['description'] .= sprintf(
     654                                        '<p>%s</p>',
     655                                        __( 'Your site has 1 installed theme, and it is up to date.' )
     656                                );
     657                        } elseif ( $themes_total > 0 ) {
     658                                $result['description'] .= sprintf(
     659                                        '<p>%s</p>',
     660                                        sprintf(
     661                                                /* translators: %d: The number of themes. */
     662                                                _n(
     663                                                        'Your site has %d installed theme, and it is up to date.',
     664                                                        'Your site has %d installed themes, and they are all up to date.',
     665                                                        $themes_total
     666                                                ),
     667                                                $themes_total
     668                                        )
     669                                );
     670                        } else {
     671                                $result['description'] .= sprintf(
     672                                        '<p>%s</p>',
     673                                        __( 'Your site does not have any installed themes.' )
     674                                );
     675                        }
     676                }
     677
     678                if ( $has_unused_themes && $show_unused_themes && ! is_multisite() ) {
     679
     680                        // This is a child theme, so we want to be a bit more explicit in our messages.
     681                        if ( $active_theme->parent() ) {
     682                                // Recommend removing inactive themes, except a default theme, your current one, and the parent theme.
     683                                $result['status'] = 'recommended';
     684
     685                                $result['label'] = __( 'You should remove inactive themes' );
     686
     687                                if ( $using_default_theme ) {
     688                                        $result['description'] .= sprintf(
     689                                                '<p>%s %s</p>',
     690                                                sprintf(
     691                                                        /* translators: %d: The number of inactive themes. */
     692                                                        _n(
     693                                                                'Your site has %d inactive theme.',
     694                                                                'Your site has %d inactive themes.',
     695                                                                $themes_inactive
     696                                                        ),
     697                                                        $themes_inactive
     698                                                ),
     699                                                sprintf(
     700                                                        /* translators: 1: The currently active theme. 2: The active theme's parent theme. */
     701                                                        __( 'To enhance your site&#8217;s security, you should consider removing any themes you are not using. You should keep your active theme, %1$s, and %2$s, its parent theme.' ),
     702                                                        $active_theme->name,
     703                                                        $active_theme->parent()->name
     704                                                )
     705                                        );
     706                                } else {
     707                                        $result['description'] .= sprintf(
     708                                                '<p>%s %s</p>',
     709                                                sprintf(
     710                                                        /* translators: %d: The number of inactive themes. */
     711                                                        _n(
     712                                                                'Your site has %d inactive theme.',
     713                                                                'Your site has %d inactive themes.',
     714                                                                $themes_inactive
     715                                                        ),
     716                                                        $themes_inactive
     717                                                ),
     718                                                sprintf(
     719                                                        /* translators: 1: The default theme for WordPress. 2: The currently active theme. 3: The active theme's parent theme. */
     720                                                        __( 'To enhance your site&#8217;s security, you should consider removing any themes you are not using. You should keep %1$s, the default WordPress theme, %2$s, your active theme, and %3$s, its parent theme.' ),
     721                                                        $default_theme ? $default_theme->name : WP_DEFAULT_THEME,
     722                                                        $active_theme->name,
     723                                                        $active_theme->parent()->name
     724                                                )
     725                                        );
     726                                }
     727                        } else {
     728                                // Recommend removing all inactive themes.
     729                                $result['status'] = 'recommended';
     730
     731                                $result['label'] = __( 'You should remove inactive themes' );
     732
     733                                if ( $using_default_theme ) {
     734                                        $result['description'] .= sprintf(
     735                                                '<p>%s %s</p>',
     736                                                sprintf(
     737                                                        /* translators: 1: The amount of inactive themes. 2: The currently active theme. */
     738                                                        _n(
     739                                                                'Your site has %1$d inactive theme, other than %2$s, your active theme.',
     740                                                                'Your site has %1$d inactive themes, other than %2$s, your active theme.',
     741                                                                $themes_inactive
     742                                                        ),
     743                                                        $themes_inactive,
     744                                                        $active_theme->name
     745                                                ),
     746                                                __( 'You should consider removing any unused themes to enhance your site&#8217;s security.' )
     747                                        );
     748                                } else {
     749                                        $result['description'] .= sprintf(
     750                                                '<p>%s %s</p>',
     751                                                sprintf(
     752                                                        /* translators: 1: The amount of inactive themes. 2: The default theme for WordPress. 3: The currently active theme. */
     753                                                        _n(
     754                                                                'Your site has %1$d inactive theme, other than %2$s, the default WordPress theme, and %3$s, your active theme.',
     755                                                                'Your site has %1$d inactive themes, other than %2$s, the default WordPress theme, and %3$s, your active theme.',
     756                                                                $themes_inactive
     757                                                        ),
     758                                                        $themes_inactive,
     759                                                        $default_theme ? $default_theme->name : WP_DEFAULT_THEME,
     760                                                        $active_theme->name
     761                                                ),
     762                                                __( 'You should consider removing any unused themes to enhance your site&#8217;s security.' )
     763                                        );
     764                                }
     765                        }
     766                }
     767
     768                // If no default Twenty* theme exists.
     769                if ( ! $has_default_theme ) {
     770                        $result['status'] = 'recommended';
     771
     772                        $result['label'] = __( 'Have a default theme available' );
     773
     774                        $result['description'] .= sprintf(
     775                                '<p>%s</p>',
     776                                __( 'Your site does not have any default theme. Default themes are used by WordPress automatically if anything is wrong with your chosen theme.' )
     777                        );
     778                }
     779
     780                return $result;
     781        }
     782
     783        /**
     784         * Tests if the supplied PHP version is supported.
     785         *
     786         * @since 5.2.0
     787         *
     788         * @return array The test results.
     789         */
     790        public function get_test_php_version() {
     791                $response = wp_check_php_version();
     792
     793                $result = array(
     794                        'label'       => sprintf(
     795                                /* translators: %s: The server PHP version. */
     796                                __( 'Your site is running PHP %s' ),
     797                                PHP_VERSION
     798                        ),
     799                        'status'      => 'good',
     800                        'badge'       => array(
     801                                'label' => __( 'Performance' ),
     802                                'color' => 'blue',
     803                        ),
     804                        'description' => sprintf(
     805                                '<p>%s</p>',
     806                                __( 'PHP is one of the programming languages used to build WordPress. Newer versions of PHP receive regular security updates and may increase your site&#8217;s performance.' )
     807                        ),
     808                        'actions'     => sprintf(
     809                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     810                                esc_url( wp_get_update_php_url() ),
     811                                __( 'Learn more about updating PHP' ),
     812                                /* translators: Hidden accessibility text. */
     813                                __( '(opens in a new tab)' )
     814                        ),
     815                        'test'        => 'php_version',
     816                );
     817
     818                if ( ! $response ) {
     819                        $result['label'] = sprintf(
     820                                /* translators: %s: The server PHP version. */
     821                                __( 'Unable to determine the status of the current PHP version (%s)' ),
     822                                PHP_VERSION
     823                        );
     824                        $result['status']      = 'recommended';
     825                        $result['description'] = '<p><em>' . sprintf(
     826                                /* translators: %s is the URL to the Serve Happy docs page. */
     827                                __( 'Unable to access the WordPress.org API for <a href="%s">Serve Happy</a>.' ),
     828                                'https://codex.wordpress.org/WordPress.org_API#Serve_Happy'
     829                        ) . '</em></p>' . $result['description'];
     830                        return $result;
     831                }
     832
     833                $result['description'] .= '<p>' . sprintf(
     834                        /* translators: %s: The minimum recommended PHP version. */
     835                        __( 'The minimum recommended version of PHP is %s.' ),
     836                        $response['recommended_version']
     837                ) . '</p>';
     838
     839                // PHP is up to date.
     840                if ( version_compare( PHP_VERSION, $response['recommended_version'], '>=' ) ) {
     841                        $result['label'] = sprintf(
     842                                /* translators: %s: The server PHP version. */
     843                                __( 'Your site is running a recommended version of PHP (%s)' ),
     844                                PHP_VERSION
     845                        );
     846                        $result['status'] = 'good';
     847
     848                        return $result;
     849                }
     850
     851                // The PHP version is older than the recommended version, but still receiving active support.
     852                if ( $response['is_supported'] ) {
     853                        $result['label'] = sprintf(
     854                                /* translators: %s: The server PHP version. */
     855                                __( 'Your site is running on an older version of PHP (%s)' ),
     856                                PHP_VERSION
     857                        );
     858                        $result['status'] = 'recommended';
     859
     860                        return $result;
     861                }
     862
     863                /*
     864                 * The PHP version is still receiving security fixes, but is lower than
     865                 * the expected minimum version that will be required by WordPress in the near future.
     866                 */
     867                if ( $response['is_secure'] && $response['is_lower_than_future_minimum'] ) {
     868                        // The `is_secure` array key name doesn't actually imply this is a secure version of PHP. It only means it receives security updates.
     869
     870                        $result['label'] = sprintf(
     871                                /* translators: %s: The server PHP version. */
     872                                __( 'Your site is running on an outdated version of PHP (%s), which soon will not be supported by WordPress.' ),
     873                                PHP_VERSION
     874                        );
     875
     876                        $result['status']         = 'critical';
     877                        $result['badge']['label'] = __( 'Requirements' );
     878
     879                        return $result;
     880                }
     881
     882                // The PHP version is only receiving security fixes.
     883                if ( $response['is_secure'] ) {
     884                        $result['label'] = sprintf(
     885                                /* translators: %s: The server PHP version. */
     886                                __( 'Your site is running on an older version of PHP (%s), which should be updated' ),
     887                                PHP_VERSION
     888                        );
     889                        $result['status'] = 'recommended';
     890
     891                        return $result;
     892                }
     893
     894                // No more security updates for the PHP version, and lower than the expected minimum version required by WordPress.
     895                if ( $response['is_lower_than_future_minimum'] ) {
     896                        $message = sprintf(
     897                                /* translators: %s: The server PHP version. */
     898                                __( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates and soon will not be supported by WordPress.' ),
     899                                PHP_VERSION
     900                        );
     901                } else {
     902                        // No more security updates for the PHP version, must be updated.
     903                        $message = sprintf(
     904                                /* translators: %s: The server PHP version. */
     905                                __( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates. It should be updated.' ),
     906                                PHP_VERSION
     907                        );
     908                }
     909
     910                $result['label']  = $message;
     911                $result['status'] = 'critical';
     912
     913                $result['badge']['label'] = __( 'Security' );
     914
     915                return $result;
     916        }
     917
     918        /**
     919         * Checks if the passed extension or function are available.
     920         *
     921         * Make the check for available PHP modules into a simple boolean operator for a cleaner test runner.
     922         *
     923         * @since 5.2.0
     924         * @since 5.3.0 The `$constant_name` and `$class_name` parameters were added.
     925         *
     926         * @param string $extension_name Optional. The extension name to test. Default null.
     927         * @param string $function_name  Optional. The function name to test. Default null.
     928         * @param string $constant_name  Optional. The constant name to test for. Default null.
     929         * @param string $class_name     Optional. The class name to test for. Default null.
     930         * @return bool Whether or not the extension and function are available.
     931         */
     932        private function test_php_extension_availability( $extension_name = null, $function_name = null, $constant_name = null, $class_name = null ) {
     933                // If no extension or function is passed, claim to fail testing, as we have nothing to test against.
     934                if ( ! $extension_name && ! $function_name && ! $constant_name && ! $class_name ) {
     935                        return false;
     936                }
     937
     938                if ( $extension_name && ! extension_loaded( $extension_name ) ) {
     939                        return false;
     940                }
     941
     942                if ( $function_name && ! function_exists( $function_name ) ) {
     943                        return false;
     944                }
     945
     946                if ( $constant_name && ! defined( $constant_name ) ) {
     947                        return false;
     948                }
     949
     950                if ( $class_name && ! class_exists( $class_name ) ) {
     951                        return false;
     952                }
     953
     954                return true;
     955        }
     956
     957        /**
     958         * Tests if required PHP modules are installed on the host.
     959         *
     960         * This test builds on the recommendations made by the WordPress Hosting Team
     961         * as seen at https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions
     962         *
     963         * @since 5.2.0
     964         *
     965         * @return array
     966         */
     967        public function get_test_php_extensions() {
     968                $result = array(
     969                        'label'       => __( 'Required and recommended modules are installed' ),
     970                        'status'      => 'good',
     971                        'badge'       => array(
     972                                'label' => __( 'Performance' ),
     973                                'color' => 'blue',
     974                        ),
     975                        'description' => sprintf(
     976                                '<p>%s</p><p>%s</p>',
     977                                __( '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.' ),
     978                                sprintf(
     979                                        /* translators: 1: Link to the hosting group page about recommended PHP modules. 2: Additional link attributes. 3: Accessibility text. */
     980                                        __( '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>.' ),
     981                                        /* translators: Localized team handbook, if one exists. */
     982                                        esc_url( __( 'https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions' ) ),
     983                                        'target="_blank"',
     984                                        sprintf(
     985                                                '<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span>',
     986                                                /* translators: Hidden accessibility text. */
     987                                                __( '(opens in a new tab)' )
     988                                        )
     989                                )
     990                        ),
     991                        'actions'     => '',
     992                        'test'        => 'php_extensions',
     993                );
     994
     995                $modules = array(
     996                        'curl'      => array(
     997                                'function' => 'curl_version',
     998                                'required' => false,
     999                        ),
     1000                        'dom'       => array(
     1001                                'class'    => 'DOMNode',
     1002                                'required' => false,
     1003                        ),
     1004                        'exif'      => array(
     1005                                'function' => 'exif_read_data',
     1006                                'required' => false,
     1007                        ),
     1008                        'fileinfo'  => array(
     1009                                'function' => 'finfo_file',
     1010                                'required' => false,
     1011                        ),
     1012                        'hash'      => array(
     1013                                'function' => 'hash',
     1014                                'required' => true,
     1015                        ),
     1016                        'imagick'   => array(
     1017                                'extension' => 'imagick',
     1018                                'required'  => false,
     1019                        ),
     1020                        'json'      => array(
     1021                                'function' => 'json_last_error',
     1022                                'required' => true,
     1023                        ),
     1024                        'mbstring'  => array(
     1025                                'function' => 'mb_check_encoding',
     1026                                'required' => false,
     1027                        ),
     1028                        'mysqli'    => array(
     1029                                'function' => 'mysqli_connect',
     1030                                'required' => false,
     1031                        ),
     1032                        'libsodium' => array(
     1033                                'constant'            => 'SODIUM_LIBRARY_VERSION',
     1034                                'required'            => false,
     1035                                'php_bundled_version' => '7.2.0',
     1036                        ),
     1037                        'openssl'   => array(
     1038                                'function' => 'openssl_encrypt',
     1039                                'required' => false,
     1040                        ),
     1041                        'pcre'      => array(
     1042                                'function' => 'preg_match',
     1043                                'required' => false,
     1044                        ),
     1045                        'mod_xml'   => array(
     1046                                'extension' => 'libxml',
     1047                                'required'  => false,
     1048                        ),
     1049                        'zip'       => array(
     1050                                'class'    => 'ZipArchive',
     1051                                'required' => false,
     1052                        ),
     1053                        'filter'    => array(
     1054                                'function' => 'filter_list',
     1055                                'required' => false,
     1056                        ),
     1057                        'gd'        => array(
     1058                                'extension'    => 'gd',
     1059                                'required'     => false,
     1060                                'fallback_for' => 'imagick',
     1061                        ),
     1062                        'iconv'     => array(
     1063                                'function' => 'iconv',
     1064                                'required' => false,
     1065                        ),
     1066                        'intl'      => array(
     1067                                'extension' => 'intl',
     1068                                'required'  => false,
     1069                        ),
     1070                        'mcrypt'    => array(
     1071                                'extension'    => 'mcrypt',
     1072                                'required'     => false,
     1073                                'fallback_for' => 'libsodium',
     1074                        ),
     1075                        'simplexml' => array(
     1076                                'extension'    => 'simplexml',
     1077                                'required'     => false,
     1078                                'fallback_for' => 'mod_xml',
     1079                        ),
     1080                        'xmlreader' => array(
     1081                                'extension'    => 'xmlreader',
     1082                                'required'     => false,
     1083                                'fallback_for' => 'mod_xml',
     1084                        ),
     1085                        'zlib'      => array(
     1086                                'extension'    => 'zlib',
     1087                                'required'     => false,
     1088                                'fallback_for' => 'zip',
     1089                        ),
     1090                );
     1091
     1092                /**
     1093                 * Filters the array representing all the modules we wish to test for.
     1094                 *
     1095                 * @since 5.2.0
     1096                 * @since 5.3.0 The `$constant` and `$class` parameters were added.
     1097                 *
     1098                 * @param array $modules {
     1099                 *     An associative array of modules to test for.
     1100                 *
     1101                 *     @type array ...$0 {
     1102                 *         An associative array of module properties used during testing.
     1103                 *         One of either `$function` or `$extension` must be provided, or they will fail by default.
     1104                 *
     1105                 *         @type string $function     Optional. A function name to test for the existence of.
     1106                 *         @type string $extension    Optional. An extension to check if is loaded in PHP.
     1107                 *         @type string $constant     Optional. A constant name to check for to verify an extension exists.
     1108                 *         @type string $class        Optional. A class name to check for to verify an extension exists.
     1109                 *         @type bool   $required     Is this a required feature or not.
     1110                 *         @type string $fallback_for Optional. The module this module replaces as a fallback.
     1111                 *     }
     1112                 * }
     1113                 */
     1114                $modules = apply_filters( 'site_status_test_php_modules', $modules );
     1115
     1116                $failures = array();
     1117
     1118                foreach ( $modules as $library => $module ) {
     1119                        $extension_name = ( isset( $module['extension'] ) ? $module['extension'] : null );
     1120                        $function_name  = ( isset( $module['function'] ) ? $module['function'] : null );
     1121                        $constant_name  = ( isset( $module['constant'] ) ? $module['constant'] : null );
     1122                        $class_name     = ( isset( $module['class'] ) ? $module['class'] : null );
     1123
     1124                        // If this module is a fallback for another function, check if that other function passed.
     1125                        if ( isset( $module['fallback_for'] ) ) {
     1126                                /*
     1127                                 * If that other function has a failure, mark this module as required for usual operations.
     1128                                 * If that other function hasn't failed, skip this test as it's only a fallback.
     1129                                 */
     1130                                if ( isset( $failures[ $module['fallback_for'] ] ) ) {
     1131                                        $module['required'] = true;
     1132                                } else {
     1133                                        continue;
     1134                                }
     1135                        }
     1136
     1137                        if ( ! $this->test_php_extension_availability( $extension_name, $function_name, $constant_name, $class_name )
     1138                                && ( ! isset( $module['php_bundled_version'] )
     1139                                        || version_compare( PHP_VERSION, $module['php_bundled_version'], '<' ) )
     1140                        ) {
     1141                                if ( $module['required'] ) {
     1142                                        $result['status'] = 'critical';
     1143
     1144                                        $class = 'error';
     1145                                        /* translators: Hidden accessibility text. */
     1146                                        $screen_reader = __( 'Error' );
     1147                                        $message       = sprintf(
     1148                                                /* translators: %s: The module name. */
     1149                                                __( 'The required module, %s, is not installed, or has been disabled.' ),
     1150                                                $library
     1151                                        );
     1152                                } else {
     1153                                        $class = 'warning';
     1154                                        /* translators: Hidden accessibility text. */
     1155                                        $screen_reader = __( 'Warning' );
     1156                                        $message       = sprintf(
     1157                                                /* translators: %s: The module name. */
     1158                                                __( 'The optional module, %s, is not installed, or has been disabled.' ),
     1159                                                $library
     1160                                        );
     1161                                }
     1162
     1163                                if ( ! $module['required'] && 'good' === $result['status'] ) {
     1164                                        $result['status'] = 'recommended';
     1165                                }
     1166
     1167                                $failures[ $library ] = "<span class='dashicons $class' aria-hidden='true'></span><span class='screen-reader-text'>$screen_reader</span> $message";
     1168                        }
     1169                }
     1170
     1171                if ( ! empty( $failures ) ) {
     1172                        $output = '<ul>';
     1173
     1174                        foreach ( $failures as $failure ) {
     1175                                $output .= sprintf(
     1176                                        '<li>%s</li>',
     1177                                        $failure
     1178                                );
     1179                        }
     1180
     1181                        $output .= '</ul>';
     1182                }
     1183
     1184                if ( 'good' !== $result['status'] ) {
     1185                        if ( 'recommended' === $result['status'] ) {
     1186                                $result['label'] = __( 'One or more recommended modules are missing' );
     1187                        }
     1188                        if ( 'critical' === $result['status'] ) {
     1189                                $result['label'] = __( 'One or more required modules are missing' );
     1190                        }
     1191
     1192                        $result['description'] .= $output;
     1193                }
     1194
     1195                return $result;
     1196        }
     1197
     1198        /**
     1199         * Tests if the PHP default timezone is set to UTC.
     1200         *
     1201         * @since 5.3.1
     1202         *
     1203         * @return array The test results.
     1204         */
     1205        public function get_test_php_default_timezone() {
     1206                $result = array(
     1207                        'label'       => __( 'PHP default timezone is valid' ),
     1208                        'status'      => 'good',
     1209                        'badge'       => array(
     1210                                'label' => __( 'Performance' ),
     1211                                'color' => 'blue',
     1212                        ),
     1213                        'description' => sprintf(
     1214                                '<p>%s</p>',
     1215                                __( 'PHP default timezone was configured by WordPress on loading. This is necessary for correct calculations of dates and times.' )
     1216                        ),
     1217                        'actions'     => '',
     1218                        'test'        => 'php_default_timezone',
     1219                );
     1220
     1221                if ( 'UTC' !== date_default_timezone_get() ) {
     1222                        $result['status'] = 'critical';
     1223
     1224                        $result['label'] = __( 'PHP default timezone is invalid' );
     1225
     1226                        $result['description'] = sprintf(
     1227                                '<p>%s</p>',
     1228                                sprintf(
     1229                                        /* translators: %s: date_default_timezone_set() */
     1230                                        __( 'PHP default timezone was changed after WordPress loading by a %s function call. This interferes with correct calculations of dates and times.' ),
     1231                                        '<code>date_default_timezone_set()</code>'
     1232                                )
     1233                        );
     1234                }
     1235
     1236                return $result;
     1237        }
     1238
     1239        /**
     1240         * Tests if there's an active PHP session that can affect loopback requests.
     1241         *
     1242         * @since 5.5.0
     1243         *
     1244         * @return array The test results.
     1245         */
     1246        public function get_test_php_sessions() {
     1247                $result = array(
     1248                        'label'       => __( 'No PHP sessions detected' ),
     1249                        'status'      => 'good',
     1250                        'badge'       => array(
     1251                                'label' => __( 'Performance' ),
     1252                                'color' => 'blue',
     1253                        ),
     1254                        'description' => sprintf(
     1255                                '<p>%s</p>',
     1256                                sprintf(
     1257                                        /* translators: 1: session_start(), 2: session_write_close() */
     1258                                        __( '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.' ),
     1259                                        '<code>session_start()</code>',
     1260                                        '<code>session_write_close()</code>'
     1261                                )
     1262                        ),
     1263                        'test'        => 'php_sessions',
     1264                );
     1265
     1266                if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) {
     1267                        $result['status'] = 'critical';
     1268
     1269                        $result['label'] = __( 'An active PHP session was detected' );
     1270
     1271                        $result['description'] = sprintf(
     1272                                '<p>%s</p>',
     1273                                sprintf(
     1274                                        /* translators: 1: session_start(), 2: session_write_close() */
     1275                                        __( '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.' ),
     1276                                        '<code>session_start()</code>',
     1277                                        '<code>session_write_close()</code>'
     1278                                )
     1279                        );
     1280                }
     1281
     1282                return $result;
     1283        }
     1284
     1285        /**
     1286         * Tests if the SQL server is up to date.
     1287         *
     1288         * @since 5.2.0
     1289         *
     1290         * @return array The test results.
     1291         */
     1292        public function get_test_sql_server() {
     1293                if ( ! $this->mysql_server_version ) {
     1294                        $this->prepare_sql_data();
     1295                }
     1296
     1297                $result = array(
     1298                        'label'       => __( 'SQL server is up to date' ),
     1299                        'status'      => 'good',
     1300                        'badge'       => array(
     1301                                'label' => __( 'Performance' ),
     1302                                'color' => 'blue',
     1303                        ),
     1304                        'description' => sprintf(
     1305                                '<p>%s</p>',
     1306                                __( 'The SQL server is a required piece of software for the database WordPress uses to store all your site&#8217;s content and settings.' )
     1307                        ),
     1308                        'actions'     => sprintf(
     1309                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1310                                /* translators: Localized version of WordPress requirements if one exists. */
     1311                                esc_url( __( 'https://wordpress.org/about/requirements/' ) ),
     1312                                __( 'Learn more about what WordPress requires to run.' ),
     1313                                /* translators: Hidden accessibility text. */
     1314                                __( '(opens in a new tab)' )
     1315                        ),
     1316                        'test'        => 'sql_server',
     1317                );
     1318
     1319                $db_dropin = file_exists( WP_CONTENT_DIR . '/db.php' );
     1320
     1321                if ( ! $this->is_recommended_mysql_version ) {
     1322                        $result['status'] = 'recommended';
     1323
     1324                        $result['label'] = __( 'Outdated SQL server' );
     1325
     1326                        $result['description'] .= sprintf(
     1327                                '<p>%s</p>',
     1328                                sprintf(
     1329                                        /* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server recommended version number. */
     1330                                        __( 'For optimal performance and security reasons, you should consider running %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
     1331                                        ( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
     1332                                        $this->mysql_recommended_version
     1333                                )
     1334                        );
     1335                }
     1336
     1337                if ( ! $this->is_acceptable_mysql_version ) {
     1338                        $result['status'] = 'critical';
     1339
     1340                        $result['label']          = __( 'Severely outdated SQL server' );
     1341                        $result['badge']['label'] = __( 'Security' );
     1342
     1343                        $result['description'] .= sprintf(
     1344                                '<p>%s</p>',
     1345                                sprintf(
     1346                                        /* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server minimum version number. */
     1347                                        __( 'WordPress requires %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
     1348                                        ( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
     1349                                        $this->mysql_required_version
     1350                                )
     1351                        );
     1352                }
     1353
     1354                if ( $db_dropin ) {
     1355                        $result['description'] .= sprintf(
     1356                                '<p>%s</p>',
     1357                                wp_kses(
     1358                                        sprintf(
     1359                                                /* translators: 1: The name of the drop-in. 2: The name of the database engine. */
     1360                                                __( 'You are using a %1$s drop-in which might mean that a %2$s database is not being used.' ),
     1361                                                '<code>wp-content/db.php</code>',
     1362                                                ( $this->is_mariadb ? 'MariaDB' : 'MySQL' )
     1363                                        ),
     1364                                        array(
     1365                                                'code' => true,
     1366                                        )
     1367                                )
     1368                        );
     1369                }
     1370
     1371                return $result;
     1372        }
     1373
     1374        /**
     1375         * Tests if the site can communicate with WordPress.org.
     1376         *
     1377         * @since 5.2.0
     1378         *
     1379         * @return array The test results.
     1380         */
     1381        public function get_test_dotorg_communication() {
     1382                $result = array(
     1383                        'label'       => __( 'Can communicate with WordPress.org' ),
     1384                        'status'      => '',
     1385                        'badge'       => array(
     1386                                'label' => __( 'Security' ),
     1387                                'color' => 'blue',
     1388                        ),
     1389                        'description' => sprintf(
     1390                                '<p>%s</p>',
     1391                                __( 'Communicating with the WordPress servers is used to check for new versions, and to both install and update WordPress core, themes or plugins.' )
     1392                        ),
     1393                        'actions'     => '',
     1394                        'test'        => 'dotorg_communication',
     1395                );
     1396
     1397                $wp_dotorg = wp_remote_get(
     1398                        'https://api.wordpress.org',
     1399                        array(
     1400                                'timeout' => 10,
     1401                        )
     1402                );
     1403                if ( ! is_wp_error( $wp_dotorg ) ) {
     1404                        $result['status'] = 'good';
     1405                } else {
     1406                        $result['status'] = 'critical';
     1407
     1408                        $result['label'] = __( 'Could not reach WordPress.org' );
     1409
     1410                        $result['description'] .= sprintf(
     1411                                '<p>%s</p>',
     1412                                sprintf(
     1413                                        '<span class="error"><span class="screen-reader-text">%s</span></span> %s',
     1414                                        /* translators: Hidden accessibility text. */
     1415                                        __( 'Error' ),
     1416                                        sprintf(
     1417                                                /* translators: 1: The IP address WordPress.org resolves to. 2: The error returned by the lookup. */
     1418                                                __( 'Your site is unable to reach WordPress.org at %1$s, and returned the error: %2$s' ),
     1419                                                gethostbyname( 'api.wordpress.org' ),
     1420                                                $wp_dotorg->get_error_message()
     1421                                        )
     1422                                )
     1423                        );
     1424
     1425                        $result['actions'] = sprintf(
     1426                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1427                                /* translators: Localized Support reference. */
     1428                                esc_url( __( 'https://wordpress.org/support/forums/' ) ),
     1429                                __( 'Get help resolving this issue.' ),
     1430                                /* translators: Hidden accessibility text. */
     1431                                __( '(opens in a new tab)' )
     1432                        );
     1433                }
     1434
     1435                return $result;
     1436        }
     1437
     1438        /**
     1439         * Tests if debug information is enabled.
     1440         *
     1441         * When WP_DEBUG is enabled, errors and information may be disclosed to site visitors,
     1442         * or logged to a publicly accessible file.
     1443         *
     1444         * Debugging is also frequently left enabled after looking for errors on a site,
     1445         * as site owners do not understand the implications of this.
     1446         *
     1447         * @since 5.2.0
     1448         *
     1449         * @return array The test results.
     1450         */
     1451        public function get_test_is_in_debug_mode() {
     1452                $result = array(
     1453                        'label'       => __( 'Your site is not set to output debug information' ),
     1454                        'status'      => 'good',
     1455                        'badge'       => array(
     1456                                'label' => __( 'Security' ),
     1457                                'color' => 'blue',
     1458                        ),
     1459                        'description' => sprintf(
     1460                                '<p>%s</p>',
     1461                                __( '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.' )
     1462                        ),
     1463                        'actions'     => sprintf(
     1464                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1465                                /* translators: Documentation explaining debugging in WordPress. */
     1466                                esc_url( __( 'https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/' ) ),
     1467                                __( 'Learn more about debugging in WordPress.' ),
     1468                                /* translators: Hidden accessibility text. */
     1469                                __( '(opens in a new tab)' )
     1470                        ),
     1471                        'test'        => 'is_in_debug_mode',
     1472                );
     1473
     1474                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
     1475                        if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
     1476                                $result['label'] = __( 'Your site is set to log errors to a potentially public file' );
     1477
     1478                                $result['status'] = str_starts_with( ini_get( 'error_log' ), ABSPATH ) ? 'critical' : 'recommended';
     1479
     1480                                $result['description'] .= sprintf(
     1481                                        '<p>%s</p>',
     1482                                        sprintf(
     1483                                                /* translators: %s: WP_DEBUG_LOG */
     1484                                                __( '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.' ),
     1485                                                '<code>WP_DEBUG_LOG</code>'
     1486                                        )
     1487                                );
     1488                        }
     1489
     1490                        if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
     1491                                $result['label'] = __( 'Your site is set to display errors to site visitors' );
     1492
     1493                                $result['status'] = 'critical';
     1494
     1495                                // On development environments, set the status to recommended.
     1496                                if ( $this->is_development_environment() ) {
     1497                                        $result['status'] = 'recommended';
     1498                                }
     1499
     1500                                $result['description'] .= sprintf(
     1501                                        '<p>%s</p>',
     1502                                        sprintf(
     1503                                                /* translators: 1: WP_DEBUG_DISPLAY, 2: WP_DEBUG */
     1504                                                __( '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.' ),
     1505                                                '<code>WP_DEBUG_DISPLAY</code>',
     1506                                                '<code>WP_DEBUG</code>'
     1507                                        )
     1508                                );
     1509                        }
     1510                }
     1511
     1512                return $result;
     1513        }
     1514
     1515        /**
     1516         * Tests if the site is serving content over HTTPS.
     1517         *
     1518         * Many sites have varying degrees of HTTPS support, the most common of which is sites that have it
     1519         * enabled, but only if you visit the right site address.
     1520         *
     1521         * @since 5.2.0
     1522         * @since 5.7.0 Updated to rely on {@see wp_is_using_https()} and {@see wp_is_https_supported()}.
     1523         *
     1524         * @return array The test results.
     1525         */
     1526        public function get_test_https_status() {
     1527                /*
     1528                 * Check HTTPS detection results.
     1529                 */
     1530                $errors = wp_get_https_detection_errors();
     1531
     1532                $default_update_url = wp_get_default_update_https_url();
     1533
     1534                $result = array(
     1535                        'label'       => __( 'Your website is using an active HTTPS connection' ),
     1536                        'status'      => 'good',
     1537                        'badge'       => array(
     1538                                'label' => __( 'Security' ),
     1539                                'color' => 'blue',
     1540                        ),
     1541                        'description' => sprintf(
     1542                                '<p>%s</p>',
     1543                                __( '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.' )
     1544                        ),
     1545                        'actions'     => sprintf(
     1546                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1547                                esc_url( $default_update_url ),
     1548                                __( 'Learn more about why you should use HTTPS' ),
     1549                                /* translators: Hidden accessibility text. */
     1550                                __( '(opens in a new tab)' )
     1551                        ),
     1552                        'test'        => 'https_status',
     1553                );
     1554
     1555                if ( ! wp_is_using_https() ) {
     1556                        /*
     1557                         * If the website is not using HTTPS, provide more information
     1558                         * about whether it is supported and how it can be enabled.
     1559                         */
     1560                        $result['status'] = 'recommended';
     1561                        $result['label']  = __( 'Your website does not use HTTPS' );
     1562
     1563                        if ( wp_is_site_url_using_https() ) {
     1564                                if ( is_ssl() ) {
     1565                                        $result['description'] = sprintf(
     1566                                                '<p>%s</p>',
     1567                                                sprintf(
     1568                                                        /* translators: %s: URL to Settings > General > Site Address. */
     1569                                                        __( 'You are accessing this website using HTTPS, but your <a href="%s">Site Address</a> is not set up to use HTTPS by default.' ),
     1570                                                        esc_url( admin_url( 'options-general.php' ) . '#home' )
     1571                                                )
     1572                                        );
     1573                                } else {
     1574                                        $result['description'] = sprintf(
     1575                                                '<p>%s</p>',
     1576                                                sprintf(
     1577                                                        /* translators: %s: URL to Settings > General > Site Address. */
     1578                                                        __( 'Your <a href="%s">Site Address</a> is not set up to use HTTPS.' ),
     1579                                                        esc_url( admin_url( 'options-general.php' ) . '#home' )
     1580                                                )
     1581                                        );
     1582                                }
     1583                        } else {
     1584                                if ( is_ssl() ) {
     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                                                        __( '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.' ),
     1590                                                        esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
     1591                                                        esc_url( admin_url( 'options-general.php' ) . '#home' )
     1592                                                )
     1593                                        );
     1594                                } else {
     1595                                        $result['description'] = sprintf(
     1596                                                '<p>%s</p>',
     1597                                                sprintf(
     1598                                                        /* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
     1599                                                        __( 'Your <a href="%1$s">WordPress Address</a> and <a href="%2$s">Site Address</a> are not set up to use HTTPS.' ),
     1600                                                        esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
     1601                                                        esc_url( admin_url( 'options-general.php' ) . '#home' )
     1602                                                )
     1603                                        );
     1604                                }
     1605                        }
     1606
     1607                        if ( wp_is_https_supported() ) {
     1608                                $result['description'] .= sprintf(
     1609                                        '<p>%s</p>',
     1610                                        __( 'HTTPS is already supported for your website.' )
     1611                                );
     1612
     1613                                if ( defined( 'WP_HOME' ) || defined( 'WP_SITEURL' ) ) {
     1614                                        $result['description'] .= sprintf(
     1615                                                '<p>%s</p>',
     1616                                                sprintf(
     1617                                                        /* translators: 1: wp-config.php, 2: WP_HOME, 3: WP_SITEURL */
     1618                                                        __( '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.' ),
     1619                                                        '<code>wp-config.php</code>',
     1620                                                        '<code>WP_HOME</code>',
     1621                                                        '<code>WP_SITEURL</code>'
     1622                                                )
     1623                                        );
     1624                                } elseif ( current_user_can( 'update_https' ) ) {
     1625                                        $default_direct_update_url = add_query_arg( 'action', 'update_https', wp_nonce_url( admin_url( 'site-health.php' ), 'wp_update_https' ) );
     1626                                        $direct_update_url         = wp_get_direct_update_https_url();
     1627
     1628                                        if ( ! empty( $direct_update_url ) ) {
     1629                                                $result['actions'] = sprintf(
     1630                                                        '<p class="button-container"><a class="button button-primary" href="%1$s" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1631                                                        esc_url( $direct_update_url ),
     1632                                                        __( 'Update your site to use HTTPS' ),
     1633                                                        /* translators: Hidden accessibility text. */
     1634                                                        __( '(opens in a new tab)' )
     1635                                                );
     1636                                        } else {
     1637                                                $result['actions'] = sprintf(
     1638                                                        '<p class="button-container"><a class="button button-primary" href="%1$s">%2$s</a></p>',
     1639                                                        esc_url( $default_direct_update_url ),
     1640                                                        __( 'Update your site to use HTTPS' )
     1641                                                );
     1642                                        }
     1643                                }
     1644                        } else {
     1645                                // If host-specific "Update HTTPS" URL is provided, include a link.
     1646                                $update_url = wp_get_update_https_url();
     1647                                if ( $update_url !== $default_update_url ) {
     1648                                        $result['description'] .= sprintf(
     1649                                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1650                                                esc_url( $update_url ),
     1651                                                __( 'Talk to your web host about supporting HTTPS for your website.' ),
     1652                                                /* translators: Hidden accessibility text. */
     1653                                                __( '(opens in a new tab)' )
     1654                                        );
     1655                                } else {
     1656                                        $result['description'] .= sprintf(
     1657                                                '<p>%s</p>',
     1658                                                __( 'Talk to your web host about supporting HTTPS for your website.' )
     1659                                        );
     1660                                }
     1661                        }
     1662                }
     1663
     1664                return $result;
     1665        }
     1666
     1667        /**
     1668         * Checks if the HTTP API can handle SSL/TLS requests.
     1669         *
     1670         * @since 5.2.0
     1671         *
     1672         * @return array The test result.
     1673         */
     1674        public function get_test_ssl_support() {
     1675                $result = array(
     1676                        'label'       => '',
     1677                        'status'      => '',
     1678                        'badge'       => array(
     1679                                'label' => __( 'Security' ),
     1680                                'color' => 'blue',
     1681                        ),
     1682                        'description' => sprintf(
     1683                                '<p>%s</p>',
     1684                                __( 'Securely communicating between servers are needed for transactions such as fetching files, conducting sales on store sites, and much more.' )
     1685                        ),
     1686                        'actions'     => '',
     1687                        'test'        => 'ssl_support',
     1688                );
     1689
     1690                $supports_https = wp_http_supports( array( 'ssl' ) );
     1691
     1692                if ( $supports_https ) {
     1693                        $result['status'] = 'good';
     1694
     1695                        $result['label'] = __( 'Your site can communicate securely with other services' );
     1696                } else {
     1697                        $result['status'] = 'critical';
     1698
     1699                        $result['label'] = __( 'Your site is unable to communicate securely with other services' );
     1700
     1701                        $result['description'] .= sprintf(
     1702                                '<p>%s</p>',
     1703                                __( 'Talk to your web host about OpenSSL support for PHP.' )
     1704                        );
     1705                }
     1706
     1707                return $result;
     1708        }
     1709
     1710        /**
     1711         * Tests if scheduled events run as intended.
     1712         *
     1713         * If scheduled events are not running, this may indicate something with WP_Cron is not working
     1714         * as intended, or that there are orphaned events hanging around from older code.
     1715         *
     1716         * @since 5.2.0
     1717         *
     1718         * @return array The test results.
     1719         */
     1720        public function get_test_scheduled_events() {
     1721                $result = array(
     1722                        'label'       => __( 'Scheduled events are running' ),
     1723                        'status'      => 'good',
     1724                        'badge'       => array(
     1725                                'label' => __( 'Performance' ),
     1726                                'color' => 'blue',
     1727                        ),
     1728                        'description' => sprintf(
     1729                                '<p>%s</p>',
     1730                                __( '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.' )
     1731                        ),
     1732                        'actions'     => '',
     1733                        'test'        => 'scheduled_events',
     1734                );
     1735
     1736                $this->wp_schedule_test_init();
     1737
     1738                if ( is_wp_error( $this->has_missed_cron() ) ) {
     1739                        $result['status'] = 'critical';
     1740
     1741                        $result['label'] = __( 'It was not possible to check your scheduled events' );
     1742
     1743                        $result['description'] = sprintf(
     1744                                '<p>%s</p>',
     1745                                sprintf(
     1746                                        /* translators: %s: The error message returned while from the cron scheduler. */
     1747                                        __( 'While trying to test your site&#8217;s scheduled events, the following error was returned: %s' ),
     1748                                        $this->has_missed_cron()->get_error_message()
     1749                                )
     1750                        );
     1751                } elseif ( $this->has_missed_cron() ) {
     1752                        $result['status'] = 'recommended';
     1753
     1754                        $result['label'] = __( 'A scheduled event has failed' );
     1755
     1756                        $result['description'] = sprintf(
     1757                                '<p>%s</p>',
     1758                                sprintf(
     1759                                        /* translators: %s: The name of the failed cron event. */
     1760                                        __( '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.' ),
     1761                                        $this->last_missed_cron
     1762                                )
     1763                        );
     1764                } elseif ( $this->has_late_cron() ) {
     1765                        $result['status'] = 'recommended';
     1766
     1767                        $result['label'] = __( 'A scheduled event is late' );
     1768
     1769                        $result['description'] = sprintf(
     1770                                '<p>%s</p>',
     1771                                sprintf(
     1772                                        /* translators: %s: The name of the late cron event. */
     1773                                        __( '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.' ),
     1774                                        $this->last_late_cron
     1775                                )
     1776                        );
     1777                }
     1778
     1779                return $result;
     1780        }
     1781
     1782        /**
     1783         * Tests if WordPress can run automated background updates.
     1784         *
     1785         * Background updates in WordPress are primarily used for minor releases and security updates.
     1786         * It's important to either have these working, or be aware that they are intentionally disabled
     1787         * for whatever reason.
     1788         *
     1789         * @since 5.2.0
     1790         *
     1791         * @return array The test results.
     1792         */
     1793        public function get_test_background_updates() {
     1794                $result = array(
     1795                        'label'       => __( 'Background updates are working' ),
     1796                        'status'      => 'good',
     1797                        'badge'       => array(
     1798                                'label' => __( 'Security' ),
     1799                                'color' => 'blue',
     1800                        ),
     1801                        'description' => sprintf(
     1802                                '<p>%s</p>',
     1803                                __( 'Background updates ensure that WordPress can auto-update if a security update is released for the version you are currently using.' )
     1804                        ),
     1805                        'actions'     => '',
     1806                        'test'        => 'background_updates',
     1807                );
     1808
     1809                if ( ! class_exists( 'WP_Site_Health_Auto_Updates' ) ) {
     1810                        require_once ABSPATH . 'wp-admin/includes/class-wp-site-health-auto-updates.php';
     1811                }
     1812
     1813                /*
     1814                 * Run the auto-update tests in a separate class,
     1815                 * as there are many considerations to be made.
     1816                 */
     1817                $automatic_updates = new WP_Site_Health_Auto_Updates();
     1818                $tests             = $automatic_updates->run_tests();
     1819
     1820                $output = '<ul>';
     1821
     1822                foreach ( $tests as $test ) {
     1823                        /* translators: Hidden accessibility text. */
     1824                        $severity_string = __( 'Passed' );
     1825
     1826                        if ( 'fail' === $test->severity ) {
     1827                                $result['label'] = __( 'Background updates are not working as expected' );
     1828
     1829                                $result['status'] = 'critical';
     1830
     1831                                /* translators: Hidden accessibility text. */
     1832                                $severity_string = __( 'Error' );
     1833                        }
     1834
     1835                        if ( 'warning' === $test->severity && 'good' === $result['status'] ) {
     1836                                $result['label'] = __( 'Background updates may not be working properly' );
     1837
     1838                                $result['status'] = 'recommended';
     1839
     1840                                /* translators: Hidden accessibility text. */
     1841                                $severity_string = __( 'Warning' );
     1842                        }
     1843
     1844                        $output .= sprintf(
     1845                                '<li><span class="dashicons %s"><span class="screen-reader-text">%s</span></span> %s</li>',
     1846                                esc_attr( $test->severity ),
     1847                                $severity_string,
     1848                                $test->description
     1849                        );
     1850                }
     1851
     1852                $output .= '</ul>';
     1853
     1854                if ( 'good' !== $result['status'] ) {
     1855                        $result['description'] .= $output;
     1856                }
     1857
     1858                return $result;
     1859        }
     1860
     1861        /**
     1862         * Tests if plugin and theme auto-updates appear to be configured correctly.
     1863         *
     1864         * @since 5.5.0
     1865         *
     1866         * @return array The test results.
     1867         */
     1868        public function get_test_plugin_theme_auto_updates() {
     1869                $result = array(
     1870                        'label'       => __( 'Plugin and theme auto-updates appear to be configured correctly' ),
     1871                        'status'      => 'good',
     1872                        'badge'       => array(
     1873                                'label' => __( 'Security' ),
     1874                                'color' => 'blue',
     1875                        ),
     1876                        'description' => sprintf(
     1877                                '<p>%s</p>',
     1878                                __( 'Plugin and theme auto-updates ensure that the latest versions are always installed.' )
     1879                        ),
     1880                        'actions'     => '',
     1881                        'test'        => 'plugin_theme_auto_updates',
     1882                );
     1883
     1884                $check_plugin_theme_updates = $this->detect_plugin_theme_auto_update_issues();
     1885
     1886                $result['status'] = $check_plugin_theme_updates->status;
     1887
     1888                if ( 'good' !== $result['status'] ) {
     1889                        $result['label'] = __( 'Your site may have problems auto-updating plugins and themes' );
     1890
     1891                        $result['description'] .= sprintf(
     1892                                '<p>%s</p>',
     1893                                $check_plugin_theme_updates->message
     1894                        );
     1895                }
     1896
     1897                return $result;
     1898        }
     1899
     1900        /**
     1901         * Tests available disk space for updates.
     1902         *
     1903         * @since 6.3.0
     1904         *
     1905         * @return array The test results.
     1906         */
     1907        public function get_test_available_updates_disk_space() {
     1908                $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;
     1909
     1910                $result = array(
     1911                        'label'       => __( 'Disk space available to safely perform updates' ),
     1912                        'status'      => 'good',
     1913                        'badge'       => array(
     1914                                'label' => __( 'Security' ),
     1915                                'color' => 'blue',
     1916                        ),
     1917                        'description' => sprintf(
     1918                                /* translators: %s: Available disk space in MB or GB. */
     1919                                '<p>' . __( '%s available disk space was detected, update routines can be performed safely.' ) . '</p>',
     1920                                size_format( $available_space )
     1921                        ),
     1922                        'actions'     => '',
     1923                        'test'        => 'available_updates_disk_space',
     1924                );
     1925
     1926                if ( false === $available_space ) {
     1927                        $result['description'] = __( 'Could not determine available disk space for updates.' );
     1928                        $result['status']      = 'recommended';
     1929                } elseif ( $available_space < 20 * MB_IN_BYTES ) {
     1930                        $result['description'] = sprintf(
     1931                                /* translators: %s: Available disk space in MB or GB. */
     1932                                __( 'Available disk space is critically low, less than %s available. Proceed with caution, updates may fail.' ),
     1933                                size_format( 20 * MB_IN_BYTES )
     1934                        );
     1935                        $result['status'] = 'critical';
     1936                } elseif ( $available_space < 100 * MB_IN_BYTES ) {
     1937                        $result['description'] = sprintf(
     1938                                /* translators: %s: Available disk space in MB or GB. */
     1939                                __( 'Available disk space is low, less than %s available.' ),
     1940                                size_format( 100 * MB_IN_BYTES )
     1941                        );
     1942                        $result['status'] = 'recommended';
     1943                }
     1944
     1945                return $result;
     1946        }
     1947
     1948        /**
     1949         * Tests if plugin and theme temporary backup directories are writable or can be created.
     1950         *
     1951         * @since 6.3.0
     1952         *
     1953         * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
     1954         *
     1955         * @return array The test results.
     1956         */
     1957        public function get_test_update_temp_backup_writable() {
     1958                global $wp_filesystem;
     1959
     1960                $result = array(
     1961                        'label'       => __( 'Plugin and theme temporary backup directory is writable' ),
     1962                        'status'      => 'good',
     1963                        'badge'       => array(
     1964                                'label' => __( 'Security' ),
     1965                                'color' => 'blue',
     1966                        ),
     1967                        'description' => sprintf(
     1968                                /* translators: %s: wp-content/upgrade-temp-backup */
     1969                                '<p>' . __( 'The %s directory used to improve the stability of plugin and theme updates is writable.' ) . '</p>',
     1970                                '<code>wp-content/upgrade-temp-backup</code>'
     1971                        ),
     1972                        'actions'     => '',
     1973                        'test'        => 'update_temp_backup_writable',
     1974                );
     1975
     1976                if ( ! function_exists( 'WP_Filesystem' ) ) {
     1977                        require_once ABSPATH . 'wp-admin/includes/file.php';
     1978                }
     1979
     1980                ob_start();
     1981                $credentials = request_filesystem_credentials( '' );
     1982                ob_end_clean();
     1983
     1984                if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
     1985                        $result['status']      = 'recommended';
     1986                        $result['label']       = __( 'Could not access filesystem' );
     1987                        $result['description'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
     1988                        return $result;
     1989                }
     1990
     1991                $wp_content = $wp_filesystem->wp_content_dir();
     1992
     1993                if ( ! $wp_content ) {
     1994                        $result['status']      = 'critical';
     1995                        $result['label']       = __( 'Unable to locate WordPress content directory' );
     1996                        $result['description'] = sprintf(
     1997                                /* translators: %s: wp-content */
     1998                                '<p>' . __( 'The %s directory cannot be located.' ) . '</p>',
     1999                                '<code>wp-content</code>'
     2000                        );
     2001                        return $result;
     2002                }
     2003
     2004                $upgrade_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade" );
     2005                $upgrade_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade" );
     2006                $backup_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade-temp-backup" );
     2007                $backup_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade-temp-backup" );
     2008
     2009                $plugins_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade-temp-backup/plugins" );
     2010                $plugins_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade-temp-backup/plugins" );
     2011                $themes_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade-temp-backup/themes" );
     2012                $themes_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade-temp-backup/themes" );
     2013
     2014                if ( $plugins_dir_exists && ! $plugins_dir_is_writable && $themes_dir_exists && ! $themes_dir_is_writable ) {
     2015                        $result['status']      = 'critical';
     2016                        $result['label']       = __( 'Plugin and theme temporary backup directories exist but are not writable' );
     2017                        $result['description'] = sprintf(
     2018                                /* translators: 1: wp-content/upgrade-temp-backup/plugins, 2: wp-content/upgrade-temp-backup/themes. */
     2019                                '<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>',
     2020                                '<code>wp-content/upgrade-temp-backup/plugins</code>',
     2021                                '<code>wp-content/upgrade-temp-backup/themes</code>'
     2022                        );
     2023                        return $result;
     2024                }
     2025
     2026                if ( $plugins_dir_exists && ! $plugins_dir_is_writable ) {
     2027                        $result['status']      = 'critical';
     2028                        $result['label']       = __( 'Plugin temporary backup directory exists but is not writable' );
     2029                        $result['description'] = sprintf(
     2030                                /* translators: %s: wp-content/upgrade-temp-backup/plugins */
     2031                                '<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>',
     2032                                '<code>wp-content/upgrade-temp-backup/plugins</code>'
     2033                        );
     2034                        return $result;
     2035                }
     2036
     2037                if ( $themes_dir_exists && ! $themes_dir_is_writable ) {
     2038                        $result['status']      = 'critical';
     2039                        $result['label']       = __( 'Theme temporary backup directory exists but is not writable' );
     2040                        $result['description'] = sprintf(
     2041                                /* translators: %s: wp-content/upgrade-temp-backup/themes */
     2042                                '<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>',
     2043                                '<code>wp-content/upgrade-temp-backup/themes</code>'
     2044                        );
     2045                        return $result;
     2046                }
     2047
     2048                if ( ( ! $plugins_dir_exists || ! $themes_dir_exists ) && $backup_dir_exists && ! $backup_dir_is_writable ) {
     2049                        $result['status']      = 'critical';
     2050                        $result['label']       = __( 'The temporary backup directory exists but is not writable' );
     2051                        $result['description'] = sprintf(
     2052                                /* translators: %s: wp-content/upgrade-temp-backup */
     2053                                '<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>',
     2054                                '<code>wp-content/upgrade-temp-backup</code>'
     2055                        );
     2056                        return $result;
     2057                }
     2058
     2059                if ( ! $backup_dir_exists && $upgrade_dir_exists && ! $upgrade_dir_is_writable ) {
     2060                        $result['status']      = 'critical';
     2061                        $result['label']       = __( 'The upgrade directory exists but is not writable' );
     2062                        $result['description'] = sprintf(
     2063                                /* translators: %s: wp-content/upgrade */
     2064                                '<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>',
     2065                                '<code>wp-content/upgrade</code>'
     2066                        );
     2067                        return $result;
     2068                }
     2069
     2070                if ( ! $upgrade_dir_exists && ! $wp_filesystem->is_writable( $wp_content ) ) {
     2071                        $result['status']      = 'critical';
     2072                        $result['label']       = __( 'The upgrade directory cannot be created' );
     2073                        $result['description'] = sprintf(
     2074                                /* translators: 1: wp-content/upgrade, 2: wp-content. */
     2075                                '<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>',
     2076                                '<code>wp-content/upgrade</code>',
     2077                                '<code>wp-content</code>'
     2078                        );
     2079                        return $result;
     2080                }
     2081
     2082                return $result;
     2083        }
     2084
     2085        /**
     2086         * Tests if loopbacks work as expected.
     2087         *
     2088         * A loopback is when WordPress queries itself, for example to start a new WP_Cron instance,
     2089         * or when editing a plugin or theme. This has shown itself to be a recurring issue,
     2090         * as code can very easily break this interaction.
     2091         *
     2092         * @since 5.2.0
     2093         *
     2094         * @return array The test results.
     2095         */
     2096        public function get_test_loopback_requests() {
     2097                $result = array(
     2098                        'label'       => __( 'Your site can perform loopback requests' ),
     2099                        'status'      => 'good',
     2100                        'badge'       => array(
     2101                                'label' => __( 'Performance' ),
     2102                                'color' => 'blue',
     2103                        ),
     2104                        'description' => sprintf(
     2105                                '<p>%s</p>',
     2106                                __( '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.' )
     2107                        ),
     2108                        'actions'     => '',
     2109                        'test'        => 'loopback_requests',
     2110                );
     2111
     2112                $check_loopback = $this->can_perform_loopback();
     2113
     2114                $result['status'] = $check_loopback->status;
     2115
     2116                if ( 'good' !== $result['status'] ) {
     2117                        $result['label'] = __( 'Your site could not complete a loopback request' );
     2118
     2119                        $result['description'] .= sprintf(
     2120                                '<p>%s</p>',
     2121                                $check_loopback->message
     2122                        );
     2123                }
     2124
     2125                return $result;
     2126        }
     2127
     2128        /**
     2129         * Tests if HTTP requests are blocked.
     2130         *
     2131         * It's possible to block all outgoing communication (with the possibility of allowing certain
     2132         * hosts) via the HTTP API. This may create problems for users as many features are running as
     2133         * services these days.
     2134         *
     2135         * @since 5.2.0
     2136         *
     2137         * @return array The test results.
     2138         */
     2139        public function get_test_http_requests() {
     2140                $result = array(
     2141                        'label'       => __( 'HTTP requests seem to be working as expected' ),
     2142                        'status'      => 'good',
     2143                        'badge'       => array(
     2144                                'label' => __( 'Performance' ),
     2145                                'color' => 'blue',
     2146                        ),
     2147                        'description' => sprintf(
     2148                                '<p>%s</p>',
     2149                                __( '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.' )
     2150                        ),
     2151                        'actions'     => '',
     2152                        'test'        => 'http_requests',
     2153                );
     2154
     2155                $blocked = false;
     2156                $hosts   = array();
     2157
     2158                if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) {
     2159                        $blocked = true;
     2160                }
     2161
     2162                if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
     2163                        $hosts = explode( ',', WP_ACCESSIBLE_HOSTS );
     2164                }
     2165
     2166                if ( $blocked && 0 === count( $hosts ) ) {
     2167                        $result['status'] = 'critical';
     2168
     2169                        $result['label'] = __( 'HTTP requests are blocked' );
     2170
     2171                        $result['description'] .= sprintf(
     2172                                '<p>%s</p>',
     2173                                sprintf(
     2174                                        /* translators: %s: Name of the constant used. */
     2175                                        __( 'HTTP requests have been blocked by the %s constant, with no allowed hosts.' ),
     2176                                        '<code>WP_HTTP_BLOCK_EXTERNAL</code>'
     2177                                )
     2178                        );
     2179                }
     2180
     2181                if ( $blocked && 0 < count( $hosts ) ) {
     2182                        $result['status'] = 'recommended';
     2183
     2184                        $result['label'] = __( 'HTTP requests are partially blocked' );
     2185
     2186                        $result['description'] .= sprintf(
     2187                                '<p>%s</p>',
     2188                                sprintf(
     2189                                        /* translators: 1: Name of the constant used. 2: List of allowed hostnames. */
     2190                                        __( 'HTTP requests have been blocked by the %1$s constant, with some allowed hosts: %2$s.' ),
     2191                                        '<code>WP_HTTP_BLOCK_EXTERNAL</code>',
     2192                                        implode( ',', $hosts )
     2193                                )
     2194                        );
     2195                }
     2196
     2197                return $result;
     2198        }
     2199
     2200        /**
     2201         * Tests if the REST API is accessible.
     2202         *
     2203         * Various security measures may block the REST API from working, or it may have been disabled in general.
     2204         * This is required for the new block editor to work, so we explicitly test for this.
     2205         *
     2206         * @since 5.2.0
     2207         *
     2208         * @return array The test results.
     2209         */
     2210        public function get_test_rest_availability() {
     2211                $result = array(
     2212                        'label'       => __( 'The REST API is available' ),
     2213                        'status'      => 'good',
     2214                        'badge'       => array(
     2215                                'label' => __( 'Performance' ),
     2216                                'color' => 'blue',
     2217                        ),
     2218                        'description' => sprintf(
     2219                                '<p>%s</p>',
     2220                                __( 'The REST API is one way that WordPress and other applications communicate with the server. For example, the block editor screen relies on the REST API to display and save your posts and pages.' )
     2221                        ),
     2222                        'actions'     => '',
     2223                        'test'        => 'rest_availability',
     2224                );
     2225
     2226                $cookies = wp_unslash( $_COOKIE );
     2227                $timeout = 10; // 10 seconds.
     2228                $headers = array(
     2229                        'Cache-Control' => 'no-cache',
     2230                        'X-WP-Nonce'    => wp_create_nonce( 'wp_rest' ),
     2231                );
     2232                /** This filter is documented in wp-includes/class-wp-http-streams.php */
     2233                $sslverify = apply_filters( 'https_local_ssl_verify', false );
     2234
     2235                // Include Basic auth in loopback requests.
     2236                if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     2237                        $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     2238                }
     2239
     2240                $url = rest_url( 'wp/v2/types/post' );
     2241
     2242                // The context for this is editing with the new block editor.
     2243                $url = add_query_arg(
     2244                        array(
     2245                                'context' => 'edit',
     2246                        ),
     2247                        $url
     2248                );
     2249
     2250                $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
     2251
     2252                if ( is_wp_error( $r ) ) {
     2253                        $result['status'] = 'critical';
     2254
     2255                        $result['label'] = __( 'The REST API encountered an error' );
     2256
     2257                        $result['description'] .= sprintf(
     2258                                '<p>%s</p><p>%s<br>%s</p>',
     2259                                __( 'When testing the REST API, an error was encountered:' ),
     2260                                sprintf(
     2261                                        // translators: %s: The REST API URL.
     2262                                        __( 'REST API Endpoint: %s' ),
     2263                                        $url
     2264                                ),
     2265                                sprintf(
     2266                                        // translators: 1: The WordPress error code. 2: The WordPress error message.
     2267                                        __( 'REST API Response: (%1$s) %2$s' ),
     2268                                        $r->get_error_code(),
     2269                                        $r->get_error_message()
     2270                                )
     2271                        );
     2272                } elseif ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
     2273                        $result['status'] = 'recommended';
     2274
     2275                        $result['label'] = __( 'The REST API encountered an unexpected result' );
     2276
     2277                        $result['description'] .= sprintf(
     2278                                '<p>%s</p><p>%s<br>%s</p>',
     2279                                __( 'When testing the REST API, an unexpected result was returned:' ),
     2280                                sprintf(
     2281                                        // translators: %s: The REST API URL.
     2282                                        __( 'REST API Endpoint: %s' ),
     2283                                        $url
     2284                                ),
     2285                                sprintf(
     2286                                        // translators: 1: The WordPress error code. 2: The HTTP status code error message.
     2287                                        __( 'REST API Response: (%1$s) %2$s' ),
     2288                                        wp_remote_retrieve_response_code( $r ),
     2289                                        wp_remote_retrieve_response_message( $r )
     2290                                )
     2291                        );
     2292                } else {
     2293                        $json = json_decode( wp_remote_retrieve_body( $r ), true );
     2294
     2295                        if ( false !== $json && ! isset( $json['capabilities'] ) ) {
     2296                                $result['status'] = 'recommended';
     2297
     2298                                $result['label'] = __( 'The REST API did not behave correctly' );
     2299
     2300                                $result['description'] .= sprintf(
     2301                                        '<p>%s</p>',
     2302                                        sprintf(
     2303                                                /* translators: %s: The name of the query parameter being tested. */
     2304                                                __( 'The REST API did not process the %s query parameter correctly.' ),
     2305                                                '<code>context</code>'
     2306                                        )
     2307                                );
     2308                        }
     2309                }
     2310
     2311                return $result;
     2312        }
     2313
     2314        /**
     2315         * Tests if 'file_uploads' directive in PHP.ini is turned off.
     2316         *
     2317         * @since 5.5.0
     2318         *
     2319         * @return array The test results.
     2320         */
     2321        public function get_test_file_uploads() {
     2322                $result = array(
     2323                        'label'       => __( 'Files can be uploaded' ),
     2324                        'status'      => 'good',
     2325                        'badge'       => array(
     2326                                'label' => __( 'Performance' ),
     2327                                'color' => 'blue',
     2328                        ),
     2329                        'description' => sprintf(
     2330                                '<p>%s</p>',
     2331                                sprintf(
     2332                                        /* translators: 1: file_uploads, 2: php.ini */
     2333                                        __( 'The %1$s directive in %2$s determines if uploading files is allowed on your site.' ),
     2334                                        '<code>file_uploads</code>',
     2335                                        '<code>php.ini</code>'
     2336                                )
     2337                        ),
     2338                        'actions'     => '',
     2339                        'test'        => 'file_uploads',
     2340                );
     2341
     2342                if ( ! function_exists( 'ini_get' ) ) {
     2343                        $result['status']       = 'critical';
     2344                        $result['description'] .= sprintf(
     2345                                /* translators: %s: ini_get() */
     2346                                __( 'The %s function has been disabled, some media settings are unavailable because of this.' ),
     2347                                '<code>ini_get()</code>'
     2348                        );
     2349                        return $result;
     2350                }
     2351
     2352                if ( empty( ini_get( 'file_uploads' ) ) ) {
     2353                        $result['status']       = 'critical';
     2354                        $result['description'] .= sprintf(
     2355                                '<p>%s</p>',
     2356                                sprintf(
     2357                                        /* translators: 1: file_uploads, 2: 0 */
     2358                                        __( '%1$s is set to %2$s. You won\'t be able to upload files on your site.' ),
     2359                                        '<code>file_uploads</code>',
     2360                                        '<code>0</code>'
     2361                                )
     2362                        );
     2363                        return $result;
     2364                }
     2365
     2366                $post_max_size       = ini_get( 'post_max_size' );
     2367                $upload_max_filesize = ini_get( 'upload_max_filesize' );
     2368
     2369                if ( wp_convert_hr_to_bytes( $post_max_size ) < wp_convert_hr_to_bytes( $upload_max_filesize ) ) {
     2370                        $result['label'] = sprintf(
     2371                                /* translators: 1: post_max_size, 2: upload_max_filesize */
     2372                                __( 'The "%1$s" value is smaller than "%2$s"' ),
     2373                                'post_max_size',
     2374                                'upload_max_filesize'
     2375                        );
     2376                        $result['status'] = 'recommended';
     2377
     2378                        if ( 0 === wp_convert_hr_to_bytes( $post_max_size ) ) {
     2379                                $result['description'] = sprintf(
     2380                                        '<p>%s</p>',
     2381                                        sprintf(
     2382                                                /* translators: 1: post_max_size, 2: upload_max_filesize */
     2383                                                __( '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.' ),
     2384                                                '<code>post_max_size</code>',
     2385                                                '<code>upload_max_filesize</code>'
     2386                                        )
     2387                                );
     2388                        } else {
     2389                                $result['description'] = sprintf(
     2390                                        '<p>%s</p>',
     2391                                        sprintf(
     2392                                                /* translators: 1: post_max_size, 2: upload_max_filesize */
     2393                                                __( 'The setting for %1$s is smaller than %2$s, this could cause some problems when trying to upload files.' ),
     2394                                                '<code>post_max_size</code>',
     2395                                                '<code>upload_max_filesize</code>'
     2396                                        )
     2397                                );
     2398                        }
     2399
     2400                        return $result;
     2401                }
     2402
     2403                return $result;
     2404        }
     2405
     2406        /**
     2407         * Tests if the Authorization header has the expected values.
     2408         *
     2409         * @since 5.6.0
     2410         *
     2411         * @return array
     2412         */
     2413        public function get_test_authorization_header() {
     2414                $result = array(
     2415                        'label'       => __( 'The Authorization header is working as expected' ),
     2416                        'status'      => 'good',
     2417                        'badge'       => array(
     2418                                'label' => __( 'Security' ),
     2419                                'color' => 'blue',
     2420                        ),
     2421                        'description' => sprintf(
     2422                                '<p>%s</p>',
     2423                                __( 'The Authorization header is used by third-party applications you have approved for this site. Without this header, those apps cannot connect to your site.' )
     2424                        ),
     2425                        'actions'     => '',
     2426                        'test'        => 'authorization_header',
     2427                );
     2428
     2429                if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) {
     2430                        $result['label'] = __( 'The authorization header is missing' );
     2431                } elseif ( 'user' !== $_SERVER['PHP_AUTH_USER'] || 'pwd' !== $_SERVER['PHP_AUTH_PW'] ) {
     2432                        $result['label'] = __( 'The authorization header is invalid' );
     2433                } else {
     2434                        return $result;
     2435                }
     2436
     2437                $result['status']       = 'recommended';
     2438                $result['description'] .= sprintf(
     2439                        '<p>%s</p>',
     2440                        __( 'If you are still seeing this warning after having tried the actions below, you may need to contact your hosting provider for further assistance.' )
     2441                );
     2442
     2443                if ( ! function_exists( 'got_mod_rewrite' ) ) {
     2444                        require_once ABSPATH . 'wp-admin/includes/misc.php';
     2445                }
     2446
     2447                if ( got_mod_rewrite() ) {
     2448                        $result['actions'] .= sprintf(
     2449                                '<p><a href="%s">%s</a></p>',
     2450                                esc_url( admin_url( 'options-permalink.php' ) ),
     2451                                __( 'Flush permalinks' )
     2452                        );
     2453                } else {
     2454                        $result['actions'] .= sprintf(
     2455                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     2456                                __( 'https://developer.wordpress.org/rest-api/frequently-asked-questions/#why-is-authentication-not-working' ),
     2457                                __( 'Learn how to configure the Authorization header.' ),
     2458                                /* translators: Hidden accessibility text. */
     2459                                __( '(opens in a new tab)' )
     2460                        );
     2461                }
     2462
     2463                return $result;
     2464        }
     2465
     2466        /**
     2467         * Tests if a full page cache is available.
     2468         *
     2469         * @since 6.1.0
     2470         *
     2471         * @return array The test result.
     2472         */
     2473        public function get_test_page_cache() {
     2474
     2475            $description  = '<p>' . __( 'Page cache enhances the speed and performance of your site by saving and serving static pages instead of calling for a page every time a user visits.' ) . '</p>';
     2476            $description .= '<p>' . __( 'Page cache is detected by looking for an active page cache plugin as well as making three requests to the homepage and looking for one or more of the following HTTP client caching response headers:' ) . '</p>';
     2477            $description .= '<code>' . implode( '</code>, <code>', array_keys( $this->get_page_cache_headers() ) ) . '.</code>';
     2478
     2479            $result = array(
     2480                'badge'       => array(
     2481                    'label' => __( 'Performance' ),
     2482                    'color' => 'blue',
     2483                ),
     2484                'description' => wp_kses_post( $description ),
     2485                'test'        => 'page_cache',
     2486                'status'      => 'good',
     2487                'label'       => '',
     2488                'actions'     => sprintf(
     2489                    '<p><a href="%1$s" target="_blank" rel="noreferrer">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     2490                    __( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#caching' ),
     2491                    __( 'Learn more about page cache' ),
     2492                    __( '(opens in a new tab)' )
     2493                ),
     2494            );
     2495
     2496            // Get detailed page cache info.
     2497            $page_cache_detail = $this->get_page_cache_detail();
     2498
     2499            if ( is_wp_error( $page_cache_detail ) ) {
     2500                $result['label']  = __( 'Unable to detect the presence of page cache' );
     2501                $result['status'] = 'recommended';
     2502                $error_info       = sprintf(
     2503                    __( 'Unable to detect page cache due to possible loopback request problem. Please verify that the loopback request test is passing. Error: %1$s (Code: %2$s)' ),
     2504                    $page_cache_detail->get_error_message(),
     2505                    $page_cache_detail->get_error_code()
     2506                );
     2507                $result['description'] = wp_kses_post( "<p>$error_info</p>" ) . $result['description'];
     2508                return $result;
     2509            }
     2510
     2511            // Override default WP header detection with YOUR improved detection
     2512            $cache_detected = false;
     2513            if ( ! empty( $page_cache_detail['response_headers_raw'] ) ) {
     2514                foreach ( $page_cache_detail['response_headers_raw'] as $header_group ) {
     2515                    if ( $this->detect_cache_headers( $header_group ) ) {
     2516                        $cache_detected = true;
     2517                        break;
     2518                    }
     2519                }
     2520            }
     2521
     2522            // Rewrite status logic using new detection
     2523            if ( $cache_detected ) {
     2524                $page_cache_detail['headers'] = array( 'cache-detected' ); // fake entry to trigger WP's UI output
     2525                $page_cache_detail['status']  = 'good';
     2526            } else {
     2527                $page_cache_detail['headers'] = array();
     2528                $page_cache_detail['status']  = 'recommended';
     2529            }
     2530
     2531            $result['status'] = $page_cache_detail['status'];
     2532
     2533            switch ( $page_cache_detail['status'] ) {
     2534                case 'recommended':
     2535                    $result['label'] = __( 'Page cache is not detected but the server response time is OK' );
     2536                    break;
     2537
     2538                case 'good':
     2539                    $result['label'] = __( 'Page cache is detected and the server response time is good' );
     2540                    break;
     2541
     2542                default:
     2543                    if ( empty( $page_cache_detail['headers'] ) && ! $page_cache_detail['advanced_cache_present'] ) {
     2544                        $result['label'] = __( 'Page cache is not detected and the server response time is slow' );
     2545                    } else {
     2546                        $result['label'] = __( 'Page cache is detected but the server response time is still slow' );
     2547                    }
     2548            }
     2549
     2550            $page_cache_test_summary = array();
     2551
     2552            // Response time
     2553            if ( empty( $page_cache_detail['response_time'] ) ) {
     2554                $page_cache_test_summary[] = '<span class="dashicons dashicons-dismiss"></span> ' . __( 'Server response time could not be determined. Verify that loopback requests are working.' );
     2555            } else {
     2556                $threshold = $this->get_good_response_time_threshold();
     2557
     2558                if ( $page_cache_detail['response_time'] < $threshold ) {
     2559                    $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . sprintf(
     2560                        __( 'Median server response time was %1$s milliseconds. This is less than the recommended %2$s milliseconds threshold.' ),
     2561                        number_format_i18n( $page_cache_detail['response_time'] ),
     2562                        number_format_i18n( $threshold )
     2563                    );
     2564                } else {
     2565                    $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . sprintf(
     2566                        __( 'Median server response time was %1$s milliseconds. It should be less than the recommended %2$s milliseconds threshold.' ),
     2567                        number_format_i18n( $page_cache_detail['response_time'] ),
     2568                        number_format_i18n( $threshold )
     2569                    );
     2570                }
     2571
     2572                // Your new header detection integrated here
     2573                if ( ! $cache_detected ) {
     2574                    $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'No client caching response headers were detected.' );
     2575                } else {
     2576                    $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' .
     2577                        __( 'Page cache headers were detected using enhanced header analysis.' );
     2578                }
     2579            }
     2580
     2581            // Page cache plugin availability
     2582            if ( $page_cache_detail['advanced_cache_present'] ) {
     2583                $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . __( 'A page cache plugin was detected.' );
     2584            } elseif ( ! $cache_detected ) {
     2585                $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'A page cache plugin was not detected.' );
     2586            }
     2587
     2588            $result['description'] .= '<ul><li>' . implode( '</li><li>', $page_cache_test_summary ) . '</li></ul>';
     2589
     2590            return $result;
     2591        }
     2592
     2593        /**
     2594         * Tests if the site uses persistent object cache and recommends to use it if not.
     2595         *
     2596         * @since 6.1.0
     2597         *
     2598         * @return array The test result.
     2599         */
     2600        public function get_test_persistent_object_cache() {
     2601                /**
     2602                 * Filters the action URL for the persistent object cache health check.
     2603                 *
     2604                 * @since 6.1.0
     2605                 *
     2606                 * @param string $action_url Learn more link for persistent object cache health check.
     2607                 */
     2608                $action_url = apply_filters(
     2609                        'site_status_persistent_object_cache_url',
     2610                        /* translators: Localized Support reference. */
     2611                        __( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#persistent-object-cache' )
     2612                );
     2613
     2614                $result = array(
     2615                        'test'        => 'persistent_object_cache',
     2616                        'status'      => 'good',
     2617                        'badge'       => array(
     2618                                'label' => __( 'Performance' ),
     2619                                'color' => 'blue',
     2620                        ),
     2621                        'label'       => __( 'A persistent object cache is being used' ),
     2622                        'description' => sprintf(
     2623                                '<p>%s</p>',
     2624                                __( 'A persistent object cache makes your site&#8217;s database more efficient, resulting in faster load times because WordPress can retrieve your site&#8217;s content and settings much more quickly.' )
     2625                        ),
     2626                        'actions'     => sprintf(
     2627                                '<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     2628                                esc_url( $action_url ),
     2629                                __( 'Learn more about persistent object caching.' ),
     2630                                /* translators: Hidden accessibility text. */
     2631                                __( '(opens in a new tab)' )
     2632                        ),
     2633                );
     2634
     2635                if ( wp_using_ext_object_cache() ) {
     2636                        return $result;
     2637                }
     2638
     2639                if ( ! $this->should_suggest_persistent_object_cache() ) {
     2640                        $result['label'] = __( 'A persistent object cache is not required' );
     2641
     2642                        return $result;
     2643                }
     2644
     2645                $available_services = $this->available_object_cache_services();
     2646
     2647                $notes = __( 'Your hosting provider can tell you if a persistent object cache can be enabled on your site.' );
     2648
     2649                if ( ! empty( $available_services ) ) {
     2650                        $notes .= ' ' . sprintf(
     2651                                /* translators: Available object caching services. */
     2652                                __( 'Your host appears to support the following object caching services: %s.' ),
     2653                                implode( ', ', $available_services )
     2654                        );
     2655                }
     2656
     2657                /**
     2658                 * Filters the second paragraph of the health check's description
     2659                 * when suggesting the use of a persistent object cache.
     2660                 *
     2661                 * Hosts may want to replace the notes to recommend their preferred object caching solution.
     2662                 *
     2663                 * Plugin authors may want to append notes (not replace) on why object caching is recommended for their plugin.
     2664                 *
     2665                 * @since 6.1.0
     2666                 *
     2667                 * @param string   $notes              The notes appended to the health check description.
     2668                 * @param string[] $available_services The list of available persistent object cache services.
     2669                 */
     2670                $notes = apply_filters( 'site_status_persistent_object_cache_notes', $notes, $available_services );
     2671
     2672                $result['status']       = 'recommended';
     2673                $result['label']        = __( 'You should use a persistent object cache' );
     2674                $result['description'] .= sprintf(
     2675                        '<p>%s</p>',
     2676                        wp_kses(
     2677                                $notes,
     2678                                array(
     2679                                        'a'      => array( 'href' => true ),
     2680                                        'code'   => true,
     2681                                        'em'     => true,
     2682                                        'strong' => true,
     2683                                )
     2684                        )
     2685                );
     2686
     2687                return $result;
     2688        }
     2689
     2690        /**
     2691         * Calculates total amount of autoloaded data.
     2692         *
     2693         * @since 6.6.0
     2694         *
     2695         * @return int Autoloaded data in bytes.
     2696         */
     2697        public function get_autoloaded_options_size() {
     2698                $alloptions = wp_load_alloptions();
     2699
     2700                $total_length = 0;
     2701
     2702                foreach ( $alloptions as $option_value ) {
     2703                        if ( is_array( $option_value ) || is_object( $option_value ) ) {
     2704                                $option_value = maybe_serialize( $option_value );
     2705                        }
     2706                        $total_length += strlen( (string) $option_value );
     2707                }
     2708
     2709                return $total_length;
     2710        }
     2711
     2712        /**
     2713         * Tests the number of autoloaded options.
     2714         *
     2715         * @since 6.6.0
     2716         *
     2717         * @return array The test results.
     2718         */
     2719        public function get_test_autoloaded_options() {
     2720                $autoloaded_options_size  = $this->get_autoloaded_options_size();
     2721                $autoloaded_options_count = count( wp_load_alloptions() );
     2722
     2723                $base_description = __( 'Autoloaded options are configuration settings for plugins and themes that are automatically loaded with every page load in WordPress. Having too many autoloaded options can slow down your site.' );
     2724
     2725                $result = array(
     2726                        'label'       => __( 'Autoloaded options are acceptable' ),
     2727                        'status'      => 'good',
     2728                        'badge'       => array(
     2729                                'label' => __( 'Performance' ),
     2730                                'color' => 'blue',
     2731                        ),
     2732                        'description' => sprintf(
     2733                                /* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */
     2734                                '<p>' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which is acceptable.' ) . '</p>',
     2735                                $autoloaded_options_count,
     2736                                size_format( $autoloaded_options_size )
     2737                        ),
     2738                        'actions'     => '',
     2739                        'test'        => 'autoloaded_options',
     2740                );
     2741
     2742                /**
     2743                 * Filters max bytes threshold to trigger warning in Site Health.
     2744                 *
     2745                 * @since 6.6.0
     2746                 *
     2747                 * @param int $limit Autoloaded options threshold size. Default 800000.
     2748                 */
     2749                $limit = apply_filters( 'site_status_autoloaded_options_size_limit', 800000 );
     2750
     2751                if ( $autoloaded_options_size < $limit ) {
     2752                        return $result;
     2753                }
     2754
     2755                $result['status']      = 'critical';
     2756                $result['label']       = __( 'Autoloaded options could affect performance' );
     2757                $result['description'] = sprintf(
     2758                        /* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */
     2759                        '<p>' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which could cause your site to be slow. You can review the options being autoloaded in your database and remove any options that are no longer needed by your site.' ) . '</p>',
     2760                        $autoloaded_options_count,
     2761                        size_format( $autoloaded_options_size )
     2762                );
     2763
     2764                /**
     2765                 * Filters description to be shown on Site Health warning when threshold is met.
     2766                 *
     2767                 * @since 6.6.0
     2768                 *
     2769                 * @param string $description Description message when autoloaded options bigger than threshold.
     2770                 */
     2771                $result['description'] = apply_filters( 'site_status_autoloaded_options_limit_description', $result['description'] );
     2772
     2773                $result['actions'] = sprintf(
     2774                        /* translators: 1: HelpHub URL, 2: Link description. */
     2775                        '<p><a target="_blank" href="%1$s">%2$s</a></p>',
     2776                        esc_url( __( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#autoloaded-options' ) ),
     2777                        __( 'More info about optimizing autoloaded options' )
     2778                );
     2779
     2780                /**
     2781                 * Filters actionable information to tackle the problem. It can be a link to an external guide.
     2782                 *
     2783                 * @since 6.6.0
     2784                 *
     2785                 * @param string $actions Call to Action to be used to point to the right direction to solve the issue.
     2786                 */
     2787                $result['actions'] = apply_filters( 'site_status_autoloaded_options_action_to_perform', $result['actions'] );
     2788                return $result;
     2789        }
     2790
     2791        /**
     2792         * Tests whether search engine indexing is enabled.
     2793         *
     2794         * Surfaces as ΓÇ£goodΓÇ¥ if `blog_public === 1`, or ΓÇ£recommendedΓÇ¥ if `blog_public === 0`.
     2795         *
     2796         * @since 6.9.0
     2797         *
     2798         * @return array The test results.
     2799         */
     2800        public function get_test_search_engine_visibility() {
     2801                $result = array(
     2802                        'label'       => __( 'Search engine indexing is enabled.', 'default' ),
     2803                        'status'      => 'good',
     2804                        'badge'       => array(
     2805                                'label' => __( 'Privacy', 'default' ),
     2806                                'color' => 'blue',
     2807                        ),
     2808                        'description' => sprintf(
     2809                                '<p>%s</p>',
     2810                                __( 'Search engines can crawl and index your site. No action needed.', 'default' )
     2811                        ),
     2812                        'actions'     => sprintf(
     2813                                '<p><a href="%1$s">%2$s</a></p>',
     2814                                esc_url( admin_url( 'options-reading.php#blog_public' ) ),
     2815                                __( 'Review your visibility settings', 'default' )
     2816                        ),
     2817                        'test'        => 'search_engine_visibility',
     2818                );
     2819
     2820                // If indexing is discouraged, flip to ΓÇ£recommendedΓÇ¥:
     2821                if ( ! get_option( 'blog_public' ) ) {
     2822                        $result['status']         = 'recommended';
     2823                        $result['label']          = __( 'Search engines are discouraged from indexing this site.', 'default' );
     2824                        $result['badge']['color'] = 'blue';
     2825                        $result['description']    = sprintf(
     2826                                '<p>%s</p>',
     2827                                __( 'Your site is hidden from search engines. Consider enabling indexing if this is a public site.', 'default' )
     2828                        );
     2829                }
     2830
     2831                return $result;
     2832        }
     2833
     2834        /**
     2835         * Returns a set of tests that belong to the site status page.
     2836         *
     2837         * Each site status test is defined here, they may be `direct` tests, that run on page load, or `async` tests
     2838         * which will run later down the line via JavaScript calls to improve page performance and hopefully also user
     2839         * experiences.
     2840         *
     2841         * @since 5.2.0
     2842         * @since 5.6.0 Added support for `has_rest` and `permissions`.
     2843         *
     2844         * @return array The list of tests to run.
     2845         */
     2846        public static function get_tests() {
     2847                $tests = array(
     2848                        'direct' => array(
     2849                                'wordpress_version'            => array(
     2850                                        'label' => __( 'WordPress Version' ),
     2851                                        'test'  => 'wordpress_version',
     2852                                ),
     2853                                'plugin_version'               => array(
     2854                                        'label' => __( 'Plugin Versions' ),
     2855                                        'test'  => 'plugin_version',
     2856                                ),
     2857                                'theme_version'                => array(
     2858                                        'label' => __( 'Theme Versions' ),
     2859                                        'test'  => 'theme_version',
     2860                                ),
     2861                                'php_version'                  => array(
     2862                                        'label' => __( 'PHP Version' ),
     2863                                        'test'  => 'php_version',
     2864                                ),
     2865                                'php_extensions'               => array(
     2866                                        'label' => __( 'PHP Extensions' ),
     2867                                        'test'  => 'php_extensions',
     2868                                ),
     2869                                'php_default_timezone'         => array(
     2870                                        'label' => __( 'PHP Default Timezone' ),
     2871                                        'test'  => 'php_default_timezone',
     2872                                ),
     2873                                'php_sessions'                 => array(
     2874                                        'label' => __( 'PHP Sessions' ),
     2875                                        'test'  => 'php_sessions',
     2876                                ),
     2877                                'sql_server'                   => array(
     2878                                        'label' => __( 'Database Server version' ),
     2879                                        'test'  => 'sql_server',
     2880                                ),
     2881                                'ssl_support'                  => array(
     2882                                        'label' => __( 'Secure communication' ),
     2883                                        'test'  => 'ssl_support',
     2884                                ),
     2885                                'scheduled_events'             => array(
     2886                                        'label' => __( 'Scheduled events' ),
     2887                                        'test'  => 'scheduled_events',
     2888                                ),
     2889                                'http_requests'                => array(
     2890                                        'label' => __( 'HTTP Requests' ),
     2891                                        'test'  => 'http_requests',
     2892                                ),
     2893                                'rest_availability'            => array(
     2894                                        'label'     => __( 'REST API availability' ),
     2895                                        'test'      => 'rest_availability',
     2896                                        'skip_cron' => true,
     2897                                ),
     2898                                'debug_enabled'                => array(
     2899                                        'label' => __( 'Debugging enabled' ),
     2900                                        'test'  => 'is_in_debug_mode',
     2901                                ),
     2902                                'file_uploads'                 => array(
     2903                                        'label' => __( 'File uploads' ),
     2904                                        'test'  => 'file_uploads',
     2905                                ),
     2906                                'plugin_theme_auto_updates'    => array(
     2907                                        'label' => __( 'Plugin and theme auto-updates' ),
     2908                                        'test'  => 'plugin_theme_auto_updates',
     2909                                ),
     2910                                'update_temp_backup_writable'  => array(
     2911                                        'label' => __( 'Plugin and theme temporary backup directory access' ),
     2912                                        'test'  => 'update_temp_backup_writable',
     2913                                ),
     2914                                'available_updates_disk_space' => array(
     2915                                        'label' => __( 'Available disk space' ),
     2916                                        'test'  => 'available_updates_disk_space',
     2917                                ),
     2918                                'autoloaded_options'           => array(
     2919                                        'label' => __( 'Autoloaded options' ),
     2920                                        'test'  => 'autoloaded_options',
     2921                                ),
     2922                                'search_engine_visibility'     => array(
     2923                                        'label' => __( 'Search Engine Visibility' ),
     2924                                        'test'  => 'search_engine_visibility',
     2925                                ),
     2926                        ),
     2927                        'async'  => array(
     2928                                'dotorg_communication' => array(
     2929                                        'label'             => __( 'Communication with WordPress.org' ),
     2930                                        'test'              => rest_url( 'wp-site-health/v1/tests/dotorg-communication' ),
     2931                                        'has_rest'          => true,
     2932                                        'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_dotorg_communication' ),
     2933                                ),
     2934                                'background_updates'   => array(
     2935                                        'label'             => __( 'Background updates' ),
     2936                                        'test'              => rest_url( 'wp-site-health/v1/tests/background-updates' ),
     2937                                        'has_rest'          => true,
     2938                                        'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_background_updates' ),
     2939                                ),
     2940                                'loopback_requests'    => array(
     2941                                        'label'             => __( 'Loopback request' ),
     2942                                        'test'              => rest_url( 'wp-site-health/v1/tests/loopback-requests' ),
     2943                                        'has_rest'          => true,
     2944                                        'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_loopback_requests' ),
     2945                                ),
     2946                                'https_status'         => array(
     2947                                        'label'             => __( 'HTTPS status' ),
     2948                                        'test'              => rest_url( 'wp-site-health/v1/tests/https-status' ),
     2949                                        'has_rest'          => true,
     2950                                        'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_https_status' ),
     2951                                ),
     2952                        ),
     2953                );
     2954
     2955                // Conditionally include Authorization header test if the site isn't protected by Basic Auth.
     2956                if ( ! wp_is_site_protected_by_basic_auth() ) {
     2957                        $tests['async']['authorization_header'] = array(
     2958                                'label'     => __( 'Authorization header' ),
     2959                                'test'      => rest_url( 'wp-site-health/v1/tests/authorization-header' ),
     2960                                'has_rest'  => true,
     2961                                'headers'   => array( 'Authorization' => 'Basic ' . base64_encode( 'user:pwd' ) ),
     2962                                'skip_cron' => true,
     2963                        );
     2964                }
     2965
     2966                // Only check for caches in production environments.
     2967                if ( 'production' === wp_get_environment_type() ) {
     2968                        $tests['async']['page_cache'] = array(
     2969                                'label'             => __( 'Page cache' ),
     2970                                'test'              => rest_url( 'wp-site-health/v1/tests/page-cache' ),
     2971                                'has_rest'          => true,
     2972                                'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_page_cache' ),
     2973                        );
     2974
     2975                        $tests['direct']['persistent_object_cache'] = array(
     2976                                'label' => __( 'Persistent object cache' ),
     2977                                'test'  => 'persistent_object_cache',
     2978                        );
     2979                }
     2980
     2981                /**
     2982                 * Filters which site status tests are run on a site.
     2983                 *
     2984                 * The site health is determined by a set of tests based on best practices from
     2985                 * both the WordPress Hosting Team and web standards in general.
     2986                 *
     2987                 * Some sites may not have the same requirements, for example the automatic update
     2988                 * checks may be handled by a host, and are therefore disabled in core.
     2989                 * Or maybe you want to introduce a new test, is caching enabled/disabled/stale for example.
     2990                 *
     2991                 * Tests may be added either as direct, or asynchronous ones. Any test that may require some time
     2992                 * to complete should run asynchronously, to avoid extended loading periods within wp-admin.
     2993                 *
     2994                 * @since 5.2.0
     2995                 * @since 5.6.0 Added the `async_direct_test` array key for asynchronous tests.
     2996                 *              Added the `skip_cron` array key for all tests.
     2997                 *
     2998                 * @param array[] $tests {
     2999                 *     An associative array of direct and asynchronous tests.
     3000                 *
     3001                 *     @type array[] $direct {
     3002                 *         An array of direct tests.
     3003                 *
     3004                 *         @type array ...$identifier {
     3005                 *             `$identifier` should be a unique identifier for the test. Plugins and themes are encouraged to
     3006                 *             prefix test identifiers with their slug to avoid collisions between tests.
     3007                 *
     3008                 *             @type string   $label     The friendly label to identify the test.
     3009                 *             @type callable $test      The callback function that runs the test and returns its result.
     3010                 *             @type bool     $skip_cron Whether to skip this test when running as cron.
     3011                 *         }
     3012                 *     }
     3013                 *     @type array[] $async {
     3014                 *         An array of asynchronous tests.
     3015                 *
     3016                 *         @type array ...$identifier {
     3017                 *             `$identifier` should be a unique identifier for the test. Plugins and themes are encouraged to
     3018                 *             prefix test identifiers with their slug to avoid collisions between tests.
     3019                 *
     3020                 *             @type string   $label             The friendly label to identify the test.
     3021                 *             @type string   $test              An admin-ajax.php action to be called to perform the test, or
     3022                 *                                               if `$has_rest` is true, a URL to a REST API endpoint to perform
     3023                 *                                               the test.
     3024                 *             @type bool     $has_rest          Whether the `$test` property points to a REST API endpoint.
     3025                 *             @type bool     $skip_cron         Whether to skip this test when running as cron.
     3026                 *             @type callable $async_direct_test A manner of directly calling the test marked as asynchronous,
     3027                 *                                               as the scheduled event can not authenticate, and endpoints
     3028                 *                                               may require authentication.
     3029                 *         }
     3030                 *     }
     3031                 * }
     3032                 */
     3033                $tests = apply_filters( 'site_status_tests', $tests );
     3034
     3035                // Ensure that the filtered tests contain the required array keys.
     3036                $tests = array_merge(
     3037                        array(
     3038                                'direct' => array(),
     3039                                'async'  => array(),
     3040                        ),
     3041                        $tests
     3042                );
     3043
     3044                return $tests;
     3045        }
     3046
     3047        /**
     3048         * Adds a class to the body HTML tag.
     3049         *
     3050         * Filters the body class string for admin pages and adds our own class for easier styling.
     3051         *
     3052         * @since 5.2.0
     3053         *
     3054         * @param string $body_class The body class string.
     3055         * @return string The modified body class string.
     3056         */
     3057        public function admin_body_class( $body_class ) {
     3058                $screen = get_current_screen();
     3059                if ( 'site-health' !== $screen->id ) {
     3060                        return $body_class;
     3061                }
     3062
     3063                $body_class .= ' site-health';
     3064
     3065                return $body_class;
     3066        }
     3067
     3068        /**
     3069         * Initiates the WP_Cron schedule test cases.
     3070         *
     3071         * @since 5.2.0
     3072         */
     3073        private function wp_schedule_test_init() {
     3074                $this->schedules = wp_get_schedules();
     3075                $this->get_cron_tasks();
     3076        }
     3077
     3078        /**
     3079         * Populates the list of cron events and store them to a class-wide variable.
     3080         *
     3081         * @since 5.2.0
     3082         */
     3083        private function get_cron_tasks() {
     3084                $cron_tasks = _get_cron_array();
     3085
     3086                if ( empty( $cron_tasks ) ) {
     3087                        $this->crons = new WP_Error( 'no_tasks', __( 'No scheduled events exist on this site.' ) );
     3088                        return;
     3089                }
     3090
     3091                $this->crons = array();
     3092
     3093                foreach ( $cron_tasks as $time => $cron ) {
     3094                        foreach ( $cron as $hook => $dings ) {
     3095                                foreach ( $dings as $sig => $data ) {
     3096
     3097                                        $this->crons[ "$hook-$sig-$time" ] = (object) array(
     3098                                                'hook'     => $hook,
     3099                                                'time'     => $time,
     3100                                                'sig'      => $sig,
     3101                                                'args'     => $data['args'],
     3102                                                'schedule' => $data['schedule'],
     3103                                                'interval' => isset( $data['interval'] ) ? $data['interval'] : null,
     3104                                        );
     3105
     3106                                }
     3107                        }
     3108                }
     3109        }
     3110
     3111        /**
     3112         * Checks if any scheduled tasks have been missed.
     3113         *
     3114         * Returns a boolean value of `true` if a scheduled task has been missed and ends processing.
     3115         *
     3116         * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
     3117         *
     3118         * @since 5.2.0
     3119         *
     3120         * @return bool|WP_Error True if a cron was missed, false if not. WP_Error if the cron is set to that.
     3121         */
     3122        public function has_missed_cron() {
     3123                if ( is_wp_error( $this->crons ) ) {
     3124                        return $this->crons;
     3125                }
     3126
     3127                foreach ( $this->crons as $id => $cron ) {
     3128                        if ( ( $cron->time - time() ) < $this->timeout_missed_cron ) {
     3129                                $this->last_missed_cron = $cron->hook;
     3130                                return true;
     3131                        }
     3132                }
     3133
     3134                return false;
     3135        }
     3136
     3137        /**
     3138         * Checks if any scheduled tasks are late.
     3139         *
     3140         * Returns a boolean value of `true` if a scheduled task is late and ends processing.
     3141         *
     3142         * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
     3143         *
     3144         * @since 5.3.0
     3145         *
     3146         * @return bool|WP_Error True if a cron is late, false if not. WP_Error if the cron is set to that.
     3147         */
     3148        public function has_late_cron() {
     3149                if ( is_wp_error( $this->crons ) ) {
     3150                        return $this->crons;
     3151                }
     3152
     3153                foreach ( $this->crons as $id => $cron ) {
     3154                        $cron_offset = $cron->time - time();
     3155                        if (
     3156                                $cron_offset >= $this->timeout_missed_cron &&
     3157                                $cron_offset < $this->timeout_late_cron
     3158                        ) {
     3159                                $this->last_late_cron = $cron->hook;
     3160                                return true;
     3161                        }
     3162                }
     3163
     3164                return false;
     3165        }
     3166
     3167        /**
     3168         * Checks for potential issues with plugin and theme auto-updates.
     3169         *
     3170         * Though there is no way to 100% determine if plugin and theme auto-updates are configured
     3171         * correctly, a few educated guesses could be made to flag any conditions that would
     3172         * potentially cause unexpected behaviors.
     3173         *
     3174         * @since 5.5.0
     3175         *
     3176         * @return object The test results.
     3177         */
     3178        public function detect_plugin_theme_auto_update_issues() {
     3179                $mock_plugin = (object) array(
     3180                        'id'            => 'w.org/plugins/a-fake-plugin',
     3181                        'slug'          => 'a-fake-plugin',
     3182                        'plugin'        => 'a-fake-plugin/a-fake-plugin.php',
     3183                        'new_version'   => '9.9',
     3184                        'url'           => 'https://wordpress.org/plugins/a-fake-plugin/',
     3185                        'package'       => 'https://downloads.wordpress.org/plugin/a-fake-plugin.9.9.zip',
     3186                        'icons'         => array(
     3187                                '2x' => 'https://ps.w.org/a-fake-plugin/assets/icon-256x256.png',
     3188                                '1x' => 'https://ps.w.org/a-fake-plugin/assets/icon-128x128.png',
     3189                        ),
     3190                        'banners'       => array(
     3191                                '2x' => 'https://ps.w.org/a-fake-plugin/assets/banner-1544x500.png',
     3192                                '1x' => 'https://ps.w.org/a-fake-plugin/assets/banner-772x250.png',
     3193                        ),
     3194                        'banners_rtl'   => array(),
     3195                        'tested'        => '5.5.0',
     3196                        'requires_php'  => '5.6.20',
     3197                        'compatibility' => new stdClass(),
     3198                );
     3199
     3200                $mock_theme = (object) array(
     3201                        'theme'        => 'a-fake-theme',
     3202                        'new_version'  => '9.9',
     3203                        'url'          => 'https://wordpress.org/themes/a-fake-theme/',
     3204                        'package'      => 'https://downloads.wordpress.org/theme/a-fake-theme.9.9.zip',
     3205                        'requires'     => '5.0.0',
     3206                        'requires_php' => '5.6.20',
     3207                );
     3208
     3209                $test_plugins_enabled = wp_is_auto_update_forced_for_item( 'plugin', true, $mock_plugin );
     3210                $test_themes_enabled  = wp_is_auto_update_forced_for_item( 'theme', true, $mock_theme );
     3211
     3212                $ui_enabled_for_plugins = wp_is_auto_update_enabled_for_type( 'plugin' );
     3213                $ui_enabled_for_themes  = wp_is_auto_update_enabled_for_type( 'theme' );
     3214                $plugin_filter_present  = has_filter( 'auto_update_plugin' );
     3215                $theme_filter_present   = has_filter( 'auto_update_theme' );
     3216
     3217                if ( ( ! $test_plugins_enabled && $ui_enabled_for_plugins )
     3218                        || ( ! $test_themes_enabled && $ui_enabled_for_themes )
     3219                ) {
     3220                        return (object) array(
     3221                                'status'  => 'critical',
     3222                                '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.' ),
     3223                        );
     3224                }
     3225
     3226                if ( ( ! $test_plugins_enabled && $plugin_filter_present )
     3227                        && ( ! $test_themes_enabled && $theme_filter_present )
     3228                ) {
     3229                        return (object) array(
     3230                                'status'  => 'recommended',
     3231                                'message' => __( 'Auto-updates for plugins and themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
     3232                        );
     3233                } elseif ( ! $test_plugins_enabled && $plugin_filter_present ) {
     3234                        return (object) array(
     3235                                'status'  => 'recommended',
     3236                                'message' => __( 'Auto-updates for plugins appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
     3237                        );
     3238                } elseif ( ! $test_themes_enabled && $theme_filter_present ) {
     3239                        return (object) array(
     3240                                'status'  => 'recommended',
     3241                                'message' => __( 'Auto-updates for themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
     3242                        );
     3243                }
     3244
     3245                return (object) array(
     3246                        'status'  => 'good',
     3247                        'message' => __( 'There appear to be no issues with plugin and theme auto-updates.' ),
     3248                );
     3249        }
     3250
     3251        /**
     3252         * Runs a loopback test on the site.
     3253         *
     3254         * Loopbacks are what WordPress uses to communicate with itself to start up WP_Cron, scheduled posts,
     3255         * make sure plugin or theme edits don't cause site failures and similar.
     3256         *
     3257         * @since 5.2.0
     3258         *
     3259         * @return object The test results.
     3260         */
     3261        public function can_perform_loopback() {
     3262                $body    = array( 'site-health' => 'loopback-test' );
     3263                $cookies = wp_unslash( $_COOKIE );
     3264                $timeout = 10; // 10 seconds.
     3265                $headers = array(
     3266                        'Cache-Control' => 'no-cache',
     3267                );
     3268                /** This filter is documented in wp-includes/class-wp-http-streams.php */
     3269                $sslverify = apply_filters( 'https_local_ssl_verify', false );
     3270
     3271                // Include Basic auth in loopback requests.
     3272                if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     3273                        $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     3274                }
     3275
     3276                $url = site_url( 'wp-cron.php' );
     3277
     3278                /*
     3279                 * A post request is used for the wp-cron.php loopback test to cause the file
     3280                 * to finish early without triggering cron jobs. This has two benefits:
     3281                 * - cron jobs are not triggered a second time on the site health page,
     3282                 * - the loopback request finishes sooner providing a quicker result.
     3283                 *
     3284                 * Using a POST request causes the loopback to differ slightly to the standard
     3285                 * GET request WordPress uses for wp-cron.php loopback requests but is close
     3286                 * enough. See https://core.trac.wordpress.org/ticket/52547
     3287                 */
     3288                $r = wp_remote_post( $url, compact( 'body', 'cookies', 'headers', 'timeout', 'sslverify' ) );
     3289
     3290                if ( is_wp_error( $r ) ) {
     3291                        return (object) array(
     3292                                'status'  => 'critical',
     3293                                'message' => sprintf(
     3294                                        '%s<br>%s',
     3295                                        __( 'The loopback request to your site failed, this means features relying on them are not currently working as expected.' ),
     3296                                        sprintf(
     3297                                                /* translators: 1: The WordPress error message. 2: The WordPress error code. */
     3298                                                __( 'Error: %1$s (%2$s)' ),
     3299                                                $r->get_error_message(),
     3300                                                $r->get_error_code()
     3301                                        )
     3302                                ),
     3303                        );
     3304                }
     3305
     3306                if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
     3307                        return (object) array(
     3308                                'status'  => 'recommended',
     3309                                'message' => sprintf(
     3310                                        /* translators: %d: The HTTP response code returned. */
     3311                                        __( '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.' ),
     3312                                        wp_remote_retrieve_response_code( $r )
     3313                                ),
     3314                        );
     3315                }
     3316
     3317                return (object) array(
     3318                        'status'  => 'good',
     3319                        'message' => __( 'The loopback request to your site completed successfully.' ),
     3320                );
     3321        }
     3322
     3323        /**
     3324         * Creates a weekly cron event, if one does not already exist.
     3325         *
     3326         * @since 5.4.0
     3327         */
     3328        public function maybe_create_scheduled_event() {
     3329                if ( ! wp_next_scheduled( 'wp_site_health_scheduled_check' ) && ! wp_installing() ) {
     3330                        wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'wp_site_health_scheduled_check' );
     3331                }
     3332        }
     3333
     3334        /**
     3335         * Runs the scheduled event to check and update the latest site health status for the website.
     3336         *
     3337         * @since 5.4.0
     3338         */
     3339        public function wp_cron_scheduled_check() {
     3340                // Bootstrap wp-admin, as WP_Cron doesn't do this for us.
     3341                require_once trailingslashit( ABSPATH ) . 'wp-admin/includes/admin.php';
     3342
     3343                $tests = WP_Site_Health::get_tests();
     3344
     3345                $results = array();
     3346
     3347                $site_status = array(
     3348                        'good'        => 0,
     3349                        'recommended' => 0,
     3350                        'critical'    => 0,
     3351                );
     3352
     3353                // Don't run https test on development environments.
     3354                if ( $this->is_development_environment() ) {
     3355                        unset( $tests['async']['https_status'] );
     3356                }
     3357
     3358                foreach ( $tests['direct'] as $test ) {
     3359                        if ( ! empty( $test['skip_cron'] ) ) {
     3360                                continue;
     3361                        }
     3362
     3363                        if ( is_string( $test['test'] ) ) {
     3364                                $test_function = sprintf(
     3365                                        'get_test_%s',
     3366                                        $test['test']
     3367                                );
     3368
     3369                                if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
     3370                                        $results[] = $this->perform_test( array( $this, $test_function ) );
     3371                                        continue;
     3372                                }
     3373                        }
     3374
     3375                        if ( is_callable( $test['test'] ) ) {
     3376                                $results[] = $this->perform_test( $test['test'] );
     3377                        }
     3378                }
     3379
     3380                foreach ( $tests['async'] as $test ) {
     3381                        if ( ! empty( $test['skip_cron'] ) ) {
     3382                                continue;
     3383                        }
     3384
     3385                        // Local endpoints may require authentication, so asynchronous tests can pass a direct test runner as well.
     3386                        if ( ! empty( $test['async_direct_test'] ) && is_callable( $test['async_direct_test'] ) ) {
     3387                                // This test is callable, do so and continue to the next asynchronous check.
     3388                                $results[] = $this->perform_test( $test['async_direct_test'] );
     3389                                continue;
     3390                        }
     3391
     3392                        if ( is_string( $test['test'] ) ) {
     3393                                // Check if this test has a REST API endpoint.
     3394                                if ( isset( $test['has_rest'] ) && $test['has_rest'] ) {
     3395                                        $result_fetch = wp_remote_get(
     3396                                                $test['test'],
     3397                                                array(
     3398                                                        'body' => array(
     3399                                                                '_wpnonce' => wp_create_nonce( 'wp_rest' ),
     3400                                                        ),
     3401                                                )
     3402                                        );
     3403                                } else {
     3404                                        $result_fetch = wp_remote_post(
     3405                                                admin_url( 'admin-ajax.php' ),
     3406                                                array(
     3407                                                        'body' => array(
     3408                                                                'action'   => $test['test'],
     3409                                                                '_wpnonce' => wp_create_nonce( 'health-check-site-status' ),
     3410                                                        ),
     3411                                                )
     3412                                        );
     3413                                }
     3414
     3415                                if ( ! is_wp_error( $result_fetch ) && 200 === wp_remote_retrieve_response_code( $result_fetch ) ) {
     3416                                        $result = json_decode( wp_remote_retrieve_body( $result_fetch ), true );
     3417                                } else {
     3418                                        $result = false;
     3419                                }
     3420
     3421                                if ( is_array( $result ) ) {
     3422                                        $results[] = $result;
     3423                                } else {
     3424                                        $results[] = array(
     3425                                                'status' => 'recommended',
     3426                                                'label'  => __( 'A test is unavailable' ),
     3427                                        );
     3428                                }
     3429                        }
     3430                }
     3431
     3432                foreach ( $results as $result ) {
     3433                        if ( 'critical' === $result['status'] ) {
     3434                                ++$site_status['critical'];
     3435                        } elseif ( 'recommended' === $result['status'] ) {
     3436                                ++$site_status['recommended'];
     3437                        } else {
     3438                                ++$site_status['good'];
     3439                        }
     3440                }
     3441
     3442                set_transient( 'health-check-site-status-result', wp_json_encode( $site_status ) );
     3443        }
     3444
     3445        /**
     3446         * Checks if the current environment type is set to 'development' or 'local'.
     3447         *
     3448         * @since 5.6.0
     3449         *
     3450         * @return bool True if it is a development environment, false if not.
     3451         */
     3452        public function is_development_environment() {
     3453                return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
     3454        }
     3455
     3456        /**
     3457         * Returns a list of headers and its verification callback to verify if page cache is enabled or not.
     3458         *
     3459         * Note: key is header name and value could be callable function to verify header value.
     3460         * Empty value mean existence of header detect page cache is enabled.
     3461         *
     3462         * @since 6.1.0
     3463         *
     3464         * @return array List of client caching headers and their (optional) verification callbacks.
     3465         */
     3466        public function get_page_cache_headers() {
     3467
     3468                $cache_hit_callback = static function ( $header_value ) {
     3469                        return str_contains( strtolower( $header_value ), 'hit' );
     3470                };
     3471
     3472                $cache_headers = array(
     3473                        'cache-control'          => static function ( $header_value ) {
     3474                                return (bool) preg_match( '/max-age=[1-9]/', $header_value );
     3475                        },
     3476                        'expires'                => static function ( $header_value ) {
     3477                                return strtotime( $header_value ) > time();
     3478                        },
     3479                        'age'                    => static function ( $header_value ) {
     3480                                return is_numeric( $header_value ) && $header_value > 0;
     3481                        },
     3482                        'last-modified'          => '',
     3483                        'etag'                   => '',
     3484                        'x-cache-enabled'        => static function ( $header_value ) {
     3485                                return 'true' === strtolower( $header_value );
     3486                        },
     3487                        'x-cache-disabled'       => static function ( $header_value ) {
     3488                                return ( 'on' !== strtolower( $header_value ) );
     3489                        },
     3490                        'x-srcache-store-status' => $cache_hit_callback,
     3491                        'x-srcache-fetch-status' => $cache_hit_callback,
     3492                );
     3493
     3494                /**
     3495                 * Filters the list of cache headers supported by core.
     3496                 *
     3497                 * @since 6.1.0
     3498                 *
     3499                 * @param array $cache_headers Array of supported cache headers.
     3500                 */
     3501                return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
     3502        }
     3503
     3504        /**
     3505         * Checks if site has page cache enabled or not.
     3506         *
     3507         * @since 6.1.0
     3508         *
     3509         * @return WP_Error|array {
     3510         *     Page cache detection details or else error information.
     3511         *
     3512         *     @type bool    $advanced_cache_present        Whether a page cache plugin is present.
     3513         *     @type array[] $page_caching_response_headers Sets of client caching headers for the responses.
     3514         *     @type float[] $response_timing               Response timings.
     3515         * }
     3516         */
     3517        private function check_for_page_caching() {
     3518
     3519                /** This filter is documented in wp-includes/class-wp-http-streams.php */
     3520                $sslverify = apply_filters( 'https_local_ssl_verify', false );
     3521
     3522                $headers = array();
     3523
     3524                /*
     3525                 * Include basic auth in loopback requests. Note that this will only pass along basic auth when user is
     3526                 * initiating the test. If a site requires basic auth, the test will fail when it runs in WP Cron as part of
     3527                 * wp_site_health_scheduled_check. This logic is copied from WP_Site_Health::can_perform_loopback().
     3528                 */
     3529                if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     3530                        $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     3531                }
     3532
     3533                $caching_headers               = $this->get_page_cache_headers();
     3534                $page_caching_response_headers = array();
     3535                $response_timing               = array();
     3536
     3537                for ( $i = 1; $i <= 3; $i++ ) {
     3538                        $start_time    = microtime( true );
     3539                        $http_response = wp_remote_get( home_url( '/' ), compact( 'sslverify', 'headers' ) );
     3540                        $end_time      = microtime( true );
     3541
     3542                        if ( is_wp_error( $http_response ) ) {
     3543                                return $http_response;
     3544                        }
     3545                        if ( wp_remote_retrieve_response_code( $http_response ) !== 200 ) {
     3546                                return new WP_Error(
     3547                                        'http_' . wp_remote_retrieve_response_code( $http_response ),
     3548                                        wp_remote_retrieve_response_message( $http_response )
     3549                                );
     3550                        }
     3551
     3552                        $response_headers = array();
     3553
     3554                        foreach ( $caching_headers as $header => $callback ) {
     3555                                $header_values = wp_remote_retrieve_header( $http_response, $header );
     3556                                if ( empty( $header_values ) ) {
     3557                                        continue;
     3558                                }
     3559                                $header_values = (array) $header_values;
     3560                                if ( empty( $callback ) || ( is_callable( $callback ) && count( array_filter( $header_values, $callback ) ) > 0 ) ) {
     3561                                        $response_headers[ $header ] = $header_values;
     3562                                }
     3563                        }
     3564
     3565                        $page_caching_response_headers[] = $response_headers;
     3566                        $response_timing[]               = ( $end_time - $start_time ) * 1000;
     3567                }
     3568
     3569                return array(
     3570                        'advanced_cache_present'        => (
     3571                                file_exists( WP_CONTENT_DIR . '/advanced-cache.php' )
     3572                                &&
     3573                                ( defined( 'WP_CACHE' ) && WP_CACHE )
     3574                                &&
     3575                                /** This filter is documented in wp-settings.php */
     3576                                apply_filters( 'enable_loading_advanced_cache_dropin', true )
     3577                        ),
     3578                        'page_caching_response_headers' => $page_caching_response_headers,
     3579                        'response_timing'               => $response_timing,
     3580                );
     3581        }
     3582
     3583        /**
     3584         * Gets page cache details.
     3585         *
     3586         * @since 6.1.0
     3587         *
     3588         * @return WP_Error|array {
     3589         *     Page cache detail or else a WP_Error if unable to determine.
     3590         *
     3591         *     @type string   $status                 Page cache status. Good, Recommended or Critical.
     3592         *     @type bool     $advanced_cache_present Whether page cache plugin is available or not.
     3593         *     @type string[] $headers                Client caching response headers detected.
     3594         *     @type float    $response_time          Response time of site.
     3595         * }
     3596         */
     3597        private function get_page_cache_detail() {
     3598                $page_cache_detail = $this->check_for_page_caching();
     3599                if ( is_wp_error( $page_cache_detail ) ) {
     3600                        return $page_cache_detail;
     3601                }
     3602
     3603                // Use the median server response time.
     3604                $response_timings = $page_cache_detail['response_timing'];
     3605                rsort( $response_timings );
     3606                $page_speed = $response_timings[ floor( count( $response_timings ) / 2 ) ];
     3607
     3608                // Obtain unique set of all client caching response headers.
     3609                $headers = array();
     3610                foreach ( $page_cache_detail['page_caching_response_headers'] as $page_caching_response_headers ) {
     3611                        $headers = array_merge( $headers, array_keys( $page_caching_response_headers ) );
     3612                }
     3613                $headers = array_unique( $headers );
     3614
     3615                // Page cache is detected if there are response headers or a page cache plugin is present.
     3616                $has_page_caching = ( count( $headers ) > 0 || $page_cache_detail['advanced_cache_present'] );
     3617
     3618                if ( $page_speed && $page_speed < $this->get_good_response_time_threshold() ) {
     3619                        $result = $has_page_caching ? 'good' : 'recommended';
     3620                } else {
     3621                        $result = 'critical';
     3622                }
     3623
     3624                return array(
     3625                        'status'                 => $result,
     3626                        'advanced_cache_present' => $page_cache_detail['advanced_cache_present'],
     3627                        'headers'                => $headers,
     3628                        'response_time'          => $page_speed,
     3629                );
     3630        }
     3631
     3632        /**
     3633         * Gets the threshold below which a response time is considered good.
     3634         *
     3635         * @since 6.1.0
     3636         *
     3637         * @return int Threshold in milliseconds.
     3638         */
     3639        private function get_good_response_time_threshold() {
     3640                /**
     3641                 * Filters the threshold below which a response time is considered good.
     3642                 *
     3643                 * The default is based on https://web.dev/time-to-first-byte/.
     3644                 *
     3645                 * @since 6.1.0
     3646                 *
     3647                 * @param int $threshold Threshold in milliseconds. Default 600.
     3648                 */
     3649                return (int) apply_filters( 'site_status_good_response_time_threshold', 600 );
     3650        }
     3651
     3652        /**
     3653         * Determines whether to suggest using a persistent object cache.
     3654         *
     3655         * @since 6.1.0
     3656         *
     3657         * @global wpdb $wpdb WordPress database abstraction object.
     3658         *
     3659         * @return bool Whether to suggest using a persistent object cache.
     3660         */
     3661        public function should_suggest_persistent_object_cache() {
     3662                global $wpdb;
     3663
     3664                /**
     3665                 * Filters whether to suggest use of a persistent object cache and bypass default threshold checks.
     3666                 *
     3667                 * Using this filter allows to override the default logic, effectively short-circuiting the method.
     3668                 *
     3669                 * @since 6.1.0
     3670                 *
     3671                 * @param bool|null $suggest Boolean to short-circuit, for whether to suggest using a persistent object cache.
     3672                 *                           Default null.
     3673                 */
     3674                $short_circuit = apply_filters( 'site_status_should_suggest_persistent_object_cache', null );
     3675                if ( is_bool( $short_circuit ) ) {
     3676                        return $short_circuit;
     3677                }
     3678
     3679                if ( is_multisite() ) {
     3680                        return true;
     3681                }
     3682
     3683                /**
     3684                 * Filters the thresholds used to determine whether to suggest the use of a persistent object cache.
     3685                 *
     3686                 * @since 6.1.0
     3687                 *
     3688                 * @param int[] $thresholds The list of threshold numbers keyed by threshold name.
     3689                 */
     3690                $thresholds = apply_filters(
     3691                        'site_status_persistent_object_cache_thresholds',
     3692                        array(
     3693                                'alloptions_count' => 500,
     3694                                'alloptions_bytes' => 100000,
     3695                                'comments_count'   => 1000,
     3696                                'options_count'    => 1000,
     3697                                'posts_count'      => 1000,
     3698                                'terms_count'      => 1000,
     3699                                'users_count'      => 1000,
     3700                        )
     3701                );
     3702
     3703                $alloptions = wp_load_alloptions();
     3704
     3705                if ( $thresholds['alloptions_count'] < count( $alloptions ) ) {
     3706                        return true;
     3707                }
     3708
     3709                if ( $thresholds['alloptions_bytes'] < strlen( serialize( $alloptions ) ) ) {
     3710                        return true;
     3711                }
     3712
     3713                $table_names = implode( "','", array( $wpdb->comments, $wpdb->options, $wpdb->posts, $wpdb->terms, $wpdb->users ) );
     3714
     3715                // With InnoDB the `TABLE_ROWS` are estimates, which are accurate enough and faster to retrieve than individual `COUNT()` queries.
     3716                $results = $wpdb->get_results(
     3717                        $wpdb->prepare(
     3718                                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query cannot use interpolation.
     3719                                "SELECT TABLE_NAME AS 'table', TABLE_ROWS AS 'rows', SUM(data_length + index_length) as 'bytes' FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ('$table_names') GROUP BY TABLE_NAME;",
     3720                                DB_NAME
     3721                        ),
     3722                        OBJECT_K
     3723                );
     3724
     3725                $threshold_map = array(
     3726                        'comments_count' => $wpdb->comments,
     3727                        'options_count'  => $wpdb->options,
     3728                        'posts_count'    => $wpdb->posts,
     3729                        'terms_count'    => $wpdb->terms,
     3730                        'users_count'    => $wpdb->users,
     3731                );
     3732
     3733                foreach ( $threshold_map as $threshold => $table ) {
     3734                        if ( $thresholds[ $threshold ] <= $results[ $table ]->rows ) {
     3735                                return true;
     3736                        }
     3737                }
     3738
     3739                return false;
     3740        }
     3741
     3742        /**
     3743         * Returns a list of available persistent object cache services.
     3744         *
     3745         * @since 6.1.0
     3746         *
     3747         * @return string[] The list of available persistent object cache services.
     3748         */
     3749        private function available_object_cache_services() {
     3750                $extensions = array_map(
     3751                        'extension_loaded',
     3752                        array(
     3753                                'APCu'      => 'apcu',
     3754                                'Redis'     => 'redis',
     3755                                'Relay'     => 'relay',
     3756                                'Memcache'  => 'memcache',
     3757                                'Memcached' => 'memcached',
     3758                        )
     3759                );
     3760
     3761                $services = array_keys( array_filter( $extensions ) );
     3762
     3763                /**
     3764                 * Filters the persistent object cache services available to the user.
     3765                 *
     3766                 * This can be useful to hide or add services not included in the defaults.
     3767                 *
     3768                 * @since 6.1.0
     3769                 *
     3770                 * @param string[] $services The list of available persistent object cache services.
     3771                 */
     3772                return apply_filters( 'site_status_available_object_cache_services', $services );
     3773        }
     3774}