diff --git a/includes/class-webdecoy-activator.php b/includes/class-webdecoy-activator.php index 412cc16..37cbe42 100644 --- a/includes/class-webdecoy-activator.php +++ b/includes/class-webdecoy-activator.php @@ -21,7 +21,7 @@ class WebDecoy_Activator /** * Database version for migrations */ - private const DB_VERSION = '2.2.0'; + private const DB_VERSION = '2.3.0'; /** * Plugin activation @@ -129,6 +129,21 @@ private static function create_tables(): void KEY created_at (created_at) ) $charset_collate;"; + // AI referral counts (2.10.0): visits AI products sent, by platform and + // landing path, waiting to be sent to WebDecoy Cloud. Counts only; + // nothing about a visitor. ref_key is sha1(platform + path), so the + // key stays within index limits on older MySQL; batch_id is '' for + // open counts and the claim id of a batch being sent. + $sql_ai_referrals = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}webdecoy_ai_referrals ( + ref_key CHAR(40) NOT NULL, + batch_id VARCHAR(36) NOT NULL DEFAULT '', + platform VARCHAR(50) NOT NULL, + landing_path VARCHAR(500) NOT NULL, + referrals BIGINT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (ref_key, batch_id), + KEY batch_id (batch_id) + ) $charset_collate;"; + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql_blocked); @@ -136,6 +151,7 @@ private static function create_tables(): void dbDelta($sql_rate_limits); dbDelta($sql_checkout); dbDelta($sql_violation_queue); + dbDelta($sql_ai_referrals); } /** @@ -272,6 +288,7 @@ public static function uninstall(): void wp_clear_scheduled_hook('webdecoy_cleanup_expired'); wp_clear_scheduled_hook('webdecoy_sync_blocked_ips'); wp_clear_scheduled_hook('webdecoy_flush_violations'); + wp_clear_scheduled_hook('webdecoy_flush_ai_referrals'); wp_clear_scheduled_hook('webdecoy_sync_entitlements'); wp_clear_scheduled_hook('webdecoy_sync_actor_feed'); } diff --git a/includes/class-webdecoy-ai-referrals.php b/includes/class-webdecoy-ai-referrals.php new file mode 100644 index 0000000..2658605 --- /dev/null +++ b/includes/class-webdecoy-ai-referrals.php @@ -0,0 +1,188 @@ +prefix . 'webdecoy_ai_referrals'; + } + + /** + * Whether counting runs: connected to WebDecoy Cloud, and not turned off + * with the webdecoy_count_ai_referrals filter. + */ + public static function enabled(string $apiKey): bool + { + return $apiKey !== '' && (bool) apply_filters('webdecoy_count_ai_referrals', true); + } + + /** Wire the counter and its flush, scheduling the flush if it is not. */ + public static function register(string $apiKey): void + { + if (!self::enabled($apiKey)) { + return; + } + add_action('template_redirect', [self::class, 'observe'], 1); + add_action(self::CRON_HOOK, static function () use ($apiKey): void { + self::flush($apiKey); + }); + if (function_exists('wp_next_scheduled') && !wp_next_scheduled(self::CRON_HOOK)) { + wp_schedule_event(time() + 900, 'fifteen_minutes', self::CRON_HOOK); + } + } + + /** Count the current request if it is a page visit an AI product sent. */ + public static function observe(): void + { + // phpcs:disable WordPress.Security.ValidatedSanitizedInput -- compared and classified, never stored or echoed + $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? '')); + $mode = (string) ($_SERVER['HTTP_SEC_FETCH_MODE'] ?? ''); + $dest = (string) ($_SERVER['HTTP_SEC_FETCH_DEST'] ?? ''); + $referer = wp_unslash((string) ($_SERVER['HTTP_REFERER'] ?? '')); + $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/'); + // phpcs:enable + $platform = self::classifyRequest($method, $mode, $dest, $referer, $uri); + if ($platform === '') { + return; + } + self::increment($platform, self::landingPath($uri)); + } + + /** + * The AI platform a request came from, or '' when it is not a page visit + * an AI product sent. Pure, for tests. + */ + public static function classifyRequest(string $method, string $mode, string $dest, string $referer, string $uri): string + { + if ($method !== 'GET' || $mode !== 'navigate' || ($dest !== '' && $dest !== 'document')) { + return ''; + } + return \WebDecoy\LlmReferral::classify($referer, 'https://site.invalid' . $uri); + } + + /** The path of a request URI, cut to the width WebDecoy stores. */ + public static function landingPath(string $uri): string + { + $path = (string) (parse_url($uri, PHP_URL_PATH) ?: '/'); + if ($path === '' || $path[0] !== '/') { + $path = '/' . $path; + } + if (strlen($path) > self::MAX_PATH) { + $path = substr($path, 0, self::MAX_PATH); + } + return mb_check_encoding($path, 'UTF-8') ? $path : '/'; + } + + private static function increment(string $platform, string $path): void + { + global $wpdb; + $table = self::table(); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix + $rows = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); + $key = sha1($platform . "\n" . $path); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $exists = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE ref_key = %s AND batch_id = ''", $key)); + if ($rows >= self::MAX_ROWS && $exists === 0) { + return; // Bounded: a new pair is dropped rather than growing the table. + } + // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix + $wpdb->query($wpdb->prepare( + "INSERT INTO {$table} (ref_key, batch_id, platform, landing_path, referrals) VALUES (%s, '', %s, %s, 1) + ON DUPLICATE KEY UPDATE referrals = referrals + 1", + $key, + $platform, + $path + )); + // phpcs:enable + } + + /** + * Send counted referrals. A batch that failed before is resent under its + * own id first; otherwise the open counts are claimed under a new one. + */ + public static function flush(string $apiKey): void + { + if ($apiKey === '' || !function_exists('wp_remote_post')) { + return; + } + global $wpdb; + $table = self::table(); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $batch = (string) $wpdb->get_var("SELECT batch_id FROM {$table} WHERE batch_id <> '' LIMIT 1"); + if ($batch === '') { + $batch = wp_generate_uuid4(); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $claimed = (int) $wpdb->query($wpdb->prepare("UPDATE {$table} SET batch_id = %s WHERE batch_id = '' LIMIT %d", $batch, self::BATCH_SIZE)); + if ($claimed === 0) { + return; + } + } + // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $rows = $wpdb->get_results($wpdb->prepare("SELECT platform, landing_path, referrals FROM {$table} WHERE batch_id = %s", $batch)); + $referrals = []; + foreach ((array) $rows as $row) { + $referrals[] = [ + 'platform' => (string) $row->platform, + 'path' => (string) $row->landing_path, + 'count' => (int) $row->referrals, + ]; + } + if ($referrals === [] || self::send($apiKey, $batch, $referrals)) { + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->delete($table, ['batch_id' => $batch]); + } + } + + /** + * True when the batch needs no retry: accepted, or refused for a reason a + * retry cannot fix (a 4xx, such as a key not scoped to one site). + * + * @param array $referrals + */ + private static function send(string $apiKey, string $batch, array $referrals): bool + { + $response = wp_remote_post(self::ENDPOINT, [ + 'timeout' => 5, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $apiKey, + ], + 'body' => wp_json_encode(['report_id' => $batch, 'source' => 'wordpress', 'referrals' => $referrals]), + ]); + if (is_wp_error($response)) { + return false; + } + $code = (int) wp_remote_retrieve_response_code($response); + return $code >= 200 && $code < 500; + } +} diff --git a/readme.txt b/readme.txt index 9af2886..f321a16 100644 --- a/readme.txt +++ b/readme.txt @@ -257,6 +257,7 @@ What is sent, and when: * When you click "Connect to WebDecoy Cloud": your browser is redirected to app.webdecoy.com to approve the connection (carrying your site URL, site name, a one-time nonce, and your monthly-report preference). After you approve, the plugin exchanges a one-time token with api.webdecoy.com (sending the token, your site URL and the nonce) to receive the site's API keys. Cancelling sends nothing further. * After connecting: the plugin fetches your plan entitlements from ingest.webdecoy.com (authenticated with your API key) twice daily. * When a detection or rule violation occurs: the visitor's IP address, user agent, request path, threat score and detection flags are sent to ingest.webdecoy.com so the event appears in your cloud dashboard. +* When a visitor arrives from an AI product such as ChatGPT, Claude, Perplexity or Gemini: the plugin adds one to a count for that AI product and the landing page's path, and sends those counts to ingest.webdecoy.com every fifteen minutes so they appear on your AI Traffic page. Only the AI product's name, the path and the count are sent; nothing about the visitor. Turn it off with the `webdecoy_count_ai_referrals` filter. * When you use an IP-reputation filter rule (e.g. ip.abuse_score, ip.tor): the visitor's IP address is sent to ingest.webdecoy.com to look up reputation/geo data. * When validating your key or forwarding a WooCommerce checkout detection: your API key, organization ID and the detection data above are sent to api.webdecoy.com / ingest.webdecoy.com. diff --git a/sdk/src/LlmReferral.php b/sdk/src/LlmReferral.php new file mode 100644 index 0000000..c6fcb4d --- /dev/null +++ b/sdk/src/LlmReferral.php @@ -0,0 +1,122 @@ +|null */ + private static $platforms = null; + + /** @return array referrer hostname => platform */ + public static function platforms(): array + { + if (self::$platforms === null) { + $loaded = require __DIR__ . '/registry/llm-platforms.generated.php'; + self::$platforms = is_array($loaded) ? $loaded : []; + } + return self::$platforms; + } + + /** + * The platform a referral came from, or '' when it is not an AI referral. + * A non-empty Referer decides on its own even when it names no AI + * product: a campaign tag is easy to forge and must not overrule it. + */ + public static function classify(string $referer, string $pageUrl): string + { + $platforms = self::platforms(); + $referer = trim($referer); + if ($referer !== '') { + $host = parse_url($referer, PHP_URL_HOST); + if (!is_string($host) || $host === '') { + return ''; + } + return $platforms[strtolower($host)] ?? ''; + } + $query = parse_url($pageUrl, PHP_URL_QUERY); + if (!is_string($query) || $query === '') { + return ''; + } + $params = self::queryValues($query); + foreach (self::TAG_KEYS as $key) { + foreach ($params[$key] ?? [] as $value) { + $platform = self::platformForTag($value); + if ($platform !== '') { + return $platform; + } + } + } + return ''; + } + + /** + * Every value of every parameter, in order. parse_str() keeps only the + * last of a repeated key, and a tag must be read the way browsers and the + * other sensors read it. + * + * @return array + */ + private static function queryValues(string $query): array + { + $out = []; + foreach (explode('&', $query) as $pair) { + if ($pair === '') { + continue; + } + $parts = explode('=', $pair, 2); + $key = urldecode(str_replace('+', ' ', $parts[0])); + $out[$key][] = urldecode(str_replace('+', ' ', $parts[1] ?? '')); + } + return $out; + } + + private static function squash(string $s): string + { + return str_replace([' ', '-', '_', '.'], '', $s); + } + + private static function platformForTag(string $raw): string + { + $platforms = self::platforms(); + $value = strtolower(trim($raw)); + if ($value === '') { + return ''; + } + if (strpos($value, '://') !== false) { + $host = parse_url($value, PHP_URL_HOST); + if (is_string($host) && $host !== '') { + $value = strtolower($host); + } + } + $value = rtrim($value, '.'); + if (isset($platforms[$value])) { + return $platforms[$value]; + } + $normalized = self::squash($value); + foreach ($platforms as $domain => $name) { + $bare = str_replace('.', '', preg_replace('/^www\./', '', $domain) ?? $domain); + if ($normalized === self::squash(strtolower($name)) || $normalized === $bare) { + return $name; + } + } + return ''; + } +} diff --git a/sdk/src/registry/llm-platforms.generated.php b/sdk/src/registry/llm-platforms.generated.php new file mode 100644 index 0000000..7946939 --- /dev/null +++ b/sdk/src/registry/llm-platforms.generated.php @@ -0,0 +1,34 @@ + AI platform. + */ + +declare(strict_types=1); + +return [ + 'chat.deepseek.com' => 'DeepSeek', + 'chat.openai.com' => 'ChatGPT', + 'chatgpt.com' => 'ChatGPT', + 'claude.ai' => 'Claude', + 'copilot.microsoft.com' => 'Copilot', + 'deepseek.com' => 'DeepSeek', + 'gemini.google.com' => 'Gemini', + 'grok.com' => 'Grok', + 'kagi.com' => 'Kagi', + 'meta.ai' => 'Meta AI', + 'perplexity.ai' => 'Perplexity', + 'phind.com' => 'Phind', + 'www.deepseek.com' => 'DeepSeek', + 'www.grok.com' => 'Grok', + 'www.kagi.com' => 'Kagi', + 'www.meta.ai' => 'Meta AI', + 'www.perplexity.ai' => 'Perplexity', + 'www.phind.com' => 'Phind', + 'www.you.com' => 'You.com', + 'you.com' => 'You.com', +]; diff --git a/sdk/src/registry/llm-referral-vectors.generated.json b/sdk/src/registry/llm-referral-vectors.generated.json new file mode 100644 index 0000000..6e2b880 --- /dev/null +++ b/sdk/src/registry/llm-referral-vectors.generated.json @@ -0,0 +1,397 @@ +[ + { + "referer": "https://chat.deepseek.com/c/123", + "page_url": "https://site.example/", + "platform": "DeepSeek" + }, + { + "referer": "https://CHAT.DEEPSEEK.COM/", + "page_url": "https://site.example/", + "platform": "DeepSeek" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=chat.deepseek.com", + "platform": "DeepSeek" + }, + { + "referer": "https://chat.openai.com/c/123", + "page_url": "https://site.example/", + "platform": "ChatGPT" + }, + { + "referer": "https://CHAT.OPENAI.COM/", + "page_url": "https://site.example/", + "platform": "ChatGPT" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=chat.openai.com", + "platform": "ChatGPT" + }, + { + "referer": "https://chatgpt.com/c/123", + "page_url": "https://site.example/", + "platform": "ChatGPT" + }, + { + "referer": "https://CHATGPT.COM/", + "page_url": "https://site.example/", + "platform": "ChatGPT" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=chatgpt.com", + "platform": "ChatGPT" + }, + { + "referer": "https://claude.ai/c/123", + "page_url": "https://site.example/", + "platform": "Claude" + }, + { + "referer": "https://CLAUDE.AI/", + "page_url": "https://site.example/", + "platform": "Claude" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=claude.ai", + "platform": "Claude" + }, + { + "referer": "https://copilot.microsoft.com/c/123", + "page_url": "https://site.example/", + "platform": "Copilot" + }, + { + "referer": "https://COPILOT.MICROSOFT.COM/", + "page_url": "https://site.example/", + "platform": "Copilot" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=copilot.microsoft.com", + "platform": "Copilot" + }, + { + "referer": "https://deepseek.com/c/123", + "page_url": "https://site.example/", + "platform": "DeepSeek" + }, + { + "referer": "https://DEEPSEEK.COM/", + "page_url": "https://site.example/", + "platform": "DeepSeek" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=deepseek.com", + "platform": "DeepSeek" + }, + { + "referer": "https://gemini.google.com/c/123", + "page_url": "https://site.example/", + "platform": "Gemini" + }, + { + "referer": "https://GEMINI.GOOGLE.COM/", + "page_url": "https://site.example/", + "platform": "Gemini" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=gemini.google.com", + "platform": "Gemini" + }, + { + "referer": "https://grok.com/c/123", + "page_url": "https://site.example/", + "platform": "Grok" + }, + { + "referer": "https://GROK.COM/", + "page_url": "https://site.example/", + "platform": "Grok" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=grok.com", + "platform": "Grok" + }, + { + "referer": "https://kagi.com/c/123", + "page_url": "https://site.example/", + "platform": "Kagi" + }, + { + "referer": "https://KAGI.COM/", + "page_url": "https://site.example/", + "platform": "Kagi" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=kagi.com", + "platform": "Kagi" + }, + { + "referer": "https://meta.ai/c/123", + "page_url": "https://site.example/", + "platform": "Meta AI" + }, + { + "referer": "https://META.AI/", + "page_url": "https://site.example/", + "platform": "Meta AI" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=meta.ai", + "platform": "Meta AI" + }, + { + "referer": "https://perplexity.ai/c/123", + "page_url": "https://site.example/", + "platform": "Perplexity" + }, + { + "referer": "https://PERPLEXITY.AI/", + "page_url": "https://site.example/", + "platform": "Perplexity" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=perplexity.ai", + "platform": "Perplexity" + }, + { + "referer": "https://phind.com/c/123", + "page_url": "https://site.example/", + "platform": "Phind" + }, + { + "referer": "https://PHIND.COM/", + "page_url": "https://site.example/", + "platform": "Phind" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=phind.com", + "platform": "Phind" + }, + { + "referer": "https://www.deepseek.com/c/123", + "page_url": "https://site.example/", + "platform": "DeepSeek" + }, + { + "referer": "https://WWW.DEEPSEEK.COM/", + "page_url": "https://site.example/", + "platform": "DeepSeek" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.deepseek.com", + "platform": "DeepSeek" + }, + { + "referer": "https://www.grok.com/c/123", + "page_url": "https://site.example/", + "platform": "Grok" + }, + { + "referer": "https://WWW.GROK.COM/", + "page_url": "https://site.example/", + "platform": "Grok" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.grok.com", + "platform": "Grok" + }, + { + "referer": "https://www.kagi.com/c/123", + "page_url": "https://site.example/", + "platform": "Kagi" + }, + { + "referer": "https://WWW.KAGI.COM/", + "page_url": "https://site.example/", + "platform": "Kagi" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.kagi.com", + "platform": "Kagi" + }, + { + "referer": "https://www.meta.ai/c/123", + "page_url": "https://site.example/", + "platform": "Meta AI" + }, + { + "referer": "https://WWW.META.AI/", + "page_url": "https://site.example/", + "platform": "Meta AI" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.meta.ai", + "platform": "Meta AI" + }, + { + "referer": "https://www.perplexity.ai/c/123", + "page_url": "https://site.example/", + "platform": "Perplexity" + }, + { + "referer": "https://WWW.PERPLEXITY.AI/", + "page_url": "https://site.example/", + "platform": "Perplexity" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.perplexity.ai", + "platform": "Perplexity" + }, + { + "referer": "https://www.phind.com/c/123", + "page_url": "https://site.example/", + "platform": "Phind" + }, + { + "referer": "https://WWW.PHIND.COM/", + "page_url": "https://site.example/", + "platform": "Phind" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.phind.com", + "platform": "Phind" + }, + { + "referer": "https://www.you.com/c/123", + "page_url": "https://site.example/", + "platform": "You.com" + }, + { + "referer": "https://WWW.YOU.COM/", + "page_url": "https://site.example/", + "platform": "You.com" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=www.you.com", + "platform": "You.com" + }, + { + "referer": "https://you.com/c/123", + "page_url": "https://site.example/", + "platform": "You.com" + }, + { + "referer": "https://YOU.COM/", + "page_url": "https://site.example/", + "platform": "You.com" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=you.com", + "platform": "You.com" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=ChatGPT", + "platform": "ChatGPT" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Claude", + "platform": "Claude" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Copilot", + "platform": "Copilot" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=DeepSeek", + "platform": "DeepSeek" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Gemini", + "platform": "Gemini" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Grok", + "platform": "Grok" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Kagi", + "platform": "Kagi" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Meta AI", + "platform": "Meta AI" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Perplexity", + "platform": "Perplexity" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=Phind", + "platform": "Phind" + }, + { + "referer": "", + "page_url": "https://site.example/p?ref=You.com", + "platform": "You.com" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_medium=chatgpt.com\u0026utm_source=newsletter", + "platform": "ChatGPT" + }, + { + "referer": "https://www.google.com/", + "page_url": "https://site.example/p?utm_source=chatgpt.com", + "platform": "" + }, + { + "referer": "https://site.example/other", + "page_url": "https://site.example/p", + "platform": "" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=ai", + "platform": "" + }, + { + "referer": "", + "page_url": "https://site.example/p?utm_source=referral", + "platform": "" + }, + { + "referer": "", + "page_url": "https://site.example/p", + "platform": "" + }, + { + "referer": "not a url", + "page_url": "https://site.example/p", + "platform": "" + }, + { + "referer": "https://chatgpt.com.evil.example/", + "page_url": "https://site.example/p", + "platform": "" + } +] diff --git a/tests/LlmReferralTest.php b/tests/LlmReferralTest.php new file mode 100644 index 0000000..5afd327 --- /dev/null +++ b/tests/LlmReferralTest.php @@ -0,0 +1,56 @@ +prefix . 'webdecoy_detections', $wpdb->prefix . 'webdecoy_rate_limits', $wpdb->prefix . 'webdecoy_checkout_attempts', + $wpdb->prefix . 'webdecoy_ai_referrals', ]; foreach ($tables as $table) { @@ -56,6 +57,7 @@ wp_clear_scheduled_hook('webdecoy_sync_blocked_ips'); wp_clear_scheduled_hook('webdecoy_sync_entitlements'); wp_clear_scheduled_hook('webdecoy_sync_actor_feed'); + wp_clear_scheduled_hook('webdecoy_flush_ai_referrals'); } // Delete all transients with webdecoy_ prefix @@ -83,6 +85,7 @@ $wpdb->prefix . 'webdecoy_detections', $wpdb->prefix . 'webdecoy_rate_limits', $wpdb->prefix . 'webdecoy_checkout_attempts', + $wpdb->prefix . 'webdecoy_ai_referrals', ]; foreach ($tables as $table) { @@ -106,6 +109,7 @@ wp_clear_scheduled_hook('webdecoy_sync_blocked_ips'); wp_clear_scheduled_hook('webdecoy_sync_entitlements'); wp_clear_scheduled_hook('webdecoy_sync_actor_feed'); + wp_clear_scheduled_hook('webdecoy_flush_ai_referrals'); // Delete transients for this site $wpdb->query( diff --git a/webdecoy.php b/webdecoy.php index be0ef4b..1882b89 100644 --- a/webdecoy.php +++ b/webdecoy.php @@ -70,6 +70,7 @@ function str_starts_with(string $haystack, string $needle): bool require_once $sdk_path . 'src/Detection.php'; require_once $sdk_path . 'src/DetectionResult.php'; require_once $sdk_path . 'src/AgentRegistry.php'; + require_once $sdk_path . 'src/LlmReferral.php'; require_once $sdk_path . 'src/GoodBotList.php'; require_once $sdk_path . 'src/RouteResolution.php'; require_once $sdk_path . 'src/SignalCollector.php'; @@ -974,6 +975,10 @@ private function init_hooks(): void // Safety-net cron drain of the violation-report spool. add_action('webdecoy_flush_violations', [$this, 'cron_flush_violations']); + // AI referral counting, when connected to WebDecoy Cloud: visits AI + // products send, as aggregate counts for the AI Traffic page. + WebDecoy_AI_Referrals::register((string) ($this->options['api_key'] ?? '')); + // Load text domain // Declare HPOS compatibility for WooCommerce @@ -1040,6 +1045,7 @@ public function load_includes(): void require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-pow.php'; require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-behavioral-scorer.php'; require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-violation-reporter.php'; + require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-ai-referrals.php'; require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-honeytoken.php'; require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-ip-enrichment.php'; require_once WEBDECOY_PLUGIN_DIR . 'includes/class-webdecoy-decoy-response.php';