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