'core_update', 'previous_version' => is_string($results['version'] ?? null) ? $results['version'] : '', 'detected_at' => time(), ], false); } delete_option(ABC_INTEGRITY_MINI_MANIFEST_OPTION); delete_option(ABC_INTEGRITY_MINI_RESULTS_OPTION); } } function abc_integrity_mini_get_manifest(): ?array { $manifest = get_option(ABC_INTEGRITY_MINI_MANIFEST_OPTION); return is_array($manifest) ? $manifest : null; } function abc_integrity_mini_get_scan_results(): ?array { $results = get_option(ABC_INTEGRITY_MINI_RESULTS_OPTION); return is_array($results) ? $results : null; } function abc_integrity_mini_get_rescan_context(?array $results, string $currentVersion): ?array { $notice = get_option(ABC_INTEGRITY_MINI_RESCAN_OPTION); if (is_array($notice) && ($notice['reason'] ?? '') === 'core_update') { return [ 'previous_version' => is_string($notice['previous_version'] ?? null) ? $notice['previous_version'] : '', 'current_version' => $currentVersion, ]; } $resultVersion = is_string($results['version'] ?? null) ? $results['version'] : ''; if ($resultVersion !== '' && $resultVersion !== $currentVersion) { return [ 'previous_version' => $resultVersion, 'current_version' => $currentVersion, ]; } return null; } function abc_integrity_mini_fetch_manifest(string $version, string $locale): array { $url = add_query_arg( ['version' => $version, 'locale' => $locale], 'https://api.wordpress.org/core/checksums/1.0/' ); $response = wp_safe_remote_get($url, [ 'timeout' => 30, 'user-agent' => 'ABC Integrity/' . ABC_INTEGRITY_MINI_VERSION, ]); if (is_wp_error($response)) { return [ 'success' => false, 'message' => 'Could not reach the WordPress.org checksum service. It may be temporarily unavailable; please try again later.', ]; } $code = (int) wp_remote_retrieve_response_code($response); if ($code < 200 || $code >= 300) { if ($code === 429 || $code >= 500) { return [ 'success' => false, 'message' => "The WordPress.org checksum service is temporarily unavailable (HTTP {$code}). Please try again later.", ]; } return [ 'success' => false, 'message' => "WordPress.org could not provide the checksum manifest (HTTP {$code}). Please try again later.", ]; } $body = wp_remote_retrieve_body($response); $decoded = $body !== '' ? json_decode($body, true) : null; if (!is_array($decoded) || !array_key_exists('checksums', $decoded)) { return [ 'success' => false, 'message' => 'WordPress.org returned an unexpected checksum response. Please try again later.', ]; } if ($decoded['checksums'] === false || $decoded['checksums'] === []) { return [ 'success' => false, 'message' => "WordPress.org does not have a checksum manifest for WordPress {$version} ({$locale}). It may not be available yet for this version and locale.", ]; } if (!is_array($decoded['checksums'])) { return [ 'success' => false, 'message' => 'WordPress.org returned an unexpected checksum response. Please try again later.', ]; } $checksums = []; foreach ($decoded['checksums'] as $path => $hash) { if (!is_string($path) || !is_string($hash)) { continue; } $path = ltrim(wp_normalize_path($path), '/'); $hash = strtolower(trim($hash)); if ($path === '' || !preg_match('/^[a-f0-9]{32}$/', $hash)) { continue; } $checksums[$path] = $hash; } if (empty($checksums)) { return ['success' => false, 'message' => 'The checksum manifest contained no usable entries.']; } ksort($checksums); $manifest = [ 'version' => $version, 'locale' => $locale, 'fetched_at' => time(), 'checksums' => $checksums, ]; update_option(ABC_INTEGRITY_MINI_MANIFEST_OPTION, $manifest, false); return ['success' => true, 'manifest' => $manifest]; } function abc_integrity_mini_is_core_path(string $path): bool { return !str_starts_with($path, 'wp-content/plugins/') && !str_starts_with($path, 'wp-content/themes/'); } function abc_integrity_mini_resolve_full_path(string $relativePath): ?string { $relativePath = ltrim(wp_normalize_path($relativePath), '/'); if ($relativePath === '' || str_contains($relativePath, '../') || str_starts_with($relativePath, '..')) { return null; } if (str_starts_with($relativePath, 'wp-content/')) { $contentDir = defined('WP_CONTENT_DIR') && (string) WP_CONTENT_DIR !== '' ? wp_normalize_path((string) WP_CONTENT_DIR) : ''; if ($contentDir === '') { return null; } return rtrim($contentDir, '/') . '/' . substr($relativePath, strlen('wp-content/')); } $abspath = wp_normalize_path((string) ABSPATH); return rtrim($abspath, '/') . '/' . $relativePath; } function abc_integrity_mini_sanitize_file_read_error(?string $message, string $fullPath): string { if (!is_string($message) || trim($message) === '') { return 'Unable to read file.'; } $safeMessage = str_replace( [$fullPath, wp_normalize_path($fullPath)], '[path]', trim($message) ); $safeMessage = preg_replace('/^md5_file\(\[path\]\):\s*/', '', $safeMessage); $safeMessage = trim((string) $safeMessage); if ($safeMessage === '' || str_contains($safeMessage, '[path]')) { return 'Unable to read file.'; } if (preg_match('#(?:[A-Za-z]:[\\\\/]|/[^\\s:]+/)#', $safeMessage)) { return 'Unable to read file.'; } return $safeMessage; } function abc_integrity_mini_hash_file(string $fullPath): array { $message = null; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler set_error_handler(static function (int $severity, string $msg) use (&$message): bool { unset($severity); $message = $msg; return true; }); try { $hash = md5_file($fullPath); } finally { restore_error_handler(); } if ($hash === false) { return ['success' => false, 'message' => abc_integrity_mini_sanitize_file_read_error($message, $fullPath)]; } return ['success' => true, 'hash' => strtolower($hash)]; } function abc_integrity_mini_run_scan(array $manifest): array { $allChecksums = $manifest['checksums'] ?? []; $coreChecksums = array_filter( $allChecksums, static fn($hash, $path) => is_string($path) && is_string($hash) && abc_integrity_mini_is_core_path($path), ARRAY_FILTER_USE_BOTH ); if (empty($coreChecksums)) { return ['success' => false, 'message' => 'The manifest contained no core file entries to verify.']; } $start = microtime(true); $verified = 0; $missing = []; $modified = []; $unreadable = []; foreach ($coreChecksums as $path => $expectedHash) { $fullPath = abc_integrity_mini_resolve_full_path($path); if ($fullPath === null) { $unreadable[] = ['path' => $path, 'reason' => 'Path could not be resolved.']; continue; } if (!file_exists($fullPath)) { $missing[] = ['path' => $path]; continue; } if (!is_file($fullPath)) { $unreadable[] = ['path' => $path, 'reason' => 'Not a regular file.']; continue; } $result = abc_integrity_mini_hash_file($fullPath); if (!$result['success']) { $unreadable[] = ['path' => $path, 'reason' => $result['message'] ?? 'Read error.']; continue; } if ($result['hash'] !== strtolower($expectedHash)) { $modified[] = ['path' => $path]; continue; } $verified++; } $results = [ 'version' => $manifest['version'], 'locale' => $manifest['locale'], 'manifest_fetched_at' => (int) ($manifest['fetched_at'] ?? 0), 'scanned_at' => time(), 'duration' => round(microtime(true) - $start, 2), 'has_findings' => !empty($missing) || !empty($modified) || !empty($unreadable), 'stats' => [ 'verified' => $verified, 'ignored' => count($allChecksums) - count($coreChecksums), 'missing' => count($missing), 'modified' => count($modified), 'unreadable' => count($unreadable), ], 'findings' => [ 'missing' => $missing, 'modified' => $modified, 'unreadable' => $unreadable, ], ]; update_option(ABC_INTEGRITY_MINI_RESULTS_OPTION, $results, false); delete_option(ABC_INTEGRITY_MINI_RESCAN_OPTION); return ['success' => true] + $results; } function abc_integrity_mini_ajax_scan(): void { check_ajax_referer('abc_integrity_mini_scan', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Unauthorized.'], 403); return; } $version = (string) get_bloginfo('version'); $locale = get_locale() ?: 'en_US'; $manifest = abc_integrity_mini_get_manifest(); $manifestStale = $manifest === null || ($manifest['version'] ?? '') !== $version || ($manifest['locale'] ?? '') !== $locale; if ($manifestStale) { $fetch = abc_integrity_mini_fetch_manifest($version, $locale); if (!$fetch['success']) { wp_send_json_error(['message' => $fetch['message']]); return; } $manifest = $fetch['manifest']; // Invalidate any results from the old manifest. delete_option(ABC_INTEGRITY_MINI_RESULTS_OPTION); } $results = abc_integrity_mini_run_scan($manifest); if (!$results['success']) { wp_send_json_error(['message' => $results['message']]); return; } wp_send_json_success($results); }