From 857c63e5fda17a7c50d84f328f62d0bae7a4bc39 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Thu, 14 May 2026 17:52:33 +0530 Subject: [PATCH 1/2] Abilities API: extend core/get-environment-info with fields and site_health. Add optional fields input to mirror core/get-site-info. Expose site_health from the health-check-site-status-result transient only (no synchronous tests), including counts, actionable issues (recommended/critical), and truncation when more than ten issues are cached. Persist issue summaries in the Site Health transient from the weekly cron and from the Site Health screen AJAX handler, with merge behavior when the client omits the issues payload. See #65232. Co-authored-by: Cursor --- src/js/_enqueues/admin/site-health.js | 30 ++- src/wp-admin/includes/ajax-actions.php | 52 +++- .../includes/class-wp-site-health.php | 22 ++ src/wp-includes/abilities.php | 253 +++++++++++++++--- .../abilities-api/wpRegisterCoreAbilities.php | 109 ++++++++ 5 files changed, 428 insertions(+), 38 deletions(-) diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index 57d5c9cbcf289..54c86193c83a6 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -161,6 +161,19 @@ jQuery( function( $ ) { issue.test = issue.status + count; } + if ( 'critical' === issue.status || 'recommended' === issue.status ) { + if ( 'undefined' === typeof SiteHealth.site_status.actionable_issues ) { + SiteHealth.site_status.actionable_issues = []; + } + + SiteHealth.site_status.actionable_issues.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 ), @@ -250,13 +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.actionable_issues ) { + postData.issues = JSON.stringify( SiteHealth.site_status.actionable_issues ); + } + $.post( ajaxurl, - { - 'action': 'health-check-site-status-result', - '_wpnonce': SiteHealth.nonce.site_status_result, - 'counts': SiteHealth.site_status.issues - } + postData ); if ( 100 === val ) { @@ -375,6 +394,7 @@ jQuery( function( $ ) { 'recommended': 0, 'critical': 0 }; + SiteHealth.site_status.actionable_issues = []; } if ( 0 < SiteHealth.site_status.direct.length ) { diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..36a17236d0040 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -5466,7 +5466,57 @@ function wp_ajax_health_check_site_status_result() { wp_send_json_error(); } - set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) ); + $counts = isset( $_POST['counts'] ) ? wp_unslash( $_POST['counts'] ) : null; + if ( ! is_array( $counts ) ) { + wp_send_json_error(); + } + + $good = isset( $counts['good'] ) ? (int) $counts['good'] : 0; + $recommended = isset( $counts['recommended'] ) ? (int) $counts['recommended'] : 0; + $critical = isset( $counts['critical'] ) ? (int) $counts['critical'] : 0; + + $payload = array( + 'good' => $good, + 'recommended' => $recommended, + 'critical' => $critical, + ); + + $previous = get_transient( 'health-check-site-status-result' ); + $previous = is_string( $previous ) ? json_decode( $previous, true ) : array(); + + if ( array_key_exists( 'issues', $_POST ) ) { + $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 ( is_array( $previous ) && isset( $previous['issues'] ) && is_array( $previous['issues'] ) ) { + $payload['issues'] = $previous['issues']; + } + } elseif ( is_array( $previous ) && isset( $previous['issues'] ) && is_array( $previous['issues'] ) ) { + $payload['issues'] = $previous['issues']; + } + + 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 75e046ef8ffa7..9414aed574373 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3454,6 +3454,10 @@ 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']; } elseif ( 'recommended' === $result['status'] ) { @@ -3463,6 +3467,24 @@ public function wp_cron_scheduled_check() { } } + $site_status['issues'] = array(); + foreach ( $results as $result ) { + if ( ! is_array( $result ) || ! isset( $result['status'] ) ) { + continue; + } + + 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'] ) : '', + ); + } + 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 4c6db1ed830e0..6b3f7f4818a8e 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -196,53 +196,161 @@ function wp_register_core_abilities(): void { ) ); + $site_health_issue_properties = array( + 'test' => array( + 'type' => 'string', + 'title' => __( 'Site Health test identifier' ), + 'description' => __( 'The machine identifier for the Site Health test that produced this entry.' ), + ), + 'label' => array( + 'type' => 'string', + 'title' => __( 'Site Health issue label' ), + 'description' => __( 'A short title describing the Site Health issue.' ), + ), + 'status' => array( + 'type' => 'string', + 'title' => __( 'Site Health issue severity' ), + 'description' => __( 'Whether this entry is a recommended improvement or a critical issue.' ), + 'enum' => array( 'recommended', 'critical' ), + ), + 'description' => array( + 'type' => 'string', + 'title' => __( 'Site Health issue description' ), + 'description' => __( 'Plain text details for the Site Health issue, sourced from cached results.' ), + ), + ); + + $environment_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 cached-only Site Health summary for agents: overall status, counts, and actionable issues.' ), + 'properties' => array( + 'status' => array( + 'type' => 'string', + 'title' => __( 'Site Health overall status' ), + 'description' => __( 'unknown: no cached Site Health results yet. good: no recommended or critical findings. recommended or critical: matching severity is present in cached counts.' ), + 'enum' => array( 'unknown', 'good', 'recommended', 'critical' ), + ), + 'counts' => array( + 'type' => 'object', + 'title' => __( 'Site Health result counts' ), + 'description' => __( 'How many Site Health tests reported each status in the cached run.' ), + 'properties' => array( + 'good' => array( + 'type' => 'integer', + 'title' => __( 'Good results' ), + 'description' => __( 'Number of tests reporting a good status.' ), + ), + 'recommended' => array( + 'type' => 'integer', + 'title' => __( 'Recommended improvements' ), + 'description' => __( 'Number of tests recommending an improvement.' ), + ), + 'critical' => array( + 'type' => 'integer', + 'title' => __( 'Critical issues' ), + 'description' => __( 'Number of tests reporting a critical issue.' ), + ), + ), + 'additionalProperties' => false, + ), + 'issues' => array( + 'type' => 'array', + 'title' => __( 'Actionable Site Health issues' ), + 'description' => __( 'Up to ten recommended or critical issues from the cached Site Health run.' ), + 'items' => array( + 'type' => 'object', + 'properties' => $site_health_issue_properties, + 'additionalProperties' => false, + ), + ), + 'truncated' => array( + 'type' => 'boolean', + 'title' => __( 'Issues list truncated' ), + 'description' => __( 'True when more than ten actionable issues exist in the cache.' ), + ), + ), + 'additionalProperties' => false, + ), + ); + $environment_info_fields = array_keys( $environment_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).' ), + 'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version), plus an optional cached Site Health summary.' ), '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' => $environment_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' => $environment_info_properties, + 'additionalProperties' => false, + ), + 'execute_callback' => static function ( $input = array() ) use ( $environment_info_fields ): array { + $input = is_array( $input ) ? $input : array(); + $requested = ! empty( $input['fields'] ) ? $input['fields'] : $environment_info_fields; + 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() ?? ''; + $result = array(); + + if ( in_array( 'environment', $requested, 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, true ) ) { + $result['php_version'] = phpversion(); + } + + if ( in_array( 'db_server_info', $requested, 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, true ) ) { + $result['wp_version'] = get_bloginfo( 'version' ); + } + + if ( in_array( 'site_health', $requested, true ) ) { + $result['site_health'] = wp_get_abilities_api_site_health_summary_from_cache(); + } + + return $result; }, 'permission_callback' => static function (): bool { return current_user_can( 'manage_options' ); @@ -258,3 +366,84 @@ 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 + * } + */ +function wp_get_abilities_api_site_health_summary_from_cache(): array { + $unknown = array( + 'status' => 'unknown', + 'counts' => array( + 'good' => 0, + 'recommended' => 0, + 'critical' => 0, + ), + 'issues' => array(), + 'truncated' => false, + ); + + $cached = get_transient( 'health-check-site-status-result' ); + + if ( false === $cached ) { + return $unknown; + } + + $data = json_decode( $cached, true ); + if ( ! is_array( $data ) ) { + return $unknown; + } + + $counts = array( + 'good' => isset( $data['good'] ) ? (int) $data['good'] : 0, + 'recommended' => isset( $data['recommended'] ) ? (int) $data['recommended'] : 0, + 'critical' => isset( $data['critical'] ) ? (int) $data['critical'] : 0, + ); + + $stored = array(); + if ( isset( $data['issues'] ) && is_array( $data['issues'] ) ) { + foreach ( $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; + } + + $stored[] = array( + 'test' => isset( $issue['test'] ) ? (string) $issue['test'] : '', + 'label' => isset( $issue['label'] ) ? (string) $issue['label'] : '', + 'status' => $status, + 'description' => isset( $issue['description'] ) ? (string) $issue['description'] : '', + ); + } + } + + $total_stored = count( $stored ); + $truncated = $total_stored > 10; + $issues = array_slice( $stored, 0, 10 ); + + $status = 'good'; + if ( $counts['critical'] > 0 ) { + $status = 'critical'; + } elseif ( $counts['recommended'] > 0 ) { + $status = 'recommended'; + } + + return array( + 'status' => $status, + 'counts' => $counts, + 'issues' => $issues, + 'truncated' => $truncated, + ); +} diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php index 48cae6efd1dee..708b675f3cd3d 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. @@ -172,7 +174,114 @@ 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'] ); + } + + /** + * 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 ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $data = $ability->execute( + array( + 'fields' => array( 'php_version' ), + ) + ); + + $this->assertCount( 1, $data ); + $this->assertArrayHasKey( 'php_version', $data ); + $this->assertArrayNotHasKey( 'environment', $data ); + } + + /** + * Tests `site_health` in `core/get-environment-info` when the Site Health transient is populated. + * + * @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 ); + + set_transient( + 'health-check-site-status-result', + wp_json_encode( + array( + 'good' => 8, + 'recommended' => 1, + 'critical' => 0, + 'issues' => array( + array( + 'test' => 'wordpress_version', + 'label' => 'WordPress update available', + 'status' => 'recommended', + 'description' => 'A new version of WordPress is available.', + ), + ), + ) + ) + ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $data = $ability->execute( + array( + 'fields' => array( 'site_health' ), + ) + ); + + $this->assertArrayHasKey( 'site_health', $data ); + $health = $data['site_health']; + $this->assertSame( 'recommended', $health['status'] ); + $this->assertSame( 8, $health['counts']['good'] ); + $this->assertSame( 1, $health['counts']['recommended'] ); + $this->assertSame( 0, $health['counts']['critical'] ); + $this->assertCount( 1, $health['issues'] ); + $this->assertSame( 'wordpress_version', $health['issues'][0]['test'] ); + $this->assertFalse( $health['truncated'] ); + } + + /** + * Tests that actionable Site Health issues are capped with `truncated` when the cache holds more than ten. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_issues_truncated(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $issues = array(); + for ( $i = 0; $i < 15; $i++ ) { + $issues[] = array( + 'test' => 'test_' . $i, + 'label' => 'Issue ' . $i, + 'status' => 'recommended', + 'description' => 'Description ' . $i, + ); + } + + set_transient( + 'health-check-site-status-result', + wp_json_encode( + array( + 'good' => 0, + 'recommended' => 15, + 'critical' => 0, + 'issues' => $issues, + ) + ) + ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $data = $ability->execute( array( 'fields' => array( 'site_health' ) ) ); + + $this->assertTrue( $data['site_health']['truncated'] ); + $this->assertCount( 10, $data['site_health']['issues'] ); } /** From 6db13d822defd934a5783e31103af9373d968a12 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Thu, 14 May 2026 17:52:33 +0530 Subject: [PATCH 2/2] Abilities API: extend core/get-environment-info with fields and site_health. Add optional fields input to mirror core/get-site-info. Expose site_health from the health-check-site-status-result transient only (no synchronous tests), including counts, actionable issues (recommended/critical), and truncation when more than ten issues are cached. Persist issue summaries in the Site Health transient from the weekly cron and from the Site Health screen AJAX handler, with merge behavior when the client omits the issues payload. See #65232. --- src/js/_enqueues/admin/site-health.js | 30 ++- src/wp-admin/includes/ajax-actions.php | 54 +++- .../includes/class-wp-site-health.php | 14 + src/wp-includes/abilities.php | 253 +++++++++++++++--- .../abilities-api/wpRegisterCoreAbilities.php | 109 ++++++++ 5 files changed, 422 insertions(+), 38 deletions(-) diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index 57d5c9cbcf289..54c86193c83a6 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -161,6 +161,19 @@ jQuery( function( $ ) { issue.test = issue.status + count; } + if ( 'critical' === issue.status || 'recommended' === issue.status ) { + if ( 'undefined' === typeof SiteHealth.site_status.actionable_issues ) { + SiteHealth.site_status.actionable_issues = []; + } + + SiteHealth.site_status.actionable_issues.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 ), @@ -250,13 +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.actionable_issues ) { + postData.issues = JSON.stringify( SiteHealth.site_status.actionable_issues ); + } + $.post( ajaxurl, - { - 'action': 'health-check-site-status-result', - '_wpnonce': SiteHealth.nonce.site_status_result, - 'counts': SiteHealth.site_status.issues - } + postData ); if ( 100 === val ) { @@ -375,6 +394,7 @@ jQuery( function( $ ) { 'recommended': 0, 'critical': 0 }; + SiteHealth.site_status.actionable_issues = []; } if ( 0 < SiteHealth.site_status.direct.length ) { diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..582e75e4783f3 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -5466,7 +5466,59 @@ function wp_ajax_health_check_site_status_result() { wp_send_json_error(); } - set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) ); + $counts = isset( $_POST['counts'] ) ? wp_unslash( $_POST['counts'] ) : null; + if ( ! is_array( $counts ) ) { + wp_send_json_error(); + } + + $good = isset( $counts['good'] ) ? (int) $counts['good'] : 0; + $recommended = isset( $counts['recommended'] ) ? (int) $counts['recommended'] : 0; + $critical = isset( $counts['critical'] ) ? (int) $counts['critical'] : 0; + + $payload = array( + 'good' => $good, + 'recommended' => $recommended, + 'critical' => $critical, + ); + + $previous_raw = get_transient( 'health-check-site-status-result' ); + $previous = is_string( $previous_raw ) ? json_decode( $previous_raw, true ) : array(); + $previous = is_array( $previous ) ? $previous : array(); + $previous_issues = ( isset( $previous['issues'] ) && is_array( $previous['issues'] ) ) ? $previous['issues'] : array(); + + if ( array_key_exists( 'issues', $_POST ) ) { + $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 ( $previous_issues ) { + $payload['issues'] = $previous_issues; + } + } elseif ( $previous_issues ) { + $payload['issues'] = $previous_issues; + } + + 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 75e046ef8ffa7..ce023d5bc8567 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -3453,7 +3453,12 @@ public function wp_cron_scheduled_check() { } } + $site_status['issues'] = array(); foreach ( $results as $result ) { + if ( ! is_array( $result ) || ! isset( $result['status'] ) ) { + continue; + } + if ( 'critical' === $result['status'] ) { ++$site_status['critical']; } elseif ( 'recommended' === $result['status'] ) { @@ -3461,6 +3466,15 @@ public function wp_cron_scheduled_check() { } else { ++$site_status['good']; } + + if ( in_array( $result['status'], array( 'recommended', 'critical' ), true ) ) { + $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'] ) : '', + ); + } } 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 4c6db1ed830e0..00b62855ae2ba 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -196,53 +196,161 @@ function wp_register_core_abilities(): void { ) ); + $site_health_issue_properties = array( + 'test' => array( + 'type' => 'string', + 'title' => __( 'Site Health test identifier' ), + 'description' => __( 'The machine identifier for the Site Health test that produced this entry.' ), + ), + 'label' => array( + 'type' => 'string', + 'title' => __( 'Site Health issue label' ), + 'description' => __( 'A short title describing the Site Health issue.' ), + ), + 'status' => array( + 'type' => 'string', + 'title' => __( 'Site Health issue severity' ), + 'description' => __( 'Whether this entry is a recommended improvement or a critical issue.' ), + 'enum' => array( 'recommended', 'critical' ), + ), + 'description' => array( + 'type' => 'string', + 'title' => __( 'Site Health issue description' ), + 'description' => __( 'Plain text details for the Site Health issue, sourced from cached results.' ), + ), + ); + + $environment_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 cached-only Site Health summary for agents: overall status, counts, and actionable issues.' ), + 'properties' => array( + 'status' => array( + 'type' => 'string', + 'title' => __( 'Site Health overall status' ), + 'description' => __( 'unknown: no cached Site Health results yet. good: no recommended or critical findings. recommended or critical: matching severity is present in cached counts.' ), + 'enum' => array( 'unknown', 'good', 'recommended', 'critical' ), + ), + 'counts' => array( + 'type' => 'object', + 'title' => __( 'Site Health result counts' ), + 'description' => __( 'How many Site Health tests reported each status in the cached run.' ), + 'properties' => array( + 'good' => array( + 'type' => 'integer', + 'title' => __( 'Good results' ), + 'description' => __( 'Number of tests reporting a good status.' ), + ), + 'recommended' => array( + 'type' => 'integer', + 'title' => __( 'Recommended improvements' ), + 'description' => __( 'Number of tests recommending an improvement.' ), + ), + 'critical' => array( + 'type' => 'integer', + 'title' => __( 'Critical issues' ), + 'description' => __( 'Number of tests reporting a critical issue.' ), + ), + ), + 'additionalProperties' => false, + ), + 'issues' => array( + 'type' => 'array', + 'title' => __( 'Actionable Site Health issues' ), + 'description' => __( 'Up to ten recommended or critical issues from the cached Site Health run.' ), + 'items' => array( + 'type' => 'object', + 'properties' => $site_health_issue_properties, + 'additionalProperties' => false, + ), + ), + 'truncated' => array( + 'type' => 'boolean', + 'title' => __( 'Issues list truncated' ), + 'description' => __( 'True when more than ten actionable issues exist in the cache.' ), + ), + ), + 'additionalProperties' => false, + ), + ); + $environment_info_fields = array_keys( $environment_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).' ), + 'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version), plus an optional cached Site Health summary.' ), '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' => $environment_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' => $environment_info_properties, + 'additionalProperties' => false, + ), + 'execute_callback' => static function ( $input = array() ) use ( $environment_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 = ! empty( $input['fields'] ) ? $input['fields'] : $environment_info_fields; + + $result = array(); + + if ( in_array( 'environment', $requested, 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, true ) ) { + $result['php_version'] = phpversion(); + } + + if ( in_array( 'db_server_info', $requested, 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, true ) ) { + $result['wp_version'] = get_bloginfo( 'version' ); + } + + if ( in_array( 'site_health', $requested, true ) ) { + $result['site_health'] = wp_get_abilities_api_site_health_summary_from_cache(); + } + + return $result; }, 'permission_callback' => static function (): bool { return current_user_can( 'manage_options' ); @@ -258,3 +366,84 @@ function wp_register_core_abilities(): void { ) ); } + +/** + * Builds the Site Health portion of `core/get-environment-info` from cached results only. + * + * @since tbd + * + * @return array{ + * status: 'unknown'|'good'|'recommended'|'critical', + * counts: array{good: int, recommended: int, critical: int}, + * issues: array, + * truncated: bool + * } + */ +function wp_get_abilities_api_site_health_summary_from_cache(): array { + $unknown = array( + 'status' => 'unknown', + 'counts' => array( + 'good' => 0, + 'recommended' => 0, + 'critical' => 0, + ), + 'issues' => array(), + 'truncated' => false, + ); + + $cached = get_transient( 'health-check-site-status-result' ); + + if ( false === $cached ) { + return $unknown; + } + + $data = json_decode( $cached, true ); + if ( ! is_array( $data ) ) { + return $unknown; + } + + $counts = array( + 'good' => isset( $data['good'] ) ? (int) $data['good'] : 0, + 'recommended' => isset( $data['recommended'] ) ? (int) $data['recommended'] : 0, + 'critical' => isset( $data['critical'] ) ? (int) $data['critical'] : 0, + ); + + $stored = array(); + if ( isset( $data['issues'] ) && is_array( $data['issues'] ) ) { + foreach ( $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; + } + + $stored[] = array( + 'test' => isset( $issue['test'] ) ? (string) $issue['test'] : '', + 'label' => isset( $issue['label'] ) ? (string) $issue['label'] : '', + 'status' => $status, + 'description' => isset( $issue['description'] ) ? (string) $issue['description'] : '', + ); + } + } + + $total_stored = count( $stored ); + $truncated = $total_stored > 10; + $issues = array_slice( $stored, 0, 10 ); + + $status = 'good'; + if ( $counts['critical'] > 0 ) { + $status = 'critical'; + } elseif ( $counts['recommended'] > 0 ) { + $status = 'recommended'; + } + + return array( + 'status' => $status, + 'counts' => $counts, + 'issues' => $issues, + 'truncated' => $truncated, + ); +} diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php index 48cae6efd1dee..708b675f3cd3d 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. @@ -172,7 +174,114 @@ 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'] ); + } + + /** + * 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 ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $data = $ability->execute( + array( + 'fields' => array( 'php_version' ), + ) + ); + + $this->assertCount( 1, $data ); + $this->assertArrayHasKey( 'php_version', $data ); + $this->assertArrayNotHasKey( 'environment', $data ); + } + + /** + * Tests `site_health` in `core/get-environment-info` when the Site Health transient is populated. + * + * @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 ); + + set_transient( + 'health-check-site-status-result', + wp_json_encode( + array( + 'good' => 8, + 'recommended' => 1, + 'critical' => 0, + 'issues' => array( + array( + 'test' => 'wordpress_version', + 'label' => 'WordPress update available', + 'status' => 'recommended', + 'description' => 'A new version of WordPress is available.', + ), + ), + ) + ) + ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $data = $ability->execute( + array( + 'fields' => array( 'site_health' ), + ) + ); + + $this->assertArrayHasKey( 'site_health', $data ); + $health = $data['site_health']; + $this->assertSame( 'recommended', $health['status'] ); + $this->assertSame( 8, $health['counts']['good'] ); + $this->assertSame( 1, $health['counts']['recommended'] ); + $this->assertSame( 0, $health['counts']['critical'] ); + $this->assertCount( 1, $health['issues'] ); + $this->assertSame( 'wordpress_version', $health['issues'][0]['test'] ); + $this->assertFalse( $health['truncated'] ); + } + + /** + * Tests that actionable Site Health issues are capped with `truncated` when the cache holds more than ten. + * + * @ticket 65232 + */ + public function test_core_get_environment_info_site_health_issues_truncated(): void { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $issues = array(); + for ( $i = 0; $i < 15; $i++ ) { + $issues[] = array( + 'test' => 'test_' . $i, + 'label' => 'Issue ' . $i, + 'status' => 'recommended', + 'description' => 'Description ' . $i, + ); + } + + set_transient( + 'health-check-site-status-result', + wp_json_encode( + array( + 'good' => 0, + 'recommended' => 15, + 'critical' => 0, + 'issues' => $issues, + ) + ) + ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $data = $ability->execute( array( 'fields' => array( 'site_health' ) ) ); + + $this->assertTrue( $data['site_health']['truncated'] ); + $this->assertCount( 10, $data['site_health']['issues'] ); } /**