}> */ private static array $request_log = []; /** @return list */ public static function allowed_paths(): array { return ['/', '/llms.txt', '/agent-map.json', '/robots.txt', '/sitemap.xml', '/wp-sitemap.xml']; } public static function reset_request_log(): void { self::$request_log = []; } /** @return list}> */ public static function request_log(): array { return self::$request_log; } public static function is_allowed_path(string $path): bool { $normalized = '/' . ltrim($path, '/'); return in_array($normalized, self::allowed_paths(), true); } public static function build_url(string $path): string { $normalized = '/' . ltrim($path, '/'); if (!self::is_allowed_path($normalized)) { throw new InvalidArgumentException('Path not allowed for same-origin observation.'); } self::assert_home_url_valid(); return home_url($normalized); } /** * @return array{ok: bool, status: int|null, content_type: string, body_sample: string, error: string, final_url: string, oversized: bool} */ public static function fetch(string $path): array { $url = self::build_url($path); $args = [ 'timeout' => self::TIMEOUT_SECONDS, 'redirection' => 0, 'limit_response_size' => self::MAX_BODY_BYTES + 1, 'sslverify' => true, 'headers' => ['Accept' => '*/*'], ]; self::$request_log[] = ['url' => $url, 'args' => $args]; $response = wp_remote_get($url, $args); if (is_wp_error($response)) { return self::failure($url, null, self::bound_error($response->get_error_message())); } $status = (int) wp_remote_retrieve_response_code($response); if ($status >= 300 && $status < 400) { return self::failure($url, $status, 'redirect rejected'); } $body = (string) wp_remote_retrieve_body($response); $oversized = strlen($body) > self::MAX_BODY_BYTES; if ($oversized) { return [ 'ok' => false, 'status' => $status, 'content_type' => strtolower((string) wp_remote_retrieve_header($response, 'content-type')), 'body_sample' => '', 'error' => 'response too large', 'final_url' => $url, 'oversized' => true, ]; } return [ 'ok' => true, 'status' => $status, 'content_type' => strtolower((string) wp_remote_retrieve_header($response, 'content-type')), 'body_sample' => $body, 'error' => '', 'final_url' => $url, 'oversized' => false, ]; } public static function assert_home_url_valid(): void { $parts = wp_parse_url(home_url('/')); if (!is_array($parts) || empty($parts['host'])) { throw new InvalidArgumentException('Invalid home URL for same-origin observation.'); } $scheme = strtolower((string) ($parts['scheme'] ?? '')); if (!in_array($scheme, ['http', 'https'], true)) { throw new InvalidArgumentException('Invalid home URL scheme for same-origin observation.'); } } public static function origin_from_home_url(): string { $parts = wp_parse_url(home_url('/')); if (!is_array($parts) || empty($parts['host'])) { return ''; } $scheme = strtolower((string) ($parts['scheme'] ?? 'https')); $host = strtolower((string) $parts['host']); $port = isset($parts['port']) ? ':' . (int) $parts['port'] : ''; return $scheme . '://' . $host . $port; } private static function failure(string $url, ?int $status, string $error): array { return [ 'ok' => false, 'status' => $status, 'content_type' => '', 'body_sample' => '', 'error' => self::bound_error($error), 'final_url' => $url, 'oversized' => false, ]; } private static function bound_error(string $error): string { return substr($error, 0, 120); } }