*/ public const INCLUSION_MODES = [self::INCLUSION_INHERIT, self::INCLUSION_INCLUDE, self::INCLUSION_EXCLUDE]; /** @var list */ public const INTENTS = [ 'general_information', 'article', 'about', 'service', 'product', 'faq', 'support', 'contact', 'legal', 'location', ]; /** @var list */ public const ALLOWED_ACTION_IDS = [ 'read_content', 'review_service_information', 'review_product_information', 'review_faq', 'find_contact_information', 'review_policy_information', 'find_location_information', ]; /** @var list */ public const BLOCKED_CLAIM_IDS = [ 'availability', 'price_guarantee', 'inventory', 'eligibility', 'endorsement', 'professional_advice', 'transaction_completion', ]; /** @var list */ public const RELATIONSHIP_TYPES = ['', 'service', 'product']; public static function action_label(string $id): string { return match ($id) { 'read_content' => __('Read page content', 'agentic-bridge'), 'review_service_information' => __('Review service information', 'agentic-bridge'), 'review_product_information' => __('Review product information', 'agentic-bridge'), 'review_faq' => __('Review FAQs', 'agentic-bridge'), 'find_contact_information' => __('Find contact information', 'agentic-bridge'), 'review_policy_information' => __('Review policy information', 'agentic-bridge'), 'find_location_information' => __('Find location information', 'agentic-bridge'), default => $id, }; } public static function blocked_claim_label(string $id): string { return match ($id) { 'availability' => __('Availability', 'agentic-bridge'), 'price_guarantee' => __('Price guarantees', 'agentic-bridge'), 'inventory' => __('Inventory status', 'agentic-bridge'), 'eligibility' => __('Eligibility', 'agentic-bridge'), 'endorsement' => __('Endorsements', 'agentic-bridge'), 'professional_advice' => __('Professional advice', 'agentic-bridge'), 'transaction_completion' => __('Transaction completion', 'agentic-bridge'), default => $id, }; } public static function intent_label(string $id): string { return match ($id) { 'general_information' => __('General information', 'agentic-bridge'), 'article' => __('Article', 'agentic-bridge'), 'about' => __('About', 'agentic-bridge'), 'service' => __('Service', 'agentic-bridge'), 'product' => __('Product', 'agentic-bridge'), 'faq' => __('FAQ', 'agentic-bridge'), 'support' => __('Support', 'agentic-bridge'), 'contact' => __('Contact', 'agentic-bridge'), 'legal' => __('Legal', 'agentic-bridge'), 'location' => __('Location', 'agentic-bridge'), default => $id, }; } public static function inclusion_mode_label(string $id): string { return match ($id) { self::INCLUSION_INHERIT => __('Inherit site defaults', 'agentic-bridge'), self::INCLUSION_INCLUDE => __('Include in artifacts', 'agentic-bridge'), self::INCLUSION_EXCLUDE => __('Exclude from artifacts', 'agentic-bridge'), default => $id, }; } public static function is_configurable_content(WP_Post $post): bool { return TheDotDevAgenticBridge_Content_Policy::evaluate_post($post)['eligible']; } public static function is_artifact_eligible(WP_Post $post): bool { if (!TheDotDevAgenticBridge_Content_Policy::evaluate_post($post)['eligible']) { return false; } $stored = self::normalized_store()['by_content_id'][(string) $post->ID] ?? []; $inclusion = is_array($stored) ? (string) ($stored['inclusionMode'] ?? self::INCLUSION_INHERIT) : self::INCLUSION_INHERIT; return $inclusion !== self::INCLUSION_EXCLUDE; } /** * @return list */ public static function artifact_eligible_content_items(): array { self::prune_stale_metadata(); if (self::artifact_publish_cap_exceeded()) { return []; } $items = []; TheDotDevAgenticBridge_Content_Policy::each_outbound_candidate(function (WP_Post $post) use (&$items): bool { if (!self::is_artifact_eligible($post)) { return true; } $items[] = TheDotDevAgenticBridge_Content_Policy::content_item_from_post($post); return true; }); return $items; } /** * Bounded artifact-eligible count (policy + metadata exclusion). Stops after $stop_after matches. */ public static function count_artifact_eligible_content(int $stop_after = TheDotDevAgenticBridge_Artifact_Validator::MAX_ENTRIES + 1): int { $count = 0; TheDotDevAgenticBridge_Content_Policy::each_outbound_candidate(function (WP_Post $post) use (&$count, $stop_after): bool { if (!self::is_artifact_eligible($post)) { return true; } ++$count; return $count < $stop_after; }); return $count; } public static function baseline_policy_eligible_count(): int { $count = 0; TheDotDevAgenticBridge_Content_Policy::each_outbound_candidate(function (WP_Post $post) use (&$count): bool { if (TheDotDevAgenticBridge_Content_Policy::evaluate_post($post)['eligible']) { ++$count; } return true; }); return $count; } public static function artifact_publish_cap_exceeded(): bool { return self::count_artifact_eligible_content() > TheDotDevAgenticBridge_Artifact_Validator::MAX_ENTRIES; } /** @return array */ public static function defaults(): array { return [ 'intent' => 'general_information', 'allowedActions' => ['read_content'], 'blockedClaims' => [], 'customBlockedClaims' => [], 'agentSummary' => '', 'relationshipLabel' => '', 'relationshipType' => '', ]; } /** @return array{version: string, defaults: array, defaultsReviewedAt: string, by_content_id: array>} */ public static function normalized_store(): array { $settings = TheDotDevAgenticBridge_Storage::get(); $raw = is_array($settings['content_metadata'] ?? null) ? $settings['content_metadata'] : []; $normalized = [ 'version' => self::VERSION, 'defaults' => self::normalize_defaults(is_array($raw['defaults'] ?? null) ? $raw['defaults'] : [], false), 'defaultsReviewedAt' => is_string($raw['defaultsReviewedAt'] ?? null) ? (string) $raw['defaultsReviewedAt'] : '', 'by_content_id' => self::normalize_by_content_id(is_array($raw['by_content_id'] ?? null) ? $raw['by_content_id'] : []), ]; if ((wp_json_encode($raw) ?: '') !== (wp_json_encode($normalized) ?: '')) { self::persist_store($normalized); } return $normalized; } /** * @param array $input * @return array{ok: bool, errors: list} */ public static function save_defaults(array $input): array { $normalized = self::normalize_item_config($input, true, false); if ($normalized['errors'] !== []) { return ['ok' => false, 'errors' => $normalized['errors']]; } $store = self::normalized_store(); $store['defaults'] = $normalized['config']; $store['defaultsReviewedAt'] = gmdate('c'); self::persist_store($store); TheDotDevAgenticBridge_Artifacts::mark_preview_stale(); return ['ok' => true, 'errors' => []]; } /** * @param array $input * @return array{ok: bool, errors: list} */ public static function save_content_config(int $content_id, array $input): array { if ($content_id <= 0) { return ['ok' => false, 'errors' => ['invalid_content_id']]; } $post = get_post($content_id); if (!$post instanceof WP_Post || !TheDotDevAgenticBridge_Content_Policy::is_manageable_preview_post($content_id)) { return ['ok' => false, 'errors' => ['content_not_manageable']]; } if (!self::is_configurable_content($post)) { return ['ok' => false, 'errors' => ['content_not_configurable']]; } $store = self::normalized_store(); if (!isset($store['by_content_id'][(string) $content_id]) && count($store['by_content_id']) >= self::MAX_BY_CONTENT_ID) { return ['ok' => false, 'errors' => ['max_configs_reached']]; } $normalized = self::normalize_item_config($input, true, true); if ($normalized['errors'] !== []) { return ['ok' => false, 'errors' => $normalized['errors']]; } $config = $normalized['config']; $config['updatedAt'] = gmdate('c'); $config['sourceModifiedAt'] = get_post_modified_time('c', true, $post); $key = (string) $content_id; if (self::item_is_empty($config)) { unset($store['by_content_id'][$key]); } else { $store['by_content_id'][$key] = $config; } self::persist_store($store); TheDotDevAgenticBridge_Artifacts::mark_preview_stale(); return ['ok' => true, 'errors' => []]; } /** Validate a proposed item without persisting it. */ public static function validate_content_config(array $input): array { $normalized = self::normalize_item_config($input, true, true); return ['ok' => $normalized['errors'] === [], 'errors' => $normalized['errors'], 'config' => $normalized['config']]; } /** @return array */ public static function effective_config_for_post(WP_Post $post): array { $store = self::normalized_store(); $defaults = $store['defaults']; $raw = $store['by_content_id'][(string) $post->ID] ?? []; if (!is_array($raw) || $raw === []) { return self::normalize_item_config($defaults, false, false)['config']; } if (!empty($raw['inheritDefaults'])) { $merged = array_merge($defaults, [ 'inclusionMode' => $raw['inclusionMode'] ?? self::INCLUSION_INHERIT, 'inheritDefaults' => true, 'updatedAt' => $raw['updatedAt'] ?? '', 'sourceModifiedAt' => $raw['sourceModifiedAt'] ?? '', ]); return self::normalize_item_config($merged, false, false)['config']; } return self::normalize_item_config(array_merge($defaults, $raw), false, false)['config']; } /** Exact stored per-content record; distinct from effective defaults. */ public static function raw_record(int $content_id): array { $store = self::normalized_store(); $key = (string) $content_id; return ['present' => array_key_exists($key, $store['by_content_id']), 'value' => $store['by_content_id'][$key] ?? null]; } /** Restore exact per-content record presence/value for a reversible local lifecycle. */ public static function restore_raw_record(int $content_id, array $snapshot): array { $post = get_post($content_id); if (!$post instanceof WP_Post || !TheDotDevAgenticBridge_Content_Policy::is_manageable_preview_post($content_id)) return ['ok' => false, 'errors' => ['content_not_manageable']]; $store = self::normalized_store(); $key = (string) $content_id; if (empty($snapshot['present'])) { unset($store['by_content_id'][$key]); self::persist_store($store); TheDotDevAgenticBridge_Artifacts::mark_preview_stale(); return ['ok' => true, 'errors' => []]; } if (!is_array($snapshot['value'] ?? null)) return ['ok' => false, 'errors' => ['invalid_snapshot']]; $normalized = self::normalize_item_config($snapshot['value'], true, true); if ($normalized['errors'] !== []) return ['ok' => false, 'errors' => $normalized['errors']]; $store['by_content_id'][$key] = $normalized['config']; self::persist_store($store); TheDotDevAgenticBridge_Artifacts::mark_preview_stale(); return ['ok' => true, 'errors' => []]; } public static function source_fingerprint(): string { self::prune_stale_metadata(); $store = self::normalized_store(); $items = []; foreach (self::artifact_eligible_content_items() as $item) { $post = get_post((int) $item['id']); if (!$post instanceof WP_Post) { continue; } $items[] = [ 'id' => (int) $item['id'], 'type' => (string) $item['type'], 'title' => (string) $item['title'], 'url' => (string) $item['url'], 'modifiedAt' => (string) $item['modifiedAt'], 'config' => self::effective_config_for_post($post), ]; } usort($items, static fn(array $a, array $b): int => $a['id'] <=> $b['id']); return hash('sha256', wp_json_encode([ 'siteUrl' => home_url('/'), 'siteName' => get_bloginfo('name'), 'schemaVersion' => TheDotDevAgenticBridge_Artifacts::SCHEMA_VERSION, 'metadataVersion' => self::VERSION, 'defaults' => $store['defaults'], 'defaultsReviewedAt' => $store['defaultsReviewedAt'], 'excluded' => TheDotDevAgenticBridge_Content_Policy::excluded_content_ids(), 'items' => $items, ]) ?: ''); } public static function prune_stale_metadata(): void { $store = self::normalized_store(); $pruned = []; $changed = false; foreach ($store['by_content_id'] as $key => $config) { $content_id = (int) $key; $post = get_post($content_id); if (!$post instanceof WP_Post || !self::is_configurable_content($post)) { $changed = true; continue; } $pruned[$key] = $config; } if ($pruned !== $store['by_content_id']) { $changed = true; } if ($changed) { $store['by_content_id'] = $pruned; self::persist_store($store); } } public static function defaults_reviewed(): bool { return self::normalized_store()['defaultsReviewedAt'] !== ''; } public static function eligible_metadata_coverage_complete(): bool { if (!self::defaults_reviewed()) { return false; } foreach (self::artifact_eligible_content_items() as $item) { $post = get_post((int) $item['id']); if (!$post instanceof WP_Post) { return false; } if (!self::has_valid_explicit_or_inherited_config($post)) { return false; } } return self::artifact_eligible_content_items() !== []; } public static function has_valid_explicit_or_inherited_config(WP_Post $post): bool { $store = self::normalized_store(); $stored = $store['by_content_id'][(string) $post->ID] ?? null; if (is_array($stored)) { if (!empty($stored['inheritDefaults'])) { return self::defaults_reviewed() && !self::config_is_stale($post, $stored); } return !self::config_is_stale($post, $stored); } return self::defaults_reviewed(); } /** @param array $stored */ public static function config_is_stale(WP_Post $post, array $stored): bool { $source_modified = (string) ($stored['sourceModifiedAt'] ?? ''); $current_modified = get_post_modified_time('c', true, $post); return $source_modified !== '' && $current_modified !== '' && $source_modified !== $current_modified; } /** * @return array{rows: list>, total: int, page: int, page_count: int} */ public static function inventory_query(int $page, int $per_page, string $search = '', string $filter = ''): array { self::prune_stale_metadata(); $store = self::normalized_store(); $per_page = max(1, $per_page); $page = max(1, $page); $search = strtolower(trim($search)); $rows = []; foreach (TheDotDevAgenticBridge_Content_Policy::preview_rows(1, TheDotDevAgenticBridge_Content_Policy::PREVIEW_PER_PAGE * 100) as $policy_row) { $post = get_post((int) $policy_row['id']); if (!$post instanceof WP_Post) { continue; } $title = strtolower((string) $policy_row['title']); if ($search !== '' && !str_contains($title, $search) && !str_contains((string) $policy_row['id'], $search)) { continue; } $key = (string) $post->ID; $stored = $store['by_content_id'][$key] ?? null; $effective = self::effective_config_for_post($post); $policy_eval = TheDotDevAgenticBridge_Content_Policy::evaluate_preview_post($post); $configurable = self::is_configurable_content($post); $explicit = is_array($stored) && empty($stored['inheritDefaults']); $inherited = is_array($stored) && !empty($stored['inheritDefaults']); $configured = is_array($stored); $stale = is_array($stored) && self::config_is_stale($post, $stored); $row = [ 'id' => (int) $post->ID, 'type' => (string) $policy_row['type'], 'title' => (string) $policy_row['title'], 'postStatus' => (string) $policy_row['postStatus'], 'policyEligible' => (bool) $policy_eval['eligible'], 'policyReason' => (string) $policy_eval['reason'], 'artifactEligible' => self::is_artifact_eligible($post), 'configurable' => $configurable, 'inclusionMode' => (string) ($effective['inclusionMode'] ?? self::INCLUSION_INHERIT), 'intent' => (string) ($effective['intent'] ?? 'general_information'), 'configured' => $configured, 'explicit' => $explicit, 'inherited' => $inherited, 'stale' => $stale, ]; if ($filter === 'eligible' && !$row['policyEligible']) { continue; } if ($filter === 'configured' && !$row['configured']) { continue; } if ($filter === 'unconfigured' && $row['configured']) { continue; } if ($filter === 'stale' && !$row['stale']) { continue; } $rows[] = $row; } $total = count($rows); $page_count = max(1, (int) ceil($total / $per_page)); $page = min($page, $page_count); $offset = ($page - 1) * $per_page; return [ 'rows' => array_slice($rows, $offset, $per_page), 'total' => $total, 'page' => $page, 'page_count' => $page_count, ]; } /** @return list */ public static function export_allowed_actions(array $action_ids): array { $out = []; foreach (self::normalize_action_ids($action_ids, false)['ids'] as $id) { $out[] = ['id' => $id, 'mode' => 'metadata_only', 'executable' => false]; } return $out; } /** @param array $config @return array{label: string, type: string}|null */ public static function export_relationship(array $config): ?array { $type = (string) ($config['relationshipType'] ?? ''); $label = (string) ($config['relationshipLabel'] ?? ''); if (!in_array($type, ['service', 'product'], true) || $label === '') { return null; } return ['label' => $label, 'type' => $type]; } public static function contains_dangerous_text_public(string $text): bool { return self::contains_dangerous_text($text); } /** @param array{version: string, defaults: array, defaultsReviewedAt: string, by_content_id: array>} $store */ private static function persist_store(array $store): void { TheDotDevAgenticBridge_Storage::update(['content_metadata' => $store]); } /** @param array $input @return array{config: array, errors: list} */ private static function normalize_item_config(array $input, bool $strict, bool $is_content_item): array { $errors = []; $known_keys = [ 'inclusionMode', 'inheritDefaults', 'intent', 'allowedActions', 'blockedClaims', 'customBlockedClaims', 'agentSummary', 'relationshipLabel', 'relationshipType', 'updatedAt', 'sourceModifiedAt', ]; if ($strict) { foreach (array_keys($input) as $key) { if (!in_array((string) $key, $known_keys, true)) { $errors[] = 'unknown_field'; } } } $inclusion_raw = array_key_exists('inclusionMode', $input) ? $input['inclusionMode'] : self::INCLUSION_INHERIT; if (!is_string($inclusion_raw)) { if ($strict) { $errors[] = 'inclusion_mode'; } $inclusion = self::INCLUSION_INHERIT; } else { $inclusion = sanitize_key($inclusion_raw); } if (!in_array($inclusion, self::INCLUSION_MODES, true)) { $errors[] = 'inclusion_mode'; if (!$strict) { $inclusion = self::INCLUSION_INHERIT; } } $inherit_defaults = !empty($input['inheritDefaults']); if ($is_content_item && $strict && !array_key_exists('inheritDefaults', $input) && $inclusion === self::INCLUSION_INHERIT) { $inherit_defaults = true; } $intent_raw = array_key_exists('intent', $input) ? $input['intent'] : 'general_information'; if (!is_string($intent_raw)) { if ($strict) { $errors[] = 'intent'; } $intent = 'general_information'; } else { $intent = sanitize_key($intent_raw); } if (!in_array($intent, self::INTENTS, true)) { $errors[] = 'intent'; if (!$strict) { $intent = 'general_information'; } } $allowed_result = self::normalize_action_ids( array_key_exists('allowedActions', $input) ? $input['allowedActions'] : [], $strict, ); if ($allowed_result['error'] !== '') { $errors[] = $allowed_result['error']; } $allowed = $allowed_result['ids']; if ($strict && $allowed === [] && $inherit_defaults) { $allowed = ['read_content']; } if ($strict && $allowed === [] && (!$inherit_defaults || !$is_content_item)) { $errors[] = 'allowed_actions_empty'; } $blocked_result = self::normalize_blocked_ids( array_key_exists('blockedClaims', $input) ? $input['blockedClaims'] : [], $strict, ); if ($blocked_result['error'] !== '') { $errors[] = $blocked_result['error']; } $blocked = $blocked_result['ids']; $custom_blocked = self::normalize_custom_blocked( array_key_exists('customBlockedClaims', $input) ? $input['customBlockedClaims'] : [], $strict, $errors, ); $summary = self::normalize_bounded_text_field( array_key_exists('agentSummary', $input) ? $input['agentSummary'] : '', self::MAX_SUMMARY_CHARS, $strict, $errors, 'agent_summary', ); $relationship_label = self::normalize_bounded_text_field( array_key_exists('relationshipLabel', $input) ? $input['relationshipLabel'] : '', self::MAX_RELATIONSHIP_CHARS, $strict, $errors, 'relationship_label', ); $relationship_type_raw = array_key_exists('relationshipType', $input) ? $input['relationshipType'] : ''; if (!is_string($relationship_type_raw)) { if ($strict && array_key_exists('relationshipType', $input)) { $errors[] = 'relationship_type'; } $relationship_type = ''; } else { $relationship_type = sanitize_key($relationship_type_raw); } if (!in_array($relationship_type, self::RELATIONSHIP_TYPES, true)) { $errors[] = 'relationship_type'; $relationship_type = ''; } if ($relationship_type === '' && $relationship_label !== '') { $errors[] = 'relationship_type_required'; } if ($relationship_type !== '' && !in_array($relationship_type, ['service', 'product'], true)) { $errors[] = 'relationship_type'; } if (in_array($relationship_type, ['service', 'product'], true) && $relationship_label === '' && $strict) { $errors[] = 'relationship_label_required'; } return [ 'config' => [ 'inclusionMode' => $inclusion, 'inheritDefaults' => $inherit_defaults, 'intent' => $intent, 'allowedActions' => $allowed === [] && !$strict ? ['read_content'] : $allowed, 'blockedClaims' => $blocked, 'customBlockedClaims' => $custom_blocked, 'agentSummary' => $summary, 'relationshipLabel' => $relationship_label, 'relationshipType' => $relationship_type, 'updatedAt' => is_string($input['updatedAt'] ?? null) ? $input['updatedAt'] : '', 'sourceModifiedAt' => is_string($input['sourceModifiedAt'] ?? null) ? $input['sourceModifiedAt'] : '', ], 'errors' => array_values(array_unique($errors)), ]; } /** @param array $input */ private static function normalize_defaults(array $input, bool $strict): array { return self::normalize_item_config(array_merge(self::defaults(), $input), $strict, false)['config']; } /** @param array $raw @return array> */ private static function normalize_by_content_id(array $raw): array { $out = []; foreach ($raw as $key => $value) { if (!is_array($value)) { continue; } $content_id = (int) $key; if ($content_id <= 0) { continue; } $normalized = self::normalize_item_config($value, false, true); if (!self::item_is_empty($normalized['config'])) { $out[(string) $content_id] = $normalized['config']; } if (count($out) >= self::MAX_BY_CONTENT_ID) { break; } } return $out; } /** @param array $config */ private static function item_is_empty(array $config): bool { return ($config['inclusionMode'] ?? self::INCLUSION_INHERIT) === self::INCLUSION_INHERIT && !empty($config['inheritDefaults']) && ($config['intent'] ?? 'general_information') === 'general_information' && ($config['allowedActions'] ?? ['read_content']) === ['read_content'] && ($config['blockedClaims'] ?? []) === [] && ($config['customBlockedClaims'] ?? []) === [] && ($config['agentSummary'] ?? '') === '' && ($config['relationshipLabel'] ?? '') === '' && ($config['relationshipType'] ?? '') === ''; } /** @return array{ids: list, error: string} */ private static function normalize_action_ids(mixed $raw, bool $strict): array { if (!is_array($raw)) { return ['ids' => [], 'error' => $strict ? 'allowed_actions_type' : '']; } $ids = []; foreach ($raw as $id) { if (is_string($id)) { $key = sanitize_key($id); } elseif (is_int($id)) { $key = sanitize_key((string) $id); } else { return ['ids' => [], 'error' => $strict ? 'allowed_actions_type' : '']; } if ($key === '') { continue; } if (!in_array($key, self::ALLOWED_ACTION_IDS, true)) { return ['ids' => [], 'error' => 'allowed_action_unknown']; } $ids[] = $key; } return ['ids' => array_values(array_unique($ids)), 'error' => '']; } /** @return array{ids: list, error: string} */ private static function normalize_blocked_ids(mixed $raw, bool $strict): array { if (!is_array($raw)) { return ['ids' => [], 'error' => $strict ? 'blocked_claims_type' : '']; } $ids = []; foreach ($raw as $id) { if (is_string($id)) { $key = sanitize_key($id); } elseif (is_int($id)) { $key = sanitize_key((string) $id); } else { return ['ids' => [], 'error' => $strict ? 'blocked_claims_type' : '']; } if ($key === '') { continue; } if (!in_array($key, self::BLOCKED_CLAIM_IDS, true)) { return ['ids' => [], 'error' => 'blocked_claim_unknown']; } $ids[] = $key; } return ['ids' => array_values(array_unique($ids)), 'error' => '']; } /** * @param mixed $raw * @param list $errors * @return list */ private static function normalize_custom_blocked(mixed $raw, bool $strict, array &$errors): array { if (!is_array($raw)) { if ($strict) { $errors[] = 'custom_blocked_type'; } return []; } if ($strict && count($raw) > self::MAX_CUSTOM_BLOCKED) { $errors[] = 'custom_blocked_count'; return []; } $out = []; foreach ($raw as $value) { if (is_string($value)) { $text = self::sanitize_bounded_text($value, self::MAX_CUSTOM_CLAIM_CHARS, $strict, $errors, 'custom_blocked_claim'); } elseif (is_int($value)) { $text = self::sanitize_bounded_text((string) $value, self::MAX_CUSTOM_CLAIM_CHARS, $strict, $errors, 'custom_blocked_claim'); } else { if ($strict) { $errors[] = 'custom_blocked_type'; } continue; } if (count($out) >= self::MAX_CUSTOM_BLOCKED) { if ($strict) { $errors[] = 'custom_blocked_count'; } break; } if ($text !== '') { $out[] = $text; } } return $out; } /** * @param mixed $raw * @param list $errors */ private static function normalize_bounded_text_field(mixed $raw, int $max, bool $strict, array &$errors, string $field): string { if (!is_string($raw)) { if ($strict) { $errors[] = $field; } return ''; } return self::sanitize_bounded_text($raw, $max, $strict, $errors, $field); } /** * @param list $errors */ private static function sanitize_bounded_text(string $text, int $max, bool $strict, array &$errors, string $field): string { $raw = trim($text); if ($raw === '') { return ''; } if (self::contains_dangerous_text($raw)) { $errors[] = $field; return ''; } $text = trim(wp_strip_all_tags($raw)); if ($text === '') { if ($strict && $raw !== '') { $errors[] = $field; } return ''; } if (self::utf8_length($text) > $max) { $errors[] = $field . '_oversize'; return ''; } return $text; } private static function utf8_length(string $text): int { return TheDotDevAgenticBridge_Artifact_Validator::utf8_length($text); } private static function contains_dangerous_text(string $text): bool { $lower = strtolower($text); $patterns = [ '/https?:\\/\\//i', '/\\/\\//', '/<\\s*\\/?\\s*[a-z]/i', '/javascript:/i', '/data:/i', '/\\b[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}\\b/i', '/\\b(?:\\+?\\d[\\d\\s().-]{7,}\\d)\\b/', '/\\b(api[_-]?key|secret|token|password|credential)\\b/i', '/\\b(ignore (all|previous) instructions|system prompt|jailbreak)\\b/i', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $text) === 1 || preg_match($pattern, $lower) === 1) { return true; } } return false; } }