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