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