$hrefs * @param callable(string): array{exists: bool, public: bool}|null $local_resolver * @return array */ public static function assess_links(array $hrefs, string $site_url, ?callable $local_resolver = null): array { $site_origin = self::origin_for_url($site_url); $items = []; foreach (array_slice($hrefs, 0, self::MAX_LINKS) as $href) { $items[] = self::assess_link((string) $href, $site_url, $site_origin, $local_resolver); } $counts = [ 'confirmed_ok' => 0, 'confirmed_issue' => 0, 'review_needed' => 0, 'unknown' => 0, 'not_applicable' => 0, ]; foreach ($items as $item) { $status = (string) ($item['status'] ?? 'unknown'); if (array_key_exists($status, $counts)) { $counts[$status]++; } } return [ 'version' => self::VERSION, 'source' => 'local_content', 'networkRequests' => 0, 'linksFound' => count($hrefs), 'linksAssessed' => count($items), 'truncated' => count($hrefs) > self::MAX_LINKS, 'counts' => $counts, 'items' => $items, 'limitation' => 'External response and redirect state were not checked.', ]; } /** @return array */ public static function assess_post(WP_Post $post): array { $eligibility = TheDotDevAgenticBridge_Content_Policy::evaluate_post($post); if (empty($eligibility['eligible'])) { return [ 'version' => self::VERSION, 'source' => 'local_content', 'networkRequests' => 0, 'status' => 'not_applicable', 'reason' => (string) ($eligibility['reason'] ?? 'ineligible'), 'linksFound' => 0, 'linksAssessed' => 0, 'truncated' => false, 'counts' => [], 'items' => [], 'limitation' => 'Only eligible public posts and pages are assessed.', ]; } if (!class_exists('WP_HTML_Tag_Processor')) { return [ 'version' => self::VERSION, 'source' => 'local_content', 'networkRequests' => 0, 'status' => 'unknown', 'reason' => 'html_processor_unavailable', 'linksFound' => 0, 'linksAssessed' => 0, 'truncated' => false, 'counts' => [], 'items' => [], 'limitation' => 'WordPress HTML processing is unavailable.', ]; } $processor = new WP_HTML_Tag_Processor((string) $post->post_content); $hrefs = []; while (count($hrefs) < self::MAX_LINKS + 1 && $processor->next_tag('a')) { $href = $processor->get_attribute('href'); if (is_string($href)) { $hrefs[] = $href; } } return self::assess_links( $hrefs, home_url('/'), static function (string $url): array { $post_id = url_to_postid($url); if ($post_id <= 0) { return ['exists' => false, 'public' => false]; } $target = get_post($post_id); return [ 'exists' => $target instanceof WP_Post, 'public' => $target instanceof WP_Post && $target->post_status === 'publish' && $target->post_password === '', ]; }, ); } /** * @param callable(string): array{exists: bool, public: bool}|null $local_resolver * @return array */ private static function assess_link( string $href, string $site_url, string $site_origin, ?callable $local_resolver, ): array { $trimmed = trim($href); if ($trimmed === '' || str_starts_with($trimmed, '#')) { return self::item('not_applicable', 'same_page_or_empty', $trimmed); } $parts = wp_parse_url($trimmed); if (!is_array($parts)) { return self::item('confirmed_issue', 'invalid_url', 'invalid-url'); } $scheme = strtolower((string) ($parts['scheme'] ?? '')); if (in_array($scheme, ['mailto', 'tel'], true)) { return self::item('not_applicable', 'contact_link', $scheme . ':'); } if ($scheme !== '' && !in_array($scheme, ['http', 'https'], true)) { return self::item('confirmed_issue', 'unsupported_or_unsafe_scheme', $scheme . ':'); } $host = strtolower((string) ($parts['host'] ?? '')); $is_relative = $host === '' && $scheme === ''; $candidate_origin = $is_relative ? '' : self::origin_for_url($trimmed, (string) wp_parse_url($site_url, PHP_URL_SCHEME)); $is_internal = $is_relative || ($candidate_origin !== '' && $site_origin !== '' && hash_equals($site_origin, $candidate_origin)); if (!$is_internal) { return self::item('review_needed', 'external_not_checked', self::display_url($trimmed)); } $absolute = self::normalized_internal_url($trimmed, $site_url); if ($absolute === '' || $local_resolver === null) { return self::item('unknown', 'internal_target_not_resolved', self::display_url($trimmed)); } $resolved = $local_resolver($absolute); if (empty($resolved['exists'])) { return self::item('confirmed_issue', 'internal_target_missing', self::display_url($trimmed)); } if (empty($resolved['public'])) { return self::item('confirmed_issue', 'internal_target_not_public', self::display_url($trimmed)); } return self::item('confirmed_ok', 'internal_target_published', self::display_url($trimmed)); } private static function normalized_internal_url(string $href, string $site_url): string { if (str_starts_with($href, '//')) { $site_scheme = strtolower((string) wp_parse_url($site_url, PHP_URL_SCHEME)); $href = ($site_scheme !== '' ? $site_scheme : 'https') . ':' . $href; } $parts = wp_parse_url($href); if (!is_array($parts)) { return ''; } $path = (string) ($parts['path'] ?? '/'); if (empty($parts['host'])) { return rtrim($site_url, '/') . '/' . ltrim($path, '/'); } $scheme = strtolower((string) ($parts['scheme'] ?? 'https')); $host = strtolower((string) $parts['host']); $port = isset($parts['port']) ? ':' . (int) $parts['port'] : ''; return $scheme . '://' . $host . $port . '/' . ltrim($path, '/'); } private static function origin_for_url(string $url, string $default_scheme = ''): string { $parts = wp_parse_url($url); if (!is_array($parts) || empty($parts['host'])) { return ''; } $scheme = strtolower((string) ($parts['scheme'] ?? $default_scheme)); $host = strtolower((string) $parts['host']); if (!in_array($scheme, ['http', 'https'], true) || $host === '') { return ''; } $port = isset($parts['port']) ? (int) $parts['port'] : ($scheme === 'https' ? 443 : 80); return $scheme . '://' . $host . ':' . $port; } /** @return array{status: string, reason: string, displayUrl: string} */ private static function item(string $status, string $reason, string $display_url): array { return [ 'status' => $status, 'reason' => $reason, 'displayUrl' => self::bound_text($display_url), ]; } private static function display_url(string $url): string { $parts = wp_parse_url($url); if (!is_array($parts)) { return ''; } $scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) . '://' : ''; $host = strtolower((string) ($parts['host'] ?? '')); $path = (string) ($parts['path'] ?? ''); return self::bound_text($scheme . $host . $path); } private static function bound_text(string $value): string { $clean = sanitize_text_field($value); if (function_exists('mb_substr')) { return mb_substr($clean, 0, self::MAX_DISPLAY_CHARS); } return substr($clean, 0, self::MAX_DISPLAY_CHARS); } }