From a00c62ad71265960bdb26084e8d51a2a6d704c60 Mon Sep 17 00:00:00 2001 From: KarunyaChavan Date: Thu, 14 May 2026 20:40:50 +0530 Subject: [PATCH 1/2] feat(abilities): extend core/get-environment-info with cached Site Health summary. - Abilities: Added optional `fields` input parameter to `core/get-environment-info` to allow for response filtering. - Abilities: Introduced `site_health` output field providing status, issue counts, and a bounded list of actionable issues. - Site Health: Modified `WP_Site_Health::wp_cron_scheduled_check` to store critical/recommended issue details in the `health-check-site-status-result` transient. - Site Health: Updated `site-health.js` to ensure the issues list is transmitted during manual AJAX updates for cache parity. --- src/js/_enqueues/admin/site-health.js | 15 +- .../includes/class-wp-site-health.php | 3 + src/wp-includes/abilities.php | 208 +++++++++++++++--- .../abilities-api/wpRegisterCoreAbilities.php | 47 +++- 4 files changed, 239 insertions(+), 34 deletions(-) diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index 57d5c9cbcf289..0012f35b1af54 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -154,6 +154,13 @@ jQuery( function( $ ) { SiteHealth.site_status.issues[ issue.status ]++; + if ( 'critical' === issue.status || 'recommended' === issue.status ) { + if ( 'undefined' === typeof SiteHealth.site_status.issue_list ) { + SiteHealth.site_status.issue_list = []; + } + SiteHealth.site_status.issue_list.push( issue ); + } + count = SiteHealth.site_status.issues[ issue.status ]; // If no test name is supplied, append a placeholder for markup references. @@ -255,7 +262,12 @@ jQuery( function( $ ) { { 'action': 'health-check-site-status-result', '_wpnonce': SiteHealth.nonce.site_status_result, - 'counts': SiteHealth.site_status.issues + 'counts': { + good: SiteHealth.site_status.issues.good, + recommended: SiteHealth.site_status.issues.recommended, + critical: SiteHealth.site_status.issues.critical, + issues: SiteHealth.site_status.issue_list + } } ); @@ -375,6 +387,7 @@ jQuery( function( $ ) { 'recommended': 0, 'critical': 0 }; + SiteHealth.site_status.issue_list = []; } if ( 0 < SiteHealth.site_status.direct.length ) { diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 75e046ef8ffa7..ce315c1c41dbd 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3372,6 +3372,7 @@ public function wp_cron_scheduled_check() { 'good' => 0, 'recommended' => 0, 'critical' => 0, + 'issues' => array(), ); // Don't run https test on development environments. @@ -3456,8 +3457,10 @@ public function wp_cron_scheduled_check() { foreach ( $results as $result ) { if ( 'critical' === $result['status'] ) { ++$site_status['critical']; + $site_status['issues'][] = $result; } elseif ( 'recommended' === $result['status'] ) { ++$site_status['recommended']; + $site_status['issues'][] = $result; } else { ++$site_status['good']; } diff --git a/src/wp-includes/abilities.php b/src/wp-includes/abilities.php index 4c6db1ed830e0..42b36012cc34e 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -196,53 +196,199 @@ function wp_register_core_abilities(): void { ) ); + $env_info_properties = array( + 'environment' => array( + 'type' => 'string', + 'description' => __( 'The site\'s runtime environment classification (can be one of these: production, staging, development, local).' ), + 'enum' => array( 'production', 'staging', 'development', 'local' ), + ), + 'php_version' => array( + 'type' => 'string', + 'description' => __( 'The PHP runtime version executing WordPress.' ), + ), + 'db_server_info' => array( + 'type' => 'string', + 'description' => __( 'The database server vendor and version string reported by the driver.' ), + ), + 'wp_version' => array( + 'type' => 'string', + 'description' => __( 'The WordPress core version running on this site.' ), + ), + 'site_health' => array( + 'type' => 'object', + 'description' => __( 'A high-level overview of the site\'s health, populated from cached data. Can vary across calls as the cache refreshes.' ), + 'properties' => array( + 'status' => array( + 'type' => 'string', + 'title' => __( 'Status' ), + 'description' => __( 'The overall health status of the site (e.g., good, recommended, critical, unknown).' ), + ), + 'counts' => array( + 'type' => 'object', + 'title' => __( 'Counts' ), + 'description' => __( 'The count of issues by severity.' ), + 'properties' => array( + 'good' => array( + 'type' => 'integer', + 'title' => __( 'Good' ), + 'description' => __( 'Number of passing tests.' ), + ), + 'recommended' => array( + 'type' => 'integer', + 'title' => __( 'Recommended' ), + 'description' => __( 'Number of recommended improvements.' ), + ), + 'critical' => array( + 'type' => 'integer', + 'title' => __( 'Critical' ), + 'description' => __( 'Number of critical issues.' ), + ), + ), + ), + 'issues' => array( + 'type' => 'array', + 'title' => __( 'Issues' ), + 'description' => __( 'Actionable issues, capped at 10 items.' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'label' => array( + 'type' => 'string', + 'title' => __( 'Label' ), + 'description' => __( 'Short description of the issue.' ), + ), + 'severity' => array( + 'type' => 'string', + 'title' => __( 'Severity' ), + 'description' => __( 'Severity level (recommended, critical).' ), + ), + 'recommendation' => array( + 'type' => 'string', + 'title' => __( 'Recommendation' ), + 'description' => __( 'Guidance or description for resolving the issue.' ), + ), + ), + ), + ), + 'truncated' => array( + 'type' => 'boolean', + 'title' => __( 'Truncated' ), + 'description' => __( 'Whether the list of issues has been truncated due to size limits.' ), + ), + ), + ), + ); + $env_info_fields = array_keys( $env_info_properties ); + wp_register_ability( 'core/get-environment-info', array( 'label' => __( 'Get Environment Info' ), 'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version).' ), 'category' => $category_site, - 'output_schema' => array( + 'input_schema' => array( 'type' => 'object', - 'required' => array( 'environment', 'php_version', 'db_server_info', 'wp_version' ), 'properties' => array( - 'environment' => array( - 'type' => 'string', - 'description' => __( 'The site\'s runtime environment classification (can be one of these: production, staging, development, local).' ), - 'enum' => array( 'production', 'staging', 'development', 'local' ), - ), - 'php_version' => array( - 'type' => 'string', - 'description' => __( 'The PHP runtime version executing WordPress.' ), - ), - 'db_server_info' => array( - 'type' => 'string', - 'description' => __( 'The database server vendor and version string reported by the driver.' ), - ), - 'wp_version' => array( - 'type' => 'string', - 'description' => __( 'The WordPress core version running on this site.' ), + 'fields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $env_info_fields, + ), + 'description' => __( 'Optional: Limit response to specific fields. If omitted, all fields are returned.' ), ), ), 'additionalProperties' => false, + 'default' => array(), ), - 'execute_callback' => static function (): array { + 'output_schema' => array( + 'type' => 'object', + 'properties' => $env_info_properties, + 'additionalProperties' => false, + ), + 'execute_callback' => static function ( $input = array() ) use ( $env_info_fields ): array { global $wpdb; - $env = wp_get_environment_type(); - $php_version = phpversion(); - $db_server_info = ''; - if ( method_exists( $wpdb, 'db_server_info' ) ) { - $db_server_info = $wpdb->db_server_info() ?? ''; + $input = is_array( $input ) ? $input : array(); + $requested_fields = ! empty( $input['fields'] ) ? $input['fields'] : $env_info_fields; + + $result = array(); + + if ( in_array( 'environment', $requested_fields, true ) ) { + $result['environment'] = wp_get_environment_type(); } - $wp_version = get_bloginfo( 'version' ); - return array( - 'environment' => $env, - 'php_version' => $php_version, - 'db_server_info' => $db_server_info, - 'wp_version' => $wp_version, - ); + if ( in_array( 'php_version', $requested_fields, true ) ) { + $result['php_version'] = phpversion(); + } + + if ( in_array( 'db_server_info', $requested_fields, true ) ) { + $db_server_info = ''; + if ( method_exists( $wpdb, 'db_server_info' ) ) { + $db_server_info = $wpdb->db_server_info() ?? ''; + } + $result['db_server_info'] = $db_server_info; + } + + if ( in_array( 'wp_version', $requested_fields, true ) ) { + $result['wp_version'] = get_bloginfo( 'version' ); + } + + if ( in_array( 'site_health', $requested_fields, true ) ) { + $cached_health = get_transient( 'health-check-site-status-result' ); + + $site_health = array( + 'status' => 'unknown', + 'counts' => array( + 'good' => 0, + 'recommended' => 0, + 'critical' => 0, + ), + 'issues' => array(), + 'truncated' => false, + ); + + if ( false !== $cached_health ) { + $health_data = json_decode( $cached_health, true ); + + if ( is_array( $health_data ) ) { + if ( isset( $health_data['good'], $health_data['recommended'], $health_data['critical'] ) ) { + $site_health['counts']['good'] = (int) $health_data['good']; + $site_health['counts']['recommended'] = (int) $health_data['recommended']; + $site_health['counts']['critical'] = (int) $health_data['critical']; + + if ( $site_health['counts']['critical'] > 0 ) { + $site_health['status'] = 'critical'; + } elseif ( $site_health['counts']['recommended'] > 0 ) { + $site_health['status'] = 'recommended'; + } else { + $site_health['status'] = 'good'; + } + } + + if ( isset( $health_data['issues'] ) && is_array( $health_data['issues'] ) ) { + $issues_count = 0; + foreach ( $health_data['issues'] as $issue ) { + if ( $issues_count >= 10 ) { + $site_health['truncated'] = true; + break; + } + + $site_health['issues'][] = array( + 'label' => isset( $issue['label'] ) ? wp_strip_all_tags( $issue['label'] ) : '', + 'severity' => isset( $issue['status'] ) ? $issue['status'] : 'recommended', + 'recommendation' => isset( $issue['description'] ) ? wp_strip_all_tags( $issue['description'] ) : '', + ); + $issues_count++; + } + } + } + } + + $result['site_health'] = $site_health; + } + + return $result; }, 'permission_callback' => static function (): bool { return current_user_can( 'manage_options' ); diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php index 48cae6efd1dee..fb0d10c9c435e 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php @@ -163,8 +163,11 @@ public function test_core_get_environment_info_executes(): void { $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $admin_id ); - $ability = wp_get_ability( 'core/get-environment-info' ); - $environment = wp_get_environment_type(); + $ability = wp_get_ability( 'core/get-environment-info' ); + $environment = wp_get_environment_type(); + + // Uncached site_health test. + delete_transient( 'health-check-site-status-result' ); $ability_data = $ability->execute(); $this->assertIsArray( $ability_data ); @@ -172,7 +175,47 @@ public function test_core_get_environment_info_executes(): void { $this->assertArrayHasKey( 'php_version', $ability_data ); $this->assertArrayHasKey( 'db_server_info', $ability_data ); $this->assertArrayHasKey( 'wp_version', $ability_data ); + $this->assertArrayHasKey( 'site_health', $ability_data ); $this->assertSame( $environment, $ability_data['environment'] ); + + $this->assertSame( 'unknown', $ability_data['site_health']['status'] ); + $this->assertSame( 0, $ability_data['site_health']['counts']['good'] ); + $this->assertEmpty( $ability_data['site_health']['issues'] ); + + // Test fields filtering. + $filtered_data = $ability->execute( array( 'fields' => array( 'php_version', 'site_health' ) ) ); + + $this->assertCount( 2, $filtered_data ); + $this->assertArrayHasKey( 'php_version', $filtered_data ); + $this->assertArrayHasKey( 'site_health', $filtered_data ); + + // Test with cached site_health results and truncation. + $issues = array(); + for ( $i = 0; $i < 15; $i++ ) { + $issues[] = array( + 'label' => "Issue {$i}", + 'status' => 'recommended', + 'description' => "Description {$i}", + ); + } + + $cached_data = array( + 'good' => 5, + 'recommended' => 15, + 'critical' => 0, + 'issues' => $issues, + ); + set_transient( 'health-check-site-status-result', wp_json_encode( $cached_data ) ); + + $cached_ability_data = $ability->execute( array( 'fields' => array( 'site_health' ) ) ); + $site_health = $cached_ability_data['site_health']; + + $this->assertSame( 'recommended', $site_health['status'] ); + $this->assertSame( 5, $site_health['counts']['good'] ); + $this->assertSame( 15, $site_health['counts']['recommended'] ); + $this->assertCount( 10, $site_health['issues'] ); // Should be truncated to 10 + $this->assertTrue( $site_health['truncated'] ); + $this->assertSame( 'Issue 0', $site_health['issues'][0]['label'] ); } /** From aa85ece52cff3a4b61bf8a743f32dba4ec5c75d7 Mon Sep 17 00:00:00 2001 From: KarunyaChavan Date: Thu, 14 May 2026 23:55:07 +0530 Subject: [PATCH 2/2] fix(abilities): harden cached Site Health summary for core/get-environment-info. --- src/js/_enqueues/admin/site-health.js | 41 ++-- src/wp-admin/includes/ajax-actions.php | 50 +++- .../includes/class-wp-site-health.php | 19 +- src/wp-includes/abilities.php | 170 +++++++++----- .../abilities-api/wpRegisterCoreAbilities.php | 222 ++++++++++++++++-- 5 files changed, 398 insertions(+), 104 deletions(-) diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index 0012f35b1af54..7d175d9443aae 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -154,13 +154,6 @@ jQuery( function( $ ) { SiteHealth.site_status.issues[ issue.status ]++; - if ( 'critical' === issue.status || 'recommended' === issue.status ) { - if ( 'undefined' === typeof SiteHealth.site_status.issue_list ) { - SiteHealth.site_status.issue_list = []; - } - SiteHealth.site_status.issue_list.push( issue ); - } - count = SiteHealth.site_status.issues[ issue.status ]; // If no test name is supplied, append a placeholder for markup references. @@ -168,6 +161,19 @@ jQuery( function( $ ) { issue.test = issue.status + count; } + if ( 'critical' === issue.status || 'recommended' === issue.status ) { + if ( 'undefined' === typeof SiteHealth.site_status.issue_list ) { + SiteHealth.site_status.issue_list = []; + } + + SiteHealth.site_status.issue_list.push( { + test: issue.test, + label: issue.label, + status: issue.status, + description: issue.description + } ); + } + if ( 'critical' === issue.status ) { heading = sprintf( _n( '%s critical issue', '%s critical issues', count ), @@ -257,18 +263,19 @@ jQuery( function( $ ) { } if ( isStatusTab ) { + var postData = { + 'action': 'health-check-site-status-result', + '_wpnonce': SiteHealth.nonce.site_status_result, + 'counts': SiteHealth.site_status.issues + }; + + if ( 'undefined' !== typeof SiteHealth.site_status.issue_list ) { + postData.issues = JSON.stringify( SiteHealth.site_status.issue_list ); + } + $.post( ajaxurl, - { - 'action': 'health-check-site-status-result', - '_wpnonce': SiteHealth.nonce.site_status_result, - 'counts': { - good: SiteHealth.site_status.issues.good, - recommended: SiteHealth.site_status.issues.recommended, - critical: SiteHealth.site_status.issues.critical, - issues: SiteHealth.site_status.issue_list - } - } + postData ); if ( 100 === val ) { diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..12d09079ac5db 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -5465,8 +5465,56 @@ function wp_ajax_health_check_site_status_result() { if ( ! current_user_can( 'view_site_health_checks' ) ) { wp_send_json_error(); } + $counts = isset( $_POST['counts'] ) ? wp_unslash( $_POST['counts'] ) : null; + if ( ! is_array( $counts ) ) { + wp_send_json_error(); + } + + $payload = array( + 'good' => isset( $counts['good'] ) ? (int) $counts['good'] : 0, + 'recommended' => isset( $counts['recommended'] ) ? (int) $counts['recommended'] : 0, + 'critical' => isset( $counts['critical'] ) ? (int) $counts['critical'] : 0, + ); + + $has_actionable_issues = $payload['recommended'] > 0 || $payload['critical'] > 0; + $previous = get_transient( 'health-check-site-status-result' ); + $previous = is_string( $previous ) ? json_decode( $previous, true ) : array(); + + if ( isset( $_POST['issues'] ) ) { + $issues_raw = wp_unslash( $_POST['issues'] ); + $decoded = is_string( $issues_raw ) ? json_decode( $issues_raw, true ) : null; + + if ( is_array( $decoded ) ) { + $sanitized_issues = array(); + foreach ( $decoded as $issue ) { + if ( ! is_array( $issue ) ) { + continue; + } + + $status = isset( $issue['status'] ) ? sanitize_key( $issue['status'] ) : ''; + if ( ! in_array( $status, array( 'recommended', 'critical' ), true ) ) { + continue; + } + + $sanitized_issues[] = array( + 'test' => isset( $issue['test'] ) ? sanitize_text_field( $issue['test'] ) : '', + 'label' => isset( $issue['label'] ) ? sanitize_text_field( $issue['label'] ) : '', + 'status' => $status, + 'description' => isset( $issue['description'] ) ? wp_strip_all_tags( $issue['description'] ) : '', + ); + } + + $payload['issues'] = $sanitized_issues; + } elseif ( $has_actionable_issues && is_array( $previous ) && isset( $previous['issues'] ) && is_array( $previous['issues'] ) ) { + $payload['issues'] = $previous['issues']; + } + } elseif ( $has_actionable_issues && is_array( $previous ) && isset( $previous['issues'] ) && is_array( $previous['issues'] ) ) { + $payload['issues'] = $previous['issues']; + } + + $payload['timestamp'] = time(); - set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) ); + set_transient( 'health-check-site-status-result', wp_json_encode( $payload ) ); wp_send_json_success(); } diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index ce315c1c41dbd..fe3e6b5052a15 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3455,17 +3455,32 @@ public function wp_cron_scheduled_check() { } foreach ( $results as $result ) { + if ( ! is_array( $result ) || ! isset( $result['status'] ) ) { + continue; + } + if ( 'critical' === $result['status'] ) { ++$site_status['critical']; - $site_status['issues'][] = $result; } elseif ( 'recommended' === $result['status'] ) { ++$site_status['recommended']; - $site_status['issues'][] = $result; } else { ++$site_status['good']; } + + if ( ! in_array( $result['status'], array( 'recommended', 'critical' ), true ) ) { + continue; + } + + $site_status['issues'][] = array( + 'test' => isset( $result['test'] ) ? (string) $result['test'] : '', + 'label' => isset( $result['label'] ) ? wp_strip_all_tags( (string) $result['label'] ) : '', + 'status' => $result['status'], + 'description' => isset( $result['description'] ) ? wp_strip_all_tags( (string) $result['description'] ) : '', + ); } + $site_status['timestamp'] = time(); + set_transient( 'health-check-site-status-result', wp_json_encode( $site_status ) ); } diff --git a/src/wp-includes/abilities.php b/src/wp-includes/abilities.php index 42b36012cc34e..144200235418f 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -215,19 +215,20 @@ function wp_register_core_abilities(): void { 'description' => __( 'The WordPress core version running on this site.' ), ), 'site_health' => array( - 'type' => 'object', - 'description' => __( 'A high-level overview of the site\'s health, populated from cached data. Can vary across calls as the cache refreshes.' ), - 'properties' => array( + 'type' => 'object', + 'description' => __( 'A high-level overview of the site\'s health, populated from cached data. Can vary across calls as the cache refreshes.' ), + 'properties' => array( 'status' => array( 'type' => 'string', 'title' => __( 'Status' ), 'description' => __( 'The overall health status of the site (e.g., good, recommended, critical, unknown).' ), + 'enum' => array( 'unknown', 'good', 'recommended', 'critical' ), ), 'counts' => array( - 'type' => 'object', - 'title' => __( 'Counts' ), - 'description' => __( 'The count of issues by severity.' ), - 'properties' => array( + 'type' => 'object', + 'title' => __( 'Counts' ), + 'description' => __( 'The count of Site Health test results by severity.' ), + 'properties' => array( 'good' => array( 'type' => 'integer', 'title' => __( 'Good' ), @@ -244,14 +245,20 @@ function wp_register_core_abilities(): void { 'description' => __( 'Number of critical issues.' ), ), ), + 'additionalProperties' => false, ), 'issues' => array( 'type' => 'array', 'title' => __( 'Issues' ), 'description' => __( 'Actionable issues, capped at 10 items.' ), 'items' => array( - 'type' => 'object', - 'properties' => array( + 'type' => 'object', + 'properties' => array( + 'test' => array( + 'type' => 'string', + 'title' => __( 'Test Identifier' ), + 'description' => __( 'The machine-readable name of the Site Health test.' ), + ), 'label' => array( 'type' => 'string', 'title' => __( 'Label' ), @@ -261,6 +268,7 @@ function wp_register_core_abilities(): void { 'type' => 'string', 'title' => __( 'Severity' ), 'description' => __( 'Severity level (recommended, critical).' ), + 'enum' => array( 'recommended', 'critical' ), ), 'recommendation' => array( 'type' => 'string', @@ -268,6 +276,7 @@ function wp_register_core_abilities(): void { 'description' => __( 'Guidance or description for resolving the issue.' ), ), ), + 'additionalProperties' => false, ), ), 'truncated' => array( @@ -275,7 +284,13 @@ function wp_register_core_abilities(): void { 'title' => __( 'Truncated' ), 'description' => __( 'Whether the list of issues has been truncated due to size limits.' ), ), + 'timestamp' => array( + 'type' => 'integer', + 'title' => __( 'Timestamp' ), + 'description' => __( 'The Unix timestamp of when the Site Health data was last collected, or 0 when no cached data exists.' ), + ), ), + 'additionalProperties' => false, ), ); $env_info_fields = array_keys( $env_info_properties ); @@ -335,57 +350,7 @@ function wp_register_core_abilities(): void { } if ( in_array( 'site_health', $requested_fields, true ) ) { - $cached_health = get_transient( 'health-check-site-status-result' ); - - $site_health = array( - 'status' => 'unknown', - 'counts' => array( - 'good' => 0, - 'recommended' => 0, - 'critical' => 0, - ), - 'issues' => array(), - 'truncated' => false, - ); - - if ( false !== $cached_health ) { - $health_data = json_decode( $cached_health, true ); - - if ( is_array( $health_data ) ) { - if ( isset( $health_data['good'], $health_data['recommended'], $health_data['critical'] ) ) { - $site_health['counts']['good'] = (int) $health_data['good']; - $site_health['counts']['recommended'] = (int) $health_data['recommended']; - $site_health['counts']['critical'] = (int) $health_data['critical']; - - if ( $site_health['counts']['critical'] > 0 ) { - $site_health['status'] = 'critical'; - } elseif ( $site_health['counts']['recommended'] > 0 ) { - $site_health['status'] = 'recommended'; - } else { - $site_health['status'] = 'good'; - } - } - - if ( isset( $health_data['issues'] ) && is_array( $health_data['issues'] ) ) { - $issues_count = 0; - foreach ( $health_data['issues'] as $issue ) { - if ( $issues_count >= 10 ) { - $site_health['truncated'] = true; - break; - } - - $site_health['issues'][] = array( - 'label' => isset( $issue['label'] ) ? wp_strip_all_tags( $issue['label'] ) : '', - 'severity' => isset( $issue['status'] ) ? $issue['status'] : 'recommended', - 'recommendation' => isset( $issue['description'] ) ? wp_strip_all_tags( $issue['description'] ) : '', - ); - $issues_count++; - } - } - } - } - - $result['site_health'] = $site_health; + $result['site_health'] = wp_get_abilities_api_site_health_summary_from_cache(); } return $result; @@ -404,3 +369,88 @@ function wp_register_core_abilities(): void { ) ); } + +/** + * Builds the Site Health portion of `core/get-environment-info` from cached results only. + * + * @since 6.9.1 + * + * @return array{ + * status: 'unknown'|'good'|'recommended'|'critical', + * counts: array{good: int, recommended: int, critical: int}, + * issues: array, + * truncated: bool, + * timestamp: int + * } + */ +function wp_get_abilities_api_site_health_summary_from_cache(): array { + $site_health = array( + 'status' => 'unknown', + 'counts' => array( + 'good' => 0, + 'recommended' => 0, + 'critical' => 0, + ), + 'issues' => array(), + 'truncated' => false, + 'timestamp' => 0, + ); + + $cached_health = get_transient( 'health-check-site-status-result' ); + + if ( false === $cached_health ) { + return $site_health; + } + + $health_data = json_decode( $cached_health, true ); + if ( ! is_array( $health_data ) ) { + return $site_health; + } + + if ( isset( $health_data['good'], $health_data['recommended'], $health_data['critical'] ) ) { + $site_health['counts']['good'] = (int) $health_data['good']; + $site_health['counts']['recommended'] = (int) $health_data['recommended']; + $site_health['counts']['critical'] = (int) $health_data['critical']; + + if ( $site_health['counts']['critical'] > 0 ) { + $site_health['status'] = 'critical'; + } elseif ( $site_health['counts']['recommended'] > 0 ) { + $site_health['status'] = 'recommended'; + } else { + $site_health['status'] = 'good'; + } + } + + if ( isset( $health_data['timestamp'] ) ) { + $site_health['timestamp'] = (int) $health_data['timestamp']; + } + + if ( isset( $health_data['issues'] ) && is_array( $health_data['issues'] ) ) { + $issues_count = 0; + foreach ( $health_data['issues'] as $issue ) { + if ( ! is_array( $issue ) ) { + continue; + } + + $status = isset( $issue['status'] ) ? (string) $issue['status'] : ''; + if ( ! in_array( $status, array( 'recommended', 'critical' ), true ) ) { + continue; + } + + if ( $issues_count >= 10 ) { + $site_health['truncated'] = true; + break; + } + + $site_health['issues'][] = array( + 'test' => isset( $issue['test'] ) ? (string) $issue['test'] : '', + 'label' => isset( $issue['label'] ) ? wp_strip_all_tags( (string) $issue['label'] ) : '', + 'severity' => $status, + 'recommendation' => isset( $issue['description'] ) ? wp_strip_all_tags( (string) $issue['description'] ) : '', + ); + ++$issues_count; + } + } + + return $site_health; +} diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php index fb0d10c9c435e..c10271181edaa 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php @@ -156,7 +156,9 @@ public function test_core_get_current_user_info_returns_user_data(): void { /** * Tests executing the environment info ability. + * * @ticket 64146 + * @ticket 65232 */ public function test_core_get_environment_info_executes(): void { // Requires manage_options. @@ -166,8 +168,6 @@ public function test_core_get_environment_info_executes(): void { $ability = wp_get_ability( 'core/get-environment-info' ); $environment = wp_get_environment_type(); - // Uncached site_health test. - delete_transient( 'health-check-site-status-result' ); $ability_data = $ability->execute(); $this->assertIsArray( $ability_data ); @@ -177,45 +177,219 @@ public function test_core_get_environment_info_executes(): void { $this->assertArrayHasKey( 'wp_version', $ability_data ); $this->assertArrayHasKey( 'site_health', $ability_data ); $this->assertSame( $environment, $ability_data['environment'] ); + } - $this->assertSame( 'unknown', $ability_data['site_health']['status'] ); - $this->assertSame( 0, $ability_data['site_health']['counts']['good'] ); - $this->assertEmpty( $ability_data['site_health']['issues'] ); + /** + * Tests that the `fields` input limits `core/get-environment-info` output. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_fields_filtering(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); - // Test fields filtering. + $ability = wp_get_ability( 'core/get-environment-info' ); $filtered_data = $ability->execute( array( 'fields' => array( 'php_version', 'site_health' ) ) ); $this->assertCount( 2, $filtered_data ); $this->assertArrayHasKey( 'php_version', $filtered_data ); $this->assertArrayHasKey( 'site_health', $filtered_data ); + $this->assertArrayNotHasKey( 'environment', $filtered_data ); + $this->assertArrayNotHasKey( 'db_server_info', $filtered_data ); + $this->assertArrayNotHasKey( 'wp_version', $filtered_data ); + } - // Test with cached site_health results and truncation. - $issues = array(); - for ( $i = 0; $i < 15; $i++ ) { + /** + * Tests `site_health` in `core/get-environment-info` when no Site Health cache exists. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_without_cache(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + delete_transient( 'health-check-site-status-result' ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $site_health = $ability->execute( array( 'fields' => array( 'site_health' ) ) )['site_health']; + + $this->assertSame( 'unknown', $site_health['status'] ); + $this->assertSameSetsWithIndex( + array( + 'good' => 0, + 'recommended' => 0, + 'critical' => 0, + ), + $site_health['counts'] + ); + $this->assertSame( array(), $site_health['issues'] ); + $this->assertFalse( $site_health['truncated'] ); + $this->assertSame( 0, $site_health['timestamp'] ); + } + + /** + * Tests `site_health` in `core/get-environment-info` when cached Site Health results exist. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_from_cache(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $cached_data = array( + 'good' => 8, + 'recommended' => 1, + 'critical' => 1, + 'issues' => array( + array( + 'test' => 'wordpress_version', + 'label' => 'WordPress update available', + 'status' => 'recommended', + 'description' => '

A new version of WordPress is available.

', + ), + array( + 'test' => 'authorization_header', + 'label' => 'Authorization header missing', + 'status' => 'critical', + 'description' => '

The authorization header is missing.

', + ), + ), + 'timestamp' => 1715714399, + ); + set_transient( 'health-check-site-status-result', wp_json_encode( $cached_data ) ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $site_health = $ability->execute( array( 'fields' => array( 'site_health' ) ) )['site_health']; + + $this->assertSame( 'critical', $site_health['status'] ); + $this->assertSame( 8, $site_health['counts']['good'] ); + $this->assertSame( 1, $site_health['counts']['recommended'] ); + $this->assertSame( 1, $site_health['counts']['critical'] ); + $this->assertSame( 1715714399, $site_health['timestamp'] ); + $this->assertFalse( $site_health['truncated'] ); + $this->assertSame( + array( + 'test' => 'wordpress_version', + 'label' => 'WordPress update available', + 'severity' => 'recommended', + 'recommendation' => 'A new version of WordPress is available.', + ), + $site_health['issues'][0] + ); + } + + /** + * Tests that only actionable Site Health issues are exposed and capped. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_issues_are_actionable_and_truncated(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $issues = array( + array( + 'test' => 'passing_test', + 'label' => 'Passing test', + 'status' => 'good', + 'description' => 'This should not be exposed.', + ), + array( + 'test' => 'unknown_status', + 'label' => 'Unknown status', + 'status' => 'invalid', + 'description' => 'This should not be exposed.', + ), + ); + + for ( $i = 0; $i < 11; $i++ ) { $issues[] = array( - 'label' => "Issue {$i}", + 'test' => 'test_' . $i, + 'label' => 'Issue ' . $i, 'status' => 'recommended', - 'description' => "Description {$i}", + 'description' => 'Description ' . $i, ); } - $cached_data = array( - 'good' => 5, - 'recommended' => 15, - 'critical' => 0, - 'issues' => $issues, + set_transient( + 'health-check-site-status-result', + wp_json_encode( + array( + 'good' => 1, + 'recommended' => 11, + 'critical' => 0, + 'issues' => $issues, + 'timestamp' => 1715714399, + ) + ) ); - set_transient( 'health-check-site-status-result', wp_json_encode( $cached_data ) ); - $cached_ability_data = $ability->execute( array( 'fields' => array( 'site_health' ) ) ); - $site_health = $cached_ability_data['site_health']; + $ability = wp_get_ability( 'core/get-environment-info' ); + $site_health = $ability->execute( array( 'fields' => array( 'site_health' ) ) )['site_health']; - $this->assertSame( 'recommended', $site_health['status'] ); - $this->assertSame( 5, $site_health['counts']['good'] ); - $this->assertSame( 15, $site_health['counts']['recommended'] ); - $this->assertCount( 10, $site_health['issues'] ); // Should be truncated to 10 $this->assertTrue( $site_health['truncated'] ); - $this->assertSame( 'Issue 0', $site_health['issues'][0]['label'] ); + $this->assertCount( 10, $site_health['issues'] ); + $this->assertSame( 'test_0', $site_health['issues'][0]['test'] ); + $this->assertSame( 'test_9', $site_health['issues'][9]['test'] ); + } + + /** + * Tests that malformed cached Site Health data is treated as unknown. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_with_malformed_cache(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + set_transient( 'health-check-site-status-result', '{not-json' ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $site_health = $ability->execute( array( 'fields' => array( 'site_health' ) ) )['site_health']; + + $this->assertSame( 'unknown', $site_health['status'] ); + $this->assertSame( array(), $site_health['issues'] ); + } + + /** + * Tests that the environment info ability reads cached Site Health data only. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_does_not_run_site_health_tests(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $fail_on_test_discovery = function ( $tests ) { + $this->fail( 'core/get-environment-info must not run or discover Site Health tests.' ); + return $tests; + }; + + add_filter( 'site_status_tests', $fail_on_test_discovery ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $site_health = $ability->execute( array( 'fields' => array( 'site_health' ) ) )['site_health']; + + remove_filter( 'site_status_tests', $fail_on_test_discovery ); + + $this->assertIsArray( $site_health ); + } + + /** + * Tests that the environment info ability documents the Site Health schema precisely. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_schema(): void { + $ability = wp_get_ability( 'core/get-environment-info' ); + $site_health = $ability->get_output_schema()['properties']['site_health']; + $issue_schema = $site_health['properties']['issues']['items']; + + $this->assertSame( array( 'unknown', 'good', 'recommended', 'critical' ), $site_health['properties']['status']['enum'] ); + $this->assertFalse( $site_health['additionalProperties'] ); + $this->assertFalse( $site_health['properties']['counts']['additionalProperties'] ); + $this->assertSame( array( 'recommended', 'critical' ), $issue_schema['properties']['severity']['enum'] ); + $this->assertFalse( $issue_schema['additionalProperties'] ); } /**