*/ public static function abilities(): array { return [ new Ability( 'sarcio/site-health', __('Look at the site’s health', 'sarcio'), Ability::READ, 'WordPress’s Site Health: what passed, what is recommended and what is critical, each with its plain description, plus what is waiting to be updated.', ['properties' => [], 'required' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('manage_options') || current_user_can('view_site_health_checks'), static fn (array $args): array => self::health(), ), new Ability( 'sarcio/site-check', __('Check the site for common problems', 'sarcio'), Ability::READ, 'Images without alt text, links on the site’s own pages that answer with an error, and published pages without a search description (where Yoast SEO or Rank Math is installed). Each with the ids needed to fix it with the other abilities.', ['properties' => ['links' => ['type' => 'boolean']], 'required' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('edit_posts'), static fn (array $args): array => self::check(!isset($args['links']) || (bool) $args['links']), ), ]; } /** @return array{counts:array{critical:int,good:int,recommended:int},issues:array} */ public static function summary(): array { $counts = ['critical' => 0, 'good' => 0, 'recommended' => 0]; $issues = []; if (!class_exists('\WP_Site_Health')) { if (!defined('ABSPATH') || !is_file(ABSPATH . 'wp-admin/includes/class-wp-site-health.php')) { return ['counts' => $counts, 'issues' => []]; } require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php'; } $health = \WP_Site_Health::get_instance(); $tests = \WP_Site_Health::get_tests(); foreach ((array) ($tests['direct'] ?? []) as $name => $test) { if (in_array((string) $name, self::SKIP_TESTS, true)) { continue; } $callable = is_array($test) && is_callable($test['test'] ?? null) ? $test['test'] : (is_array($test) && is_string($test['test'] ?? null) && method_exists($health, 'get_test_' . $test['test']) ? [$health, 'get_test_' . $test['test']] : null); if ($callable === null) { continue; } try { $result = call_user_func($callable); } catch (\Throwable) { continue; } if (!is_array($result)) { continue; } $status = (string) ($result['status'] ?? 'good'); $counts[$status] = ($counts[$status] ?? 0) + 1; if ($status !== 'good') { $issues[] = ['label' => (string) ($result['label'] ?? $name), 'status' => $status, 'text' => mb_substr(trim(wp_strip_all_tags((string) ($result['description'] ?? ''))), 0, 300)]; } } return ['counts' => ['critical' => (int) $counts['critical'], 'good' => (int) $counts['good'], 'recommended' => (int) $counts['recommended']], 'issues' => $issues]; } private static function health(): array { $summary = self::summary(); $updates = current_user_can('update_plugins') ? count(UpdateAbilities::pendingPlugins()) : 0; return $summary + ['pluginUpdates' => $updates, 'wordpress' => function_exists('get_bloginfo') ? (string) get_bloginfo('version') : '']; } private static function check(bool $links): array { return [ 'brokenLinks' => $links ? self::brokenLinks() : [], 'imagesWithoutAlt' => self::imagesWithoutAlt(), 'pagesWithoutDescription' => self::pagesWithoutDescription(), ]; } /** @return array */ public static function imagesWithoutAlt(): array { $images = get_posts(['numberposts' => 200, 'post_mime_type' => 'image', 'post_status' => 'inherit', 'post_type' => 'attachment']); $out = []; foreach (is_array($images) ? $images : [] as $image) { if (trim((string) get_post_meta((int) $image->ID, '_wp_attachment_image_alt', true)) !== '') { continue; } $out[] = ['id' => (int) $image->ID, 'title' => (string) $image->post_title, 'url' => function_exists('wp_get_attachment_url') ? (string) wp_get_attachment_url((int) $image->ID) : '']; if (count($out) >= 100) { break; } } return $out; } /** @return array */ private static function pagesWithoutDescription(): array { $key = SeoAbilities::descriptionKey(); if ($key === null) { return []; } $out = []; foreach ((array) get_posts(['numberposts' => 200, 'post_status' => 'publish', 'post_type' => ['page', 'post']]) as $post) { if (trim((string) get_post_meta((int) $post->ID, $key, true)) === '') { $out[] = ['id' => (int) $post->ID, 'title' => (string) $post->post_title]; } } return $out; } /** @return array */ private static function brokenLinks(): array { $home = (string) wp_parse_url(home_url('/'), PHP_URL_HOST); $seen = []; $out = []; $checked = 0; foreach ((array) get_posts(['numberposts' => 50, 'post_status' => 'publish', 'post_type' => ['page', 'post']]) as $post) { if (!preg_match_all('~href=["\']([^"\'#]+)~i', (string) $post->post_content, $matches)) { continue; } foreach ($matches[1] as $href) { $url = str_starts_with($href, '/') ? home_url($href) : $href; $host = (string) wp_parse_url($url, PHP_URL_HOST); if ($host !== $home || isset($seen[$url]) || !preg_match('~^https?://~i', $url)) { continue; } $seen[$url] = true; if ($checked >= self::LINK_CHECKS) { break 2; } $checked++; $response = wp_remote_head($url, ['redirection' => 3, 'timeout' => 5]); $status = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); if ($status === 0 || $status >= 400) { $out[] = ['from' => (int) $post->ID, 'fromTitle' => (string) $post->post_title, 'status' => $status, 'url' => $url]; } } } return $out; } }