array( 'org' => 'OpenAI', 'type' => 'crawler', ), 'ChatGPT-User' => array( 'org' => 'OpenAI', 'type' => 'search', ), 'OAI-SearchBot' => array( 'org' => 'OpenAI', 'type' => 'search', ), 'ClaudeBot' => array( 'org' => 'Anthropic', 'type' => 'crawler', ), 'Claude-User' => array( 'org' => 'Anthropic', 'type' => 'search', ), 'Claude-SearchBot' => array( 'org' => 'Anthropic', 'type' => 'search', ), 'anthropic-ai' => array( 'org' => 'Anthropic', 'type' => 'crawler', ), 'Google-Extended' => array( 'org' => 'Google', 'type' => 'crawler', ), 'Googlebot' => array( 'org' => 'Google', 'type' => 'crawler', ), 'PerplexityBot' => array( 'org' => 'Perplexity', 'type' => 'search', ), 'Bingbot' => array( 'org' => 'Microsoft', 'type' => 'crawler', ), 'FacebookExternalHit' => array( 'org' => 'Meta', 'type' => 'crawler', ), 'meta-externalagent' => array( 'org' => 'Meta', 'type' => 'crawler', ), 'Bytespider' => array( 'org' => 'ByteDance', 'type' => 'crawler', ), 'CCBot' => array( 'org' => 'Common Crawl', 'type' => 'crawler', ), 'cohere-ai' => array( 'org' => 'Cohere', 'type' => 'crawler', ), 'Applebot-Extended' => array( 'org' => 'Apple', 'type' => 'crawler', ), ); /** * AI referrer domain patterns. * Kept in sync with the AI-Advisors platform's known AI referrers. */ private const AI_REFERRERS = array( 'chat.openai.com' => 'chatgpt', 'chatgpt.com' => 'chatgpt', 'perplexity.ai' => 'perplexity', 'www.perplexity.ai' => 'perplexity', 'claude.ai' => 'claude', 'gemini.google.com' => 'gemini', 'copilot.microsoft.com' => 'copilot', 'www.bing.com' => 'copilot', 'meta.ai' => 'meta_ai', 'you.com' => 'you', 'phind.com' => 'phind', 'grok.x.com' => 'grok', ); public function __construct( $queue, $settings ) { $this->queue = $queue; $this->settings = $settings; } /** * Phase 1: Record request start time (runs very early). */ public function start_capture() { $GLOBALS['aiadvisors_request_start'] = microtime( true ); } /** * Phase 2: Capture request data if AI traffic detected (runs at shutdown). */ public function log_request() { try { if ( ! $this->settings->get( 'enabled' ) || ! $this->settings->has_domain_key() ) { return; } // Skip non-public requests. if ( Privacy::should_skip_request( $this->settings ) ) { return; } $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? sanitize_url( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; // Detect AI signals. $bot_info = $this->detect_bot( $user_agent ); $ai_referrer = $this->detect_referrer( $referrer ); $ai_utm = $this->detect_ai_utm(); // Only queue if AI traffic detected. if ( ! $bot_info && ! $ai_referrer && ! $ai_utm ) { return; } // Per-IP rate limit: a visitor sending a spoofed bot UA (or AI // referrer/UTM) on every request could otherwise flood the queue. if ( ! $this->rate_limit_ok() ) { return; } $start_time = $GLOBALS['aiadvisors_request_start'] ?? microtime( true ); $duration_ms = round( ( microtime( true ) - $start_time ) * 1000 ); $log_data = array( 'domain_key' => $this->settings->get_domain_key(), 'url' => Privacy::get_safe_path(), 'user_agent' => Privacy::sanitize_user_agent( $user_agent ), 'referrer' => Privacy::sanitize_referer( $referrer ), 'timestamp' => gmdate( 'c' ), 'ip' => Privacy::get_client_ip(), 'status_code' => $this->get_status_code(), 'bytes_sent' => $this->get_bytes_sent(), 'duration_ms' => $duration_ms > 0 ? $duration_ms : null, 'source' => 'wordpress_plugin', 'plugin_version' => AIADVISORS_VERSION, ); // Add UTM params if present. $utm_fields = $this->get_utm_params(); if ( ! empty( $utm_fields ) ) { $log_data = array_merge( $log_data, $utm_fields ); } // Add AI source info for referral visits. if ( $ai_referrer ) { $log_data['ai_source'] = $ai_referrer; $log_data['source_category'] = $ai_utm ? 'ai_ad' : 'organic_ai'; } // Redact sensitive query params. $query_params = Privacy::redact_query_params( $this->settings ); if ( ! empty( $query_params ) ) { $log_data['query_params'] = $query_params; } $json = wp_json_encode( $log_data ); if ( $json !== false ) { $this->queue->insert( $json ); } } catch ( \Throwable $e ) { // Never break the site - silently fail. } } /** * Per-IP throttle for queued events. Caps each client IP to a configurable * number of queued AI events per minute (default 30). * * @return bool True if under the limit (allowed to queue). */ private function rate_limit_ok() { $ip = Privacy::get_client_ip(); if ( $ip === '0.0.0.0' ) { return true; // Unidentifiable peer - don't block. } $limit = (int) apply_filters( 'aiadvisors_rate_limit_per_min', 30 ); if ( $limit <= 0 ) { return true; // Rate limiting disabled. } $key = 'aiadvisors_rl_' . md5( $ip ); $count = (int) get_transient( $key ); if ( $count >= $limit ) { return false; } set_transient( $key, $count + 1, MINUTE_IN_SECONDS ); return true; } /** * Detect AI bot from user-agent string. * * @return array|null Bot info array or null. */ private function detect_bot( $user_agent ) { if ( empty( $user_agent ) ) { return null; } foreach ( self::BOT_SIGNATURES as $pattern => $info ) { if ( stripos( $user_agent, $pattern ) !== false ) { return array_merge( array( 'name' => $pattern ), $info ); } } return null; } /** * Detect AI referral source from referrer URL. * * @return string|null AI source identifier or null. */ private function detect_referrer( $referrer ) { if ( empty( $referrer ) ) { return null; } // Skip self-referrals. if ( strpos( $referrer, home_url() ) !== false ) { return null; } $parsed = wp_parse_url( $referrer ); $host = strtolower( $parsed['host'] ?? '' ); return self::AI_REFERRERS[ $host ] ?? null; } /** * Detect AI advertising UTM parameters. * * @return bool True if AI ad traffic detected. */ private function detect_ai_utm() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $utm_medium = isset( $_GET['utm_medium'] ) ? sanitize_text_field( wp_unslash( $_GET['utm_medium'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $utm_source = isset( $_GET['utm_source'] ) ? sanitize_text_field( wp_unslash( $_GET['utm_source'] ) ) : ''; if ( $utm_medium === 'ai_ad' || $utm_medium === 'paid' ) { $ai_sources = array( 'chatgpt', 'openai', 'perplexity', 'claude', 'gemini', 'copilot' ); if ( in_array( strtolower( $utm_source ), $ai_sources, true ) ) { return true; } } return $utm_source === 'chatgpt_ads'; } /** * Extract UTM params from query string. */ private function get_utm_params() { $params = array(); $keys = array( 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content' ); foreach ( $keys as $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET[ $key ] ) && ! empty( $_GET[ $key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $params[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $key ] ) ); } } return $params; } /** * Get HTTP response status code. */ private function get_status_code() { $status = http_response_code(); if ( $status === false || $status < 100 || $status > 599 ) { $status = 200; } return $status; } /** * Get response body size in bytes. */ private function get_bytes_sent() { foreach ( headers_list() as $header ) { if ( stripos( $header, 'content-length:' ) === 0 ) { $parts = explode( ':', $header, 2 ); if ( count( $parts ) === 2 ) { $length = (int) trim( $parts[1] ); if ( $length > 0 ) { return $length; } } } } if ( ob_get_level() > 0 ) { $length = ob_get_length(); if ( $length !== false && $length > 0 ) { return $length; } } return null; } /** * Create a test event for connectivity verification. */ public function log_test_event() { $test_data = array( 'domain_key' => $this->settings->get_domain_key(), 'url' => '/aiadvisors-test-event', 'user_agent' => 'AI-Advisors-TestBot/1.0', 'referrer' => '', 'timestamp' => gmdate( 'c' ), 'ip' => Privacy::get_client_ip(), 'status_code' => 200, 'is_test' => true, 'source' => 'wordpress_plugin', 'plugin_version' => AIADVISORS_VERSION, ); $json = wp_json_encode( $test_data ); if ( $json === false ) { return false; } return $this->queue->insert( $json ); } }