Changeset 3652650
- Timestamp:
- 08/18/2026 11:15:54 AM (2 months ago)
- Location:
- llmaffiliate-ai-traffic-monetizer/trunk
- Files:
-
- 3 edited
-
index.php (modified) (1 diff)
-
llm-interceptor.php (modified) (115 diffs)
-
readme.txt (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
llmaffiliate-ai-traffic-monetizer/trunk/index.php
r3522098 r3652650 1 1 <?php 2 // Silence is golden. 2 if ( ! defined( 'ABSPATH' ) ) { 3 exit; 4 } -
llmaffiliate-ai-traffic-monetizer/trunk/llm-interceptor.php
r3624563 r3652650 3 3 * Plugin Name: LLMAffiliate AI Traffic Monetizer 4 4 * Description: Detects LLM user agents, enforces Google parity, and serves enhanced content from a remote service. 5 * Version: 1.0.1 05 * Version: 1.0.11 6 6 * Requires at least: 6.0 7 7 * Requires PHP: 7.4 … … 16 16 } 17 17 18 define( 'LLMAFFTM_VERSION', '1.0.10' ); 19 20 /** 21 * Copy options from legacy llm_interceptor_* keys to llmafftm_* (one-time). 22 */ 18 define( 'LLMAFFTM_VERSION', '1.0.11' ); 19 20 21 function llmafftm_get_client_ip() { 22 $ip_keys = array( 23 'HTTP_CF_CONNECTING_IP', 24 'HTTP_X_REAL_IP', 25 'HTTP_X_FORWARDED_FOR', 26 'HTTP_CLIENT_IP', 27 'REMOTE_ADDR', 28 ); 29 30 foreach ( $ip_keys as $key ) { 31 if ( ! empty( $_SERVER[ $key ] ) ) { 32 $ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); 33 if ( strpos( $ip, ',' ) !== false ) { 34 $ips = explode( ',', $ip ); 35 $ip = trim( $ips[0] ); 36 } 37 if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { 38 return $ip; 39 } 40 } 41 } 42 43 return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) ); 44 } 45 46 function llmafftm_get_current_url() { 47 $scheme = is_ssl() ? 'https' : 'http'; 48 $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) ); 49 $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ) ); 50 return $scheme . '://' . $host . $uri; 51 } 52 53 23 54 function llmafftm_migrate_legacy_options() { 24 55 if ( get_option( 'llmafftm_legacy_options_migrated', '' ) === '1' ) { … … 47 78 } 48 79 49 // Check minimum PHP version50 if (version_compare(PHP_VERSION, '7.4', '<')) {51 add_action('admin_notices', function() {52 echo '<div class="notice notice-error"><p><strong>LLMAffiliate AI Traffic Monetizer:</strong> Requires PHP 7.4 or higher. Current version: ' . esc_html( PHP_VERSION ) . '</p></div>';53 });54 return;55 }56 57 // Check if WordPress is loaded58 if (! function_exists('add_action')) {59 return;60 }61 62 /**63 * Advanced LLM Detection Class64 *65 * Calculates an LLM score based on multiple signals:66 * - HTTP Headers (User-Agent, Accept-Language, sec-ch-ua, etc.)67 * - Cookie presence (especially human_token from JavaScript verification)68 * - Request behavior patterns69 * - IP-based rate limiting70 */71 80 class LLMAFFTM_Detector { 72 73 private $threshold = 0.6; // Default threshold (0.0 to 1.0) 74 private $rate_limit_window = 60; // seconds 75 private $rate_limit_max_requests = 30; // max requests per window 76 77 /** 78 * Calculate LLM score for current request 79 * 80 * @return array ['score' => float, 'signals' => array, 'is_llm_like' => bool] 81 */ 81 82 private $threshold = 0.6; 83 private $rate_limit_window = 60; 84 private $rate_limit_max_requests = 30; 85 82 86 public function calculate_llm_score() { 83 87 $signals = array(); 84 88 $total_weight = 0; 85 89 $weighted_score = 0; 86 87 // Signal 1: User-Agent analysis (weight: 0.30) 90 88 91 $ua_score = $this->analyze_user_agent(); 89 92 $signals['user_agent'] = $ua_score; … … 91 94 $weighted_score += $ua_score * $weight; 92 95 $total_weight += $weight; 93 94 // Signal 2: Browser headers presence (weight: 0.25) 96 95 97 $headers_score = $this->analyze_browser_headers(); 96 98 $signals['browser_headers'] = $headers_score; … … 98 100 $weighted_score += $headers_score * $weight; 99 101 $total_weight += $weight; 100 101 // Signal 3: Cookie and session analysis (weight: 0.20) 102 102 103 $cookie_score = $this->analyze_cookies(); 103 104 $signals['cookies'] = $cookie_score; … … 105 106 $weighted_score += $cookie_score * $weight; 106 107 $total_weight += $weight; 107 108 // Signal 4: Request behavior patterns (weight: 0.15) 108 109 109 $behavior_score = $this->analyze_behavior(); 110 110 $signals['behavior'] = $behavior_score; … … 112 112 $weighted_score += $behavior_score * $weight; 113 113 $total_weight += $weight; 114 115 // Signal 5: Referer and navigation patterns (weight: 0.10) 114 116 115 $referer_score = $this->analyze_referer(); 117 116 $signals['referer'] = $referer_score; … … 119 118 $weighted_score += $referer_score * $weight; 120 119 $total_weight += $weight; 121 122 // Normalize score 120 123 121 $final_score = $total_weight > 0 ? ($weighted_score / $total_weight) : 0.5; 124 $final_score = max(0.0, min(1.0, $final_score)); // Clamp between 0 and 1122 $final_score = max(0.0, min(1.0, $final_score)); 125 123 126 124 $is_llm_like = $final_score >= $this->threshold; 127 125 $reasons = array(); 128 126 129 // Soft escalation (refined from CF_worker.js `datacenter_cold_fetch`):130 // a request originating from a known cloud / datacenter IP range131 // that arrives with NO cookies AND NO referer is suspicious — but132 // not enough on its own to justify a hard "force enhanced" override,133 // because legitimate humans behind corporate VPNs, NAT exits, or134 // cloud-hosted privacy proxies can also match this pattern.135 //136 // Instead we combine the network signal with the existing per-signal137 // scores:138 // * If at least one of the other detectors is already strongly139 // bot-leaning (programmatic UA, missing browser headers, or no140 // cookies at all), we DO mark the request as LLM-like and raise141 // the effective score to >= 0.9. This catches DeepSeek-style142 // fetchers reliably (programmatic UA + datacenter cold fetch).143 // * Otherwise we only nudge the score up to 0.65 — slightly above144 // the default 0.6 threshold. The configured threshold and the145 // other signals still get the final say, which lets site owners146 // tune sensitivity by adjusting the threshold rather than fight147 // a hardcoded override.148 127 $network = $this->classify_network_origin(); 149 128 $signals['network_datacenter'] = $network['is_datacenter'] ? 1.0 : 0.0; … … 155 134 $reasons[] = 'datacenter_cold_fetch'; 156 135 157 // curl/wget from a cloud VM is normal dev traffic — do not apply the cold-fetch158 // score bump used for opaque LLM fetchers (fixes false positives at threshold 0.5–0.6).159 136 $ua_cf = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ); 160 137 $is_cli_http_tool = (bool) preg_match( '/^\s*(?:curl|wget)\/\d/i', $ua_cf ); … … 183 160 ); 184 161 } 185 186 /** 187 * Analyze User-Agent string 188 * Returns: 0.0 (human-like) to 1.0 (bot-like) 189 */ 162 190 163 private function analyze_user_agent() { 191 164 $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ); 192 165 193 166 if (empty($user_agent)) { 194 return 0.9; // No UA is very suspicious 195 } 196 197 // Command-line HTTP clients: not LLM-specific; keep UA signal moderate so scoring 198 // can stay below typical thresholds while explicit UA patterns still win. 167 return 0.9; 168 } 169 199 170 if ( preg_match( '/^\s*(?:curl|wget)\/\d/i', $user_agent ) ) { 200 171 return 0.42; 201 172 } 202 173 203 // Known LLM/bot patterns (high score).204 //205 // Note on DeepSeek-style fetchers: DeepSeek and similar LLM agents206 // frequently use generic browser-like or generic programmatic UAs207 // (python, Go-http-client, httpx, aiohttp, etc.) without a208 // fixed, stable signature. We therefore do NOT hardcode a "DeepSeek"209 // user-agent. Instead, classification relies on the combination of:210 // - this UA pattern list (programmatic clients)211 // - HTTP header fingerprint (analyze_browser_headers)212 // - cookie state, behavior, referer (other analyze_* signals)213 // - and the datacenter-cold-fetch hard escalation in214 // calculate_llm_score().215 174 $bot_patterns = array( 216 175 '/GPTBot/i', '/OAI-SearchBot/i', '/ChatGPT/i', '/OpenAI/i', … … 220 179 '/Mistral/i', '/MistralBot/i', 221 180 '/LLMBot/i', '/AIBot/i', '/ChatBot/i', 222 // Programmatic HTTP clients commonly used by LLM-side fetchers 223 // (DeepSeek, research crawlers, language-model evaluation tools). 181 224 182 '/python(?:-requests|-urllib)?/i', 225 183 '/httpx/i', '/aiohttp/i', … … 228 186 '/\bruby\b/i', '/node-fetch/i', '/axios/i', 229 187 '/scrapy/i', '/phantomjs/i', '/headlesschrome/i', 230 // Generic fallbacks. 188 231 189 '/bot/i', '/crawler/i', '/spider/i', 232 190 ); 233 191 234 192 foreach ($bot_patterns as $pattern) { 235 193 if (preg_match($pattern, $user_agent)) { 236 return 0.95; // Strong bot signal 237 } 238 } 239 240 // Real browser patterns (low score) 194 return 0.95; 195 } 196 } 197 241 198 $browser_patterns = array( 242 199 '/Mozilla\/5\.0.*Chrome/i', … … 245 202 '/Mozilla\/5\.0.*Edge/i' 246 203 ); 247 204 248 205 $has_browser_pattern = false; 249 206 foreach ($browser_patterns as $pattern) { … … 253 210 } 254 211 } 255 256 // Check for browser version numbers (indicates real browser) 212 257 213 if ($has_browser_pattern && preg_match('/Version\/[\d.]+|Chrome\/[\d.]+|Firefox\/[\d.]+/', $user_agent)) { 258 return 0.1; // Very human-like 259 } 260 261 // Generic or suspicious UA 214 return 0.1; 215 } 216 262 217 if (strlen($user_agent) < 20) { 263 return 0.7; // Too short, suspicious 264 } 265 266 return 0.3; // Neutral 267 } 268 269 /** 270 * Analyze browser-specific headers 271 * Returns: 0.0 (human-like) to 1.0 (bot-like) 272 */ 218 return 0.7; 219 } 220 221 return 0.3; 222 } 223 273 224 private function analyze_browser_headers() { 274 225 $score = 0.0; 275 226 $signals = 0; 276 277 // sec-ch-ua headers (modern browsers send these) 227 278 228 $has_sec_ch_ua = ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA'] ?? '' ) ) ); 279 229 $has_sec_ch_ua_mobile = ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ?? '' ) ) ); 280 230 $has_sec_ch_ua_platform = ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '' ) ) ); 281 231 282 232 if ($has_sec_ch_ua && $has_sec_ch_ua_mobile && $has_sec_ch_ua_platform) { 283 $score += 0.1; // Strong human signal233 $score += 0.1; 284 234 $signals++; 285 235 } elseif (!$has_sec_ch_ua && !$has_sec_ch_ua_mobile && !$has_sec_ch_ua_platform) { 286 $score += 0.6; // Missing modern browser headers236 $score += 0.6; 287 237 $signals++; 288 238 } 289 290 // Accept-Language header 239 291 240 $accept_language = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '' ) ); 292 241 if (!empty($accept_language)) { 293 // Real browsers usually send language preferences 242 294 243 if (preg_match('/^[a-z]{2}(-[A-Z]{2})?(,\s*[a-z]{2}(-[A-Z]{2})?)*(;q=\d\.\d+)?/i', $accept_language)) { 295 $score += 0.1; // Proper language format244 $score += 0.1; 296 245 } else { 297 $score += 0.4; // Suspicious format246 $score += 0.4; 298 247 } 299 248 $signals++; 300 249 } else { 301 $score += 0.7; // Missing Accept-Language250 $score += 0.7; 302 251 $signals++; 303 252 } 304 305 // Accept header 253 306 254 $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ?? '' ) ); 307 255 if (!empty($accept) && preg_match('/text\/html|application\/xhtml\+xml/i', $accept)) { 308 $score += 0.1; // Proper Accept header256 $score += 0.1; 309 257 $signals++; 310 258 } else { 311 $score += 0.5; // Missing or suspicious Accept259 $score += 0.5; 312 260 $signals++; 313 261 } 314 315 // DNT (Do Not Track) - real browsers may send this 262 316 263 if ( ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_DNT'] ?? '' ) ) ) ) { 317 $score += 0.1; // Human signal264 $score += 0.1; 318 265 $signals++; 319 266 } 320 267 321 268 return $signals > 0 ? ($score / $signals) : 0.5; 322 269 } 323 324 /** 325 * Analyze cookies and session 326 * Returns: 0.0 (human-like) to 1.0 (bot-like) 327 */ 270 328 271 private function analyze_cookies() { 329 272 $cookies = $_COOKIE ?? array(); 330 331 // Check for human_token (set by JavaScript verification) 332 if (isset($cookies['llmafftm_human_token']) && !empty($cookies['llmafftm_human_token'])) { 333 // Verify token is valid (simple check - can be enhanced) 334 $token = sanitize_text_field($cookies['llmafftm_human_token']); 335 if (strlen($token) >= 16 && ctype_alnum($token)) { 336 return 0.0; // Strong human signal - has verified token 337 } 338 } 339 340 // No cookies at all is suspicious 273 341 274 if (empty($cookies)) { 342 return 0.8; // Very suspicious 343 } 344 345 // WordPress session cookies (wp-settings, etc.) 275 return 0.8; 276 } 277 346 278 $has_wp_cookies = false; 347 279 foreach (array_keys($cookies) as $name) { … … 351 283 } 352 284 } 353 285 354 286 if ($has_wp_cookies) { 355 return 0.2; // Has WordPress cookies, likely human 356 } 357 358 // Some cookies but not WordPress ones 359 return 0.5; // Neutral 360 } 361 362 /** 363 * Analyze request behavior patterns 364 * Returns: 0.0 (human-like) to 1.0 (bot-like) 365 */ 287 return 0.2; 288 } 289 290 return 0.5; 291 } 292 366 293 private function analyze_behavior() { 367 $client_ip = $this->get_client_ip(); 368 $current_url = $this->get_current_url(); 369 370 // Check rate limiting (basic implementation) 294 $client_ip = llmafftm_get_client_ip(); 295 $current_url = llmafftm_get_current_url(); 296 371 297 $rate_score = $this->check_rate_limit($client_ip); 372 373 // Check if accessing robots.txt (common bot behavior) 298 374 299 $is_robots = (strpos($current_url, '/robots.txt') !== false); 375 300 if ($is_robots) { 376 return 0.9; // Very bot-like 377 } 378 379 // Check request timing (if we have previous request data) 380 // This would require storing request timestamps (not implemented here for simplicity) 381 301 return 0.9; 302 } 303 382 304 return $rate_score; 383 305 } 384 385 /** 386 * Check rate limiting for IP 387 */ 306 388 307 private function check_rate_limit($ip) { 389 308 $transient_key = 'llmafftm_rate_' . md5($ip); 390 309 $requests = get_transient($transient_key); 391 310 392 311 if ($requests === false) { 393 // First request in window 312 394 313 set_transient($transient_key, 1, $this->rate_limit_window); 395 return 0.2; // Normal396 } 397 314 return 0.2; 315 } 316 398 317 $request_count = (int) $requests; 399 318 $request_count++; 400 319 401 320 if ($request_count > $this->rate_limit_max_requests) { 402 321 set_transient($transient_key, $request_count, $this->rate_limit_window); 403 return 0.95; // Rate limit exceeded - very bot-like404 } 405 322 return 0.95; 323 } 324 406 325 set_transient($transient_key, $request_count, $this->rate_limit_window); 407 408 // Calculate score based on request frequency 409 $frequency = $request_count / $this->rate_limit_window; // requests per second 326 327 $frequency = $request_count / $this->rate_limit_window; 410 328 if ($frequency > 0.5) { 411 return 0.8; // Too fast329 return 0.8; 412 330 } elseif ($frequency > 0.2) { 413 return 0.5; // Moderate 414 } 415 416 return 0.2; // Normal 417 } 418 419 /** 420 * Analyze Referer header 421 * Returns: 0.0 (human-like) to 1.0 (bot-like) 422 */ 331 return 0.5; 332 } 333 334 return 0.2; 335 } 336 423 337 private function analyze_referer() { 424 338 $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); 425 426 // No referer on direct access is normal 339 427 340 if (empty($referer)) { 428 return 0.3; // Neutral (could be direct access or bot) 429 } 430 431 // Referer from same domain is normal (navigation) 341 return 0.3; 342 } 343 432 344 $current_host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) ); 433 345 if (!empty($current_host) && strpos($referer, $current_host) !== false) { 434 return 0.1; // Internal navigation - human-like 435 } 436 437 // Referer from search engines is normal 346 return 0.1; 347 } 348 438 349 $search_engines = array('google.com', 'bing.com', 'yahoo.com', 'duckduckgo.com'); 439 350 foreach ($search_engines as $engine) { 440 351 if (strpos($referer, $engine) !== false) { 441 return 0.1; // From search - human-like 442 } 443 } 444 445 // Suspicious referer patterns 352 return 0.1; 353 } 354 } 355 446 356 if (preg_match('/bot|crawler|spider|scraper/i', $referer)) { 447 return 0.9; // Bot-like referer 448 } 449 450 return 0.4; // Neutral 451 } 452 453 /** 454 * Get client IP address 455 */ 456 private function get_client_ip() { 457 $ip_keys = array( 458 'HTTP_CF_CONNECTING_IP', // Cloudflare 459 'HTTP_X_REAL_IP', 460 'HTTP_X_FORWARDED_FOR', 461 'HTTP_CLIENT_IP', 462 'REMOTE_ADDR' 463 ); 464 465 foreach ($ip_keys as $key) { 466 if (!empty($_SERVER[$key])) { 467 $ip = sanitize_text_field( wp_unslash( $_SERVER[$key] ) ); 468 // Handle comma-separated IPs (X-Forwarded-For) 469 if (strpos($ip, ',') !== false) { 470 $ips = explode(',', $ip); 471 $ip = trim($ips[0]); 472 } 473 if (filter_var($ip, FILTER_VALIDATE_IP)) { 474 return $ip; 475 } 476 } 477 } 478 479 return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) ); 480 } 481 482 /** 483 * Get current URL 484 */ 485 private function get_current_url() { 486 $scheme = is_ssl() ? 'https' : 'http'; 487 $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) ); 488 $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ) ); 489 return $scheme . '://' . $host . $uri; 490 } 491 492 /** 493 * Default list of datacenter / cloud IP CIDR ranges used by LLM-side 494 * fetchers (DeepSeek, research crawlers, eval pipelines, proxies). 495 * 496 * This is intentionally a small, curated subset — not an exhaustive ASN 497 * dump — so the in-memory cost is negligible and the false-positive rate 498 * stays low. Real end users almost never browse product pages from these 499 * ranges; LLM fetchers very often do. 500 * 501 * The list is exposed through the 'llmafftm_datacenter_cidrs' filter so 502 * site owners can add or remove ranges without editing this file. Example: 503 * 504 * add_filter( 'llmafftm_datacenter_cidrs', function( $cidrs ) { 505 * $cidrs[] = '203.0.113.0/24'; 506 * return $cidrs; 507 * } ); 508 * 509 * @return string[] List of CIDR strings (IPv4 only — IPv6 ranges can be 510 * added through the filter if needed). 511 */ 357 return 0.9; 358 } 359 360 return 0.4; 361 } 362 512 363 private function get_datacenter_cidrs() { 513 364 $defaults = array( 514 // Huawei Cloud (incl. HK region — frequently observed in 515 // DeepSeek-side fetches). 365 516 366 '114.119.128.0/17', 517 367 '119.8.0.0/16', … … 519 369 '139.9.0.0/16', 520 370 '159.138.0.0/16', 521 // Alibaba Cloud / Aliyun (publicly published ranges). 371 522 372 '47.74.0.0/15', 523 373 '47.88.0.0/14', 524 374 '47.244.0.0/15', 525 375 '8.208.0.0/12', 526 // Tencent Cloud. 376 527 377 '49.51.0.0/16', 528 378 '170.106.0.0/16', 529 // AWS (a representative subset; AWS publishes the full list at 530 // https://ip-ranges.amazonaws.com/ip-ranges.json — operators who 531 // need full coverage can add ranges via the filter above). 379 532 380 '3.0.0.0/15', 533 381 '13.32.0.0/15', … … 535 383 '52.0.0.0/8', 536 384 '54.0.0.0/8', 537 // Google Cloud. 385 538 386 '34.64.0.0/10', 539 387 '35.184.0.0/13', … … 546 394 '35.224.0.0/12', 547 395 '35.240.0.0/13', 548 // Microsoft / Azure. 396 549 397 '13.64.0.0/11', 550 398 '20.0.0.0/8', … … 552 400 '52.224.0.0/11', 553 401 '104.40.0.0/13', 554 // DigitalOcean. 402 555 403 '64.225.0.0/16', 556 404 '67.207.64.0/19', … … 565 413 '167.99.0.0/16', 566 414 '178.62.0.0/16', 567 // Linode / Akamai. 415 568 416 '45.33.0.0/16', 569 417 '45.56.64.0/18', … … 575 423 '173.255.192.0/18', 576 424 '192.46.208.0/20', 577 // OVH. 425 578 426 '51.38.0.0/16', 579 427 '51.68.0.0/16', … … 591 439 '167.114.0.0/16', 592 440 '198.27.64.0/18', 593 // Hetzner. 441 594 442 '5.9.0.0/16', 595 443 '78.46.0.0/15', … … 609 457 '195.201.0.0/16', 610 458 '213.133.96.0/19', 611 // Vultr / Choopa. 459 612 460 '45.32.0.0/16', 613 461 '45.63.0.0/16', … … 625 473 '207.148.0.0/16', 626 474 '208.85.16.0/20', 627 // Scaleway. 475 628 476 '51.15.0.0/16', 629 477 '51.158.0.0/15', … … 632 480 '212.47.224.0/19', 633 481 '212.83.128.0/19', 634 // Contabo. 482 635 483 '109.123.224.0/19', 636 484 '161.97.128.0/17', 637 485 '173.212.192.0/18', 638 486 '207.180.192.0/18', 639 // IBM Cloud / SoftLayer. 487 640 488 '50.22.0.0/16', 641 489 '50.23.0.0/16', … … 650 498 ); 651 499 652 /**653 * Filters the list of datacenter / cloud IP CIDR ranges used to detect654 * LLM-side fetchers that arrive without a recognizable user-agent.655 *656 * @param string[] $cidrs Array of CIDR strings (IPv4 today; IPv6 may be added).657 */658 500 $filtered = apply_filters( 'llmafftm_datacenter_cidrs', $defaults ); 659 501 … … 661 503 } 662 504 663 /**664 * Check whether the given IPv4 address falls inside any of the configured665 * datacenter CIDR ranges.666 *667 * @param string $ip IPv4 address.668 * @return bool669 */670 505 private function ip_in_datacenter_range( $ip ) { 671 506 if ( ! is_string( $ip ) || $ip === '' ) { … … 674 509 675 510 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { 676 // IPv6 ranges are not in the default list; opt-in via filter. 511 677 512 return false; 678 513 } … … 703 538 } 704 539 705 /**706 * Returns true when the current request looks like a "cold" LLM-side707 * datacenter fetch — i.e. the client IP belongs to a known cloud /708 * datacenter range AND the request has no cookies AND no referer.709 *710 * Mirrors the worker's `datacenter_cold_fetch` hard-escalation rule.711 * Real human visitors browsing a product page from Huawei Cloud / AWS /712 * GCP / etc. with zero cookies and zero referer are exceedingly rare;713 * LLM fetchers (DeepSeek-style) match this pattern routinely.714 *715 * @return array{is_datacenter:bool,is_cold:bool,client_ip:string}716 */717 540 private function classify_network_origin() { 718 $client_ip = $this->get_client_ip();541 $client_ip = llmafftm_get_client_ip(); 719 542 $is_dc = $this->ip_in_datacenter_range( $client_ip ); 720 543 $cookie_hdr = sanitize_text_field( wp_unslash( $_SERVER['HTTP_COOKIE'] ?? '' ) ); … … 728 551 } 729 552 730 /**731 * Set detection threshold732 */733 553 public function set_threshold($threshold) { 734 554 $this->threshold = max(0.0, min(1.0, (float) $threshold)); 735 555 } 736 737 /** 738 * Log suspicious event for analysis 739 */ 740 public function log_suspicious_event($detection_result, $context = array()) { 741 $log_data = array( 742 'timestamp' => current_time('mysql'), 743 'ip' => $this->get_client_ip(), 744 'user_agent' => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), 745 'url' => $this->get_current_url(), 746 'score' => $detection_result['score'], 747 'is_llm_like' => $detection_result['is_llm_like'], 748 'signals' => $detection_result['signals'], 749 'context' => $context 750 ); 751 752 // Store in WordPress options (last 100 events) 753 $events = get_option('llmafftm_suspicious_events', array()); 754 $events[] = $log_data; 755 756 // Keep only last 100 events 757 if (count($events) > 100) { 758 $events = array_slice($events, -100); 759 } 760 761 update_option('llmafftm_suspicious_events', $events); 762 763 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 764 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging for suspicious events. 765 error_log('[LLMAFFTM] Suspicious event: ' . wp_json_encode($log_data)); 766 } 767 } 556 768 557 } 769 558 770 // Basic functional plugin771 559 class LLMAFFTM_Plugin { 772 560 773 561 private $option_name = 'llmafftm_token'; 774 562 private $option_site_id = 'llmafftm_site_id'; … … 776 564 private $option_request_logs = 'llmafftm_request_logs'; 777 565 private $option_enable_logging = 'llmafftm_enable_logging'; 778 /** 779 * Optional Google Search Console meta "content" value (saved from plugin settings). 780 * 781 * @var string 782 */ 566 783 567 private $option_gsc_verification = 'llmafftm_gsc_verification_token'; 784 568 private $detector; 785 569 786 570 public function __construct() { 787 571 $this->detector = new LLMAFFTM_Detector(); 788 789 // Load threshold from options 572 790 573 $threshold = get_option('llmafftm_threshold', 0.6); 791 574 $this->detector->set_threshold($threshold); 792 575 793 576 add_action( 'init', array( $this, 'maybe_serve_plugin_sitemap' ), 0 ); 577 add_action( 'init', array( $this, 'maybe_serve_plugin_robots' ), 0 ); 794 578 add_action( 'init', array( $this, 'maybe_serve_llms_txt' ), 0 ); 795 579 add_action( 'init', array( $this, 'maybe_serve_llms_full_txt' ), 0 ); … … 807 591 add_filter('robots_txt', array($this, 'filter_robots_txt'), 10, 2); 808 592 809 // Add JavaScript to set human_token cookie (footer via in_footer registration).810 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_human_verification_script' ) );811 812 // Inject Google Search Console site-verification meta tag on the813 // public frontend. The default token is filterable via814 // 'llmafftm_google_site_verification' so site owners can override815 // (or disable, by returning ''') without editing this file.816 593 add_action( 'wp_head', array( $this, 'output_google_site_verification_meta' ), 1 ); 817 594 } 818 595 819 596 public function init() { 820 // Generate site ID if not exists (but NOT token - user must generate it manually)821 597 if (!get_option($this->option_site_id)) { 822 598 $this->generate_site_id(); 823 599 } 824 600 825 // Reconcile existing/migrated state: token + site_id but onboarding not completed yet.826 601 $this->maybe_reconcile_onboarding(); 827 602 } 828 603 829 /**830 * Serve /sitemap.xml from the public LLMAffiliate API (host query only).831 *832 * @return void833 */834 604 public function maybe_serve_plugin_sitemap() { 835 if ( strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ) !== 'GET' ) { 836 return; 837 } 838 839 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { 605 if ( ! $this->is_public_get_request() ) { 840 606 return; 841 607 } 842 608 843 609 $norm = $this->get_normalized_request_path(); 844 if ( ! $this->is_ plugin_sitemap_path( $norm) ) {610 if ( ! $this->is_home_relative_path( $norm, '/sitemap.xml' ) ) { 845 611 return; 846 612 } … … 852 618 echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n"; 853 619 854 $host = wp_parse_url( home_url(), PHP_URL_HOST);620 list( $host, $items ) = $this->fetch_public_user_sitemaps( 15 ); 855 621 if ( ! is_string( $host ) || $host === '' ) { 856 622 echo '</urlset>'; … … 858 624 } 859 625 626 foreach ( $items as $row ) { 627 if ( ! is_array( $row ) || empty( $row['site_url'] ) ) { 628 continue; 629 } 630 631 $site_url = $row['site_url']; 632 if ( ! is_string( $site_url ) || esc_url( $site_url ) === '' ) { 633 continue; 634 } 635 636 $lastmod_raw = isset( $row['last_modified'] ) && is_string( $row['last_modified'] ) ? $row['last_modified'] : ''; 637 $lastmod = $this->format_sitemap_lastmod_w3c( $lastmod_raw ); 638 639 $pc = isset( $row['products_count'] ) ? (int) $row['products_count'] : 0; 640 $tt = isset( $row['total_traffic'] ) ? (int) $row['total_traffic'] : 0; 641 642 $priority = $this->compute_sitemap_priority( $pc, $tt ); 643 $changefreq = $this->compute_sitemap_changefreq( $pc ); 644 645 echo "\t<url>\n"; 646 echo "\t\t<loc>" . esc_url( $site_url ) . "</loc>\n"; 647 if ( $lastmod !== '' ) { 648 echo "\t\t<lastmod>" . esc_html( $lastmod ) . "</lastmod>\n"; 649 } 650 echo "\t\t<changefreq>" . esc_html( $changefreq ) . "</changefreq>\n"; 651 echo "\t\t<priority>" . esc_html( $priority ) . "</priority>\n"; 652 echo "\t</url>\n"; 653 } 654 655 echo '</urlset>'; 656 exit; 657 } 658 659 public function maybe_serve_plugin_robots() { 660 if ( ! $this->is_public_get_request() ) { 661 return; 662 } 663 664 $norm = $this->get_normalized_request_path(); 665 if ( ! $this->is_home_relative_path( $norm, '/robots.txt' ) ) { 666 return; 667 } 668 669 if ( (string) get_option( 'blog_public' ) === '0' ) { 670 return; 671 } 672 673 nocache_headers(); 674 header( 'Content-Type: text/plain; charset=UTF-8', true ); 675 header( 'X-LLMAFFTM-Interceptor: robots-txt', true ); 676 header( 'Cache-Control: public, max-age=3600', true ); 677 678 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain-text robots.txt from trusted internal data. 679 echo $this->build_robots_txt(); 680 exit; 681 } 682 683 private function fetch_public_user_sitemaps( $timeout ) { 684 $host = wp_parse_url( home_url(), PHP_URL_HOST ); 685 if ( ! is_string( $host ) || $host === '' ) { 686 return array( '', array() ); 687 } 688 860 689 $api_url = 'https://api.llmaffiliate.com/api/v1/enhanced-content/public/user-sitemaps?host=' . rawurlencode( $host ); 861 $response = wp_remote_get( $api_url, array( 'timeout' => 15) );690 $response = wp_remote_get( $api_url, array( 'timeout' => $timeout ) ); 862 691 863 692 $items = array(); … … 869 698 } 870 699 871 foreach ( $items as $row ) { 872 if ( ! is_array( $row ) || empty( $row['site_url'] ) ) { 873 continue; 874 } 875 876 $site_url = $row['site_url']; 877 if ( ! is_string( $site_url ) || esc_url( $site_url ) === '' ) { 878 continue; 879 } 880 881 $lastmod_raw = isset( $row['last_modified'] ) && is_string( $row['last_modified'] ) ? $row['last_modified'] : ''; 882 $lastmod = $this->format_sitemap_lastmod_w3c( $lastmod_raw ); 883 884 $pc = isset( $row['products_count'] ) ? (int) $row['products_count'] : 0; 885 $tt = isset( $row['total_traffic'] ) ? (int) $row['total_traffic'] : 0; 886 887 $priority = $this->compute_sitemap_priority( $pc, $tt ); 888 $changefreq = $this->compute_sitemap_changefreq( $pc ); 889 890 echo "\t<url>\n"; 891 echo "\t\t<loc>" . esc_url( $site_url ) . "</loc>\n"; 892 if ( $lastmod !== '' ) { 893 echo "\t\t<lastmod>" . esc_html( $lastmod ) . "</lastmod>\n"; 894 } 895 echo "\t\t<changefreq>" . esc_html( $changefreq ) . "</changefreq>\n"; 896 echo "\t\t<priority>" . esc_html( $priority ) . "</priority>\n"; 897 echo "\t</url>\n"; 898 } 899 900 echo '</urlset>'; 901 exit; 902 } 903 904 /** 905 * Normalize lastmod to W3C dateTime in UTC (Google): YYYY-MM-DDTHH:MM:SS.mmmZ. 906 * 907 * @param string $raw From API (ISO-like string; may lack timezone or use microsecond precision). 908 * @return string Empty string if unparseable. 909 */ 700 return array( $host, $items ); 701 } 702 910 703 private function format_sitemap_lastmod_w3c( $raw ) { 911 704 if ( ! is_string( $raw ) || trim( $raw ) === '' ) { … … 940 733 } 941 734 942 /**943 * Derive sitemap priority from product count and traffic volume.944 *945 * @param int $products_count Number of products on this page.946 * @param int $total_traffic Total LLM traffic hits.947 * @return string Priority value for the sitemap entry.948 */949 735 private function compute_sitemap_priority( $products_count, $total_traffic ) { 950 736 if ( $products_count >= 20 || $total_traffic >= 100 ) { … … 963 749 } 964 750 965 /**966 * Derive sitemap changefreq from product count.967 *968 * @param int $products_count Number of products on this page.969 * @return string Change frequency for the sitemap entry.970 */971 751 private function compute_sitemap_changefreq( $products_count ) { 972 752 if ( $products_count >= 5 ) { … … 979 759 } 980 760 981 /** 982 * @return string 983 */ 761 private function is_public_get_request() { 762 if ( strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ) !== 'GET' ) { 763 return false; 764 } 765 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { 766 return false; 767 } 768 return true; 769 } 770 984 771 private function get_normalized_request_path() { 985 $current_url = $this->get_current_url();772 $current_url = llmafftm_get_current_url(); 986 773 $parsed_path = wp_parse_url( $current_url, PHP_URL_PATH ); 987 774 $normalized_path = is_string( $parsed_path ) && $parsed_path !== '' … … 996 783 } 997 784 998 /** 999 * @param string $normalized_path From get_normalized_request_path(). 1000 * @return bool 1001 */ 1002 private function is_plugin_sitemap_path( $normalized_path ) { 1003 $expected = wp_parse_url( home_url( '/sitemap.xml' ), PHP_URL_PATH ); 785 private function is_home_relative_path( $normalized_path, $relative ) { 786 $expected = wp_parse_url( home_url( $relative ), PHP_URL_PATH ); 1004 787 if ( ! is_string( $expected ) || $expected === '' ) { 1005 788 return false; 1006 789 } 1007 1008 790 return untrailingslashit( $normalized_path ) === untrailingslashit( $expected ); 1009 791 } 1010 792 1011 /** 1012 * Check whether the request path matches /llms.txt. 1013 * 1014 * @param string $normalized_path From get_normalized_request_path(). 1015 * @return bool 1016 */ 1017 private function is_llms_txt_path( $normalized_path ) { 1018 $expected = wp_parse_url( home_url( '/llms.txt' ), PHP_URL_PATH ); 1019 if ( ! is_string( $expected ) || $expected === '' ) { 1020 return false; 1021 } 1022 return untrailingslashit( $normalized_path ) === untrailingslashit( $expected ); 1023 } 1024 1025 /** 1026 * Check whether the request path matches /llms-full.txt. 1027 * 1028 * @param string $normalized_path From get_normalized_request_path(). 1029 * @return bool 1030 */ 1031 private function is_llms_full_txt_path( $normalized_path ) { 1032 $expected = wp_parse_url( home_url( '/llms-full.txt' ), PHP_URL_PATH ); 1033 if ( ! is_string( $expected ) || $expected === '' ) { 1034 return false; 1035 } 1036 return untrailingslashit( $normalized_path ) === untrailingslashit( $expected ); 1037 } 1038 1039 /** 1040 * Extract an Amazon ASIN from a same-domain /buy/{asin} request path. 1041 * 1042 * Mirrors CF_worker.js matchBuyRedirectPath(): /buy/{asin} with optional trailing slash. 1043 * Uses home_url() so subdirectory WordPress installs resolve correctly. 1044 * 1045 * @param string $normalized_path From get_normalized_request_path(). 1046 * @return string Uppercase ASIN, or empty string when the path is not a buy link. 1047 */ 793 1048 794 private function match_buy_redirect_asin( $normalized_path ) { 1049 795 if ( ! is_string( $normalized_path ) || $normalized_path === '' ) { … … 1066 812 } 1067 813 1068 /**1069 * Resolve /buy/{asin} to the real merchant URL via the LLMAffiliate API.1070 *1071 * Purchase clicks bypass LLM detection (same as CF_worker.js serveBuyRedirect).1072 * Falls back to amazon.com/dp/{asin} when the API is unreachable.1073 *1074 * @return void1075 */1076 814 public function maybe_serve_buy_redirect() { 1077 if ( strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ) !== 'GET' ) { 1078 return; 1079 } 1080 1081 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { 815 if ( ! $this->is_public_get_request() ) { 1082 816 return; 1083 817 } … … 1130 864 } 1131 865 1132 /**1133 * @param string $location Absolute redirect target URL.1134 * @param string $interceptor_header Value for X-LLMAFFTM-Interceptor.1135 * @return void1136 */1137 866 private function redirect_buy_target( $location, $interceptor_header ) { 1138 867 nocache_headers(); … … 1143 872 } 1144 873 1145 /**1146 * @param string $asin Normalized ASIN.1147 * @param string $interceptor_header Value for X-LLMAFFTM-Interceptor.1148 * @return void1149 */1150 874 private function redirect_buy_fallback( $asin, $interceptor_header ) { 1151 875 $fallback = 'https://www.amazon.com/dp/' . rawurlencode( $asin ); … … 1153 877 } 1154 878 1155 /**1156 * Serve /llms.txt from the internal LLMAffiliate API.1157 *1158 * @return void1159 */1160 879 public function maybe_serve_llms_txt() { 1161 if ( strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ) !== 'GET' ) { 1162 return; 1163 } 1164 1165 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { 880 $this->maybe_serve_llms_document( 881 '/llms.txt', 882 'https://api.llmaffiliate.com/api/v1/enhanced-content/internal/llms-txt', 883 'llms-txt', 884 array( $this, 'serve_llms_txt_fallback' ) 885 ); 886 } 887 888 public function maybe_serve_llms_full_txt() { 889 $this->maybe_serve_llms_document( 890 '/llms-full.txt', 891 'https://api.llmaffiliate.com/api/v1/enhanced-content/internal/llms-full-txt', 892 'llms-full-txt', 893 array( $this, 'serve_llms_full_txt_fallback' ) 894 ); 895 } 896 897 private function maybe_serve_llms_document( $relative_path, $api_url_base, $interceptor, $fallback ) { 898 if ( ! $this->is_public_get_request() ) { 1166 899 return; 1167 900 } 1168 901 1169 902 $norm = $this->get_normalized_request_path(); 1170 if ( ! $this->is_ llms_txt_path( $norm) ) {903 if ( ! $this->is_home_relative_path( $norm, $relative_path ) ) { 1171 904 return; 1172 905 } … … 1174 907 $host = wp_parse_url( home_url(), PHP_URL_HOST ); 1175 908 if ( ! is_string( $host ) || $host === '' ) { 1176 $this->serve_llms_txt_fallback();1177 return; 1178 } 1179 1180 $api_url = 'https://api.llmaffiliate.com/api/v1/enhanced-content/internal/llms-txt?host=' . rawurlencode( $host );909 call_user_func( $fallback ); 910 return; 911 } 912 913 $api_url = $api_url_base . '?host=' . rawurlencode( $host ); 1181 914 $response = wp_remote_get( $api_url, array( 'timeout' => 15 ) ); 1182 915 … … 1187 920 1188 921 if ( $text === '' ) { 1189 $this->serve_llms_txt_fallback();922 call_user_func( $fallback ); 1190 923 return; 1191 924 } … … 1193 926 nocache_headers(); 1194 927 header( 'Content-Type: text/plain; charset=UTF-8', true ); 1195 header( 'X-LLMAFFTM-Interceptor: llms-txt', true );928 header( 'X-LLMAFFTM-Interceptor: ' . $interceptor, true ); 1196 929 header( 'Cache-Control: public, max-age=300', true ); 1197 930 … … 1201 934 } 1202 935 1203 /**1204 * Fallback llms.txt when the API is unreachable.1205 *1206 * @return void1207 */1208 936 private function serve_llms_txt_fallback() { 1209 937 $host = wp_parse_url( home_url(), PHP_URL_HOST ); … … 1223 951 } 1224 952 1225 /**1226 * Serve /llms-full.txt from the internal LLMAffiliate API.1227 *1228 * @return void1229 */1230 public function maybe_serve_llms_full_txt() {1231 if ( strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ) !== 'GET' ) {1232 return;1233 }1234 1235 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {1236 return;1237 }1238 1239 $norm = $this->get_normalized_request_path();1240 if ( ! $this->is_llms_full_txt_path( $norm ) ) {1241 return;1242 }1243 1244 $host = wp_parse_url( home_url(), PHP_URL_HOST );1245 if ( ! is_string( $host ) || $host === '' ) {1246 $this->serve_llms_full_txt_fallback();1247 return;1248 }1249 1250 $api_url = 'https://api.llmaffiliate.com/api/v1/enhanced-content/internal/llms-full-txt?host=' . rawurlencode( $host );1251 $response = wp_remote_get( $api_url, array( 'timeout' => 15 ) );1252 1253 $text = '';1254 if ( ! is_wp_error( $response ) && (int) wp_remote_retrieve_response_code( $response ) === 200 ) {1255 $text = wp_remote_retrieve_body( $response );1256 }1257 1258 if ( $text === '' ) {1259 $this->serve_llms_full_txt_fallback();1260 return;1261 }1262 1263 nocache_headers();1264 header( 'Content-Type: text/plain; charset=UTF-8', true );1265 header( 'X-LLMAFFTM-Interceptor: llms-full-txt', true );1266 header( 'Cache-Control: public, max-age=300', true );1267 1268 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain-text Markdown from trusted internal API.1269 echo $text;1270 exit;1271 }1272 1273 /**1274 * Fallback llms-full.txt when the API is unreachable.1275 *1276 * @return void1277 */1278 953 private function serve_llms_full_txt_fallback() { 1279 954 $host = wp_parse_url( home_url(), PHP_URL_HOST ); … … 1292 967 } 1293 968 1294 /**1295 * Sitemap and favicon: no LLM detection, no request logging.1296 *1297 * @param string $normalized_path From get_normalized_request_path().1298 * @return bool1299 */1300 969 private function should_bypass_llm_interception( $normalized_path ) { 1301 if ( $this->is_ plugin_sitemap_path( $normalized_path) ) {970 if ( $this->is_home_relative_path( $normalized_path, '/robots.txt' ) ) { 1302 971 return true; 1303 972 } 1304 if ( $this->is_llms_txt_path( $normalized_path ) || $this->is_llms_full_txt_path( $normalized_path ) ) { 973 if ( $this->is_home_relative_path( $normalized_path, '/sitemap.xml' ) ) { 974 return true; 975 } 976 if ( $this->is_home_relative_path( $normalized_path, '/llms.txt' ) || $this->is_home_relative_path( $normalized_path, '/llms-full.txt' ) ) { 1305 977 return true; 1306 978 } … … 1320 992 } 1321 993 1322 $admin_style_handle = 'llmafftm-admin';1323 wp_register_style( $admin_style_handle, false, array(), LLMAFFTM_VERSION );1324 wp_enqueue_style( $admin_style_handle );1325 wp_add_inline_style( $admin_style_handle, $this->get_admin_styles() );1326 1327 $admin_script_handle = 'llmafftm-admin';1328 wp_ register_script(1329 $admin_script_handle,1330 '',994 wp_enqueue_style( 995 'llmafftm-admin', 996 plugins_url( 'assets/admin.css', __FILE__ ), 997 array(), 998 LLMAFFTM_VERSION 999 ); 1000 wp_enqueue_script( 1001 'llmafftm-admin', 1002 plugins_url( 'assets/admin.js', __FILE__ ), 1331 1003 array( 'jquery' ), 1332 1004 LLMAFFTM_VERSION, 1333 1005 true 1334 1006 ); 1335 wp_enqueue_script( $admin_script_handle );1336 1007 wp_localize_script( 1337 $admin_script_handle,1008 'llmafftm-admin', 1338 1009 'llmafftmAdmin', 1339 1010 array( 1340 1011 'nonces' => array( 1341 'threshold' => wp_create_nonce( 'llmafftm_save_threshold' ), 1342 'logging' => wp_create_nonce( 'llmafftm_save_logging' ), 1343 'clearEvents' => wp_create_nonce( 'llmafftm_clear_events' ), 1344 'gscVerification' => wp_create_nonce( 'llmafftm_save_gsc_verification' ), 1012 'threshold' => wp_create_nonce( 'llmafftm_save_threshold' ), 1013 'logging' => wp_create_nonce( 'llmafftm_save_logging' ), 1014 'clearEvents' => wp_create_nonce( 'llmafftm_clear_events' ), 1015 'gscVerification' => wp_create_nonce( 'llmafftm_save_gsc_verification' ), 1016 ), 1017 'i18n' => array( 1018 'generating' => __( 'Generating...', 'llmaffiliate-ai-traffic-monetizer' ), 1019 'regenerating' => __( 'Regenerating...', 'llmaffiliate-ai-traffic-monetizer' ), 1020 'saving' => __( 'Saving...', 'llmaffiliate-ai-traffic-monetizer' ), 1021 'clearing' => __( 'Clearing...', 'llmaffiliate-ai-traffic-monetizer' ), 1022 'copied' => __( 'Copied!', 'llmaffiliate-ai-traffic-monetizer' ), 1023 'copy' => __( 'Copy', 'llmaffiliate-ai-traffic-monetizer' ), 1024 'errorPrefix' => __( 'Error:', 'llmaffiliate-ai-traffic-monetizer' ), 1025 'tokenGenerated' => __( 'Token generated successfully. The token has been sent to the onboarding endpoint.', 'llmaffiliate-ai-traffic-monetizer' ), 1026 'tokenGenerateFailed' => __( 'Failed to generate token', 'llmaffiliate-ai-traffic-monetizer' ), 1027 'tokenGenerateRetry' => __( 'Failed to generate token. Please try again.', 'llmaffiliate-ai-traffic-monetizer' ), 1028 'confirmRegenerate' => __( 'Are you sure you want to regenerate the token? The old token will no longer work and you will need to update it on llmaffiliate.com.', 'llmaffiliate-ai-traffic-monetizer' ), 1029 'tokenRegenerated' => __( 'Token regenerated successfully. Update the new token on llmaffiliate.com in your site integration settings.', 'llmaffiliate-ai-traffic-monetizer' ), 1030 'tokenRegenerateFailed' => __( 'Failed to regenerate token', 'llmaffiliate-ai-traffic-monetizer' ), 1031 'tokenRegenerateRetry' => __( 'Failed to regenerate token. Please try again.', 'llmaffiliate-ai-traffic-monetizer' ), 1032 'thresholdSaved' => __( 'Threshold saved successfully.', 'llmaffiliate-ai-traffic-monetizer' ), 1033 'thresholdSaveFailed' => __( 'Failed to save threshold', 'llmaffiliate-ai-traffic-monetizer' ), 1034 'thresholdSaveRetry' => __( 'Failed to save threshold. Please try again.', 'llmaffiliate-ai-traffic-monetizer' ), 1035 'loggingSaved' => __( 'Logging preference saved.', 'llmaffiliate-ai-traffic-monetizer' ), 1036 'saveFailed' => __( 'Failed to save', 'llmaffiliate-ai-traffic-monetizer' ), 1037 'loggingSaveFailed' => __( 'Failed to save logging preference.', 'llmaffiliate-ai-traffic-monetizer' ), 1038 'confirmClearLogs' => __( 'Are you sure you want to clear all request decision logs? This cannot be undone.', 'llmaffiliate-ai-traffic-monetizer' ), 1039 'logsCleared' => __( 'All events cleared successfully.', 'llmaffiliate-ai-traffic-monetizer' ), 1040 'clearFailed' => __( 'Failed to clear events', 'llmaffiliate-ai-traffic-monetizer' ), 1041 'clearRetry' => __( 'Failed to clear events. Please try again.', 'llmaffiliate-ai-traffic-monetizer' ), 1042 'saved' => __( 'Saved.', 'llmaffiliate-ai-traffic-monetizer' ), 1043 'saveError' => __( 'Error saving.', 'llmaffiliate-ai-traffic-monetizer' ), 1044 'gscInactive' => __( 'Status: Not configured — no verification meta tag is output.', 'llmaffiliate-ai-traffic-monetizer' ), 1045 'gscActive' => __( 'Status: Active — meta tag is output in the public HTML head.', 'llmaffiliate-ai-traffic-monetizer' ), 1046 'gscSaveFailed' => __( 'Error saving Search Console token.', 'llmaffiliate-ai-traffic-monetizer' ), 1047 'confirmClearGsc' => __( 'Remove Search Console verification from this site?', 'llmaffiliate-ai-traffic-monetizer' ), 1345 1048 ), 1346 1049 ) 1347 1050 ); 1348 wp_add_inline_script( $admin_script_handle, $this->get_admin_scripts(), 'after' ); 1349 wp_add_inline_script( $admin_script_handle, $this->get_admin_settings_inline_script(), 'after' ); 1350 } 1351 1051 } 1052 1352 1053 private function generate_token($force_onboarding = false) { 1353 1054 $token = 'llmafftm_' . bin2hex(random_bytes(32)); 1354 1055 update_option($this->option_name, $token); 1355 1356 // Reset onboarding success state when token is regenerated 1056 1357 1057 if ($force_onboarding) { 1358 1058 delete_option($this->option_onboarding_success); 1359 1059 } 1360 1361 // Send to onboarding endpoint (only if not already successful or forced) 1060 1362 1061 $this->send_to_onboarding($token, $this->get_site_id(), home_url(), $force_onboarding); 1363 1062 1364 1063 return $token; 1365 1064 } 1366 1065 1367 1066 private function generate_site_id() { 1368 1067 $site_id = 'site_' . bin2hex(random_bytes(16)); … … 1370 1069 return $site_id; 1371 1070 } 1372 1071 1373 1072 private function get_token() { 1374 1073 $token = get_option($this->option_name); 1375 1074 return $token ? $token : null; 1376 1075 } 1377 1076 1378 1077 private function get_site_id() { 1379 1078 $site_id = get_option($this->option_site_id); … … 1384 1083 } 1385 1084 1386 /**1387 * Self-heal onboarding when local token/site_id exist but backend onboarding was never completed.1388 */1389 1085 private function maybe_reconcile_onboarding() { 1390 1086 if ( get_option($this->option_onboarding_success) === 'yes' ) { … … 1402 1098 } 1403 1099 1404 // Reuse send_to_onboarding guard logic; do not force duplicate calls.1405 1100 $this->send_to_onboarding($token, $site_id, home_url(), false); 1406 1101 } 1407 1102 1408 /**1409 * Whether request and suspicious-event logging is enabled.1410 *1411 * @return bool1412 */1413 1103 private function is_logging_enabled() { 1414 1104 return (bool) get_option($this->option_enable_logging, true); … … 1444 1134 } 1445 1135 1446 private function get_client_ip() {1447 $ip_keys = array(1448 'HTTP_CF_CONNECTING_IP',1449 'HTTP_X_REAL_IP',1450 'HTTP_X_FORWARDED_FOR',1451 'HTTP_CLIENT_IP',1452 'REMOTE_ADDR'1453 );1454 1455 foreach ($ip_keys as $key) {1456 if (!empty($_SERVER[$key])) {1457 $ip = sanitize_text_field( wp_unslash( $_SERVER[$key] ) );1458 if (strpos($ip, ',') !== false) {1459 $ips = explode(',', $ip);1460 $ip = trim($ips[0]);1461 }1462 1463 if (filter_var($ip, FILTER_VALIDATE_IP)) {1464 return $ip;1465 }1466 }1467 }1468 1469 return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0' ) );1470 }1471 1472 1136 private function log_request_decision($data = array()) { 1473 $current_url = $this->get_current_url();1137 $current_url = llmafftm_get_current_url(); 1474 1138 $parsed_path = wp_parse_url($current_url, PHP_URL_PATH); 1475 $normalized_path = is_string($parsed_path) && $parsed_path !== '' 1476 ? untrailingslashit($parsed_path) 1477 : '/'; 1478 1479 if ($normalized_path === '') { 1480 $normalized_path = '/'; 1481 } 1139 $normalized_path = $this->get_normalized_request_path(); 1482 1140 1483 1141 $entry = array_merge( 1484 1142 array( 1485 1143 'timestamp' => current_time('mysql'), 1486 'ip' => $this->truncate_log_text( $this->get_client_ip(), 64),1144 'ip' => $this->truncate_log_text(llmafftm_get_client_ip(), 64), 1487 1145 'url' => $this->truncate_log_text($current_url, 500), 1488 1146 'method' => $this->truncate_log_text(strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ), 16), … … 1509 1167 'sec_ch_ua' => $this->truncate_log_text( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA'] ?? '' ) ), 300), 1510 1168 'sec_ch_ua_platform' => $this->truncate_log_text( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '' ) ), 120), 1511 'has_human_cookie' => isset($_COOKIE['llmafftm_human_token']) && !empty($_COOKIE['llmafftm_human_token']),1512 1169 'signals' => array(), 1513 1170 ), … … 1546 1203 ); 1547 1204 } 1548 1205 1549 1206 private function send_to_onboarding($token, $site_id, $host, $force = false) { 1550 // Check if already successfully sent (unless forced) 1207 1551 1208 if (!$force && get_option($this->option_onboarding_success) === 'yes') { 1552 1209 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { … … 1556 1213 return true; 1557 1214 } 1558 1215 1559 1216 $onboarding_url = 'https://api.llmaffiliate.com/api/v1/enhanced-content/onboarding/'; 1560 1217 1561 1218 try { 1562 1219 $response = wp_remote_post($onboarding_url, array( … … 1571 1228 )), 1572 1229 )); 1573 1230 1574 1231 if (is_wp_error($response)) { 1575 1232 $error_message = $response->get_error_message(); … … 1580 1237 return false; 1581 1238 } 1582 1239 1583 1240 $status_code = wp_remote_retrieve_response_code($response); 1584 1241 1585 1242 if ($status_code >= 200 && $status_code < 300) { 1586 // Mark as successful 1243 1587 1244 update_option($this->option_onboarding_success, 'yes'); 1588 1245 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { … … 1599 1256 return false; 1600 1257 } 1601 1258 1602 1259 } catch (Exception $e) { 1603 1260 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { … … 1614 1271 } 1615 1272 } 1616 1273 1617 1274 private function is_onboarding_successful() { 1618 1275 return get_option($this->option_onboarding_success) === 'yes'; 1619 1276 } 1620 1277 1621 1278 public function ajax_generate_token() { 1622 1279 check_ajax_referer('llmafftm_generate_token', 'nonce'); 1623 1280 1624 1281 if (!current_user_can('manage_options')) { 1625 wp_send_json_error(array('message' => 'Unauthorized'));1626 } 1627 1282 wp_send_json_error(array('message' => __( 'Unauthorized', 'llmaffiliate-ai-traffic-monetizer' ))); 1283 } 1284 1628 1285 try { 1629 // Check if token already exists 1286 1630 1287 if (get_option($this->option_name)) { 1631 wp_send_json_error(array('message' => 'Token already exists. Use regenerate instead.'));1288 wp_send_json_error(array('message' => __( 'Token already exists. Use regenerate instead.', 'llmaffiliate-ai-traffic-monetizer' ))); 1632 1289 return; 1633 1290 } 1634 1635 // Generate new token and force onboarding for first-time token creation. 1291 1636 1292 $new_token = $this->generate_token(true); 1637 1293 1638 1294 wp_send_json_success(array( 1639 1295 'token' => $new_token, 1640 'message' => 'Token generated and sent to onboarding endpoint'1296 'message' => __( 'Token generated and sent to onboarding endpoint', 'llmaffiliate-ai-traffic-monetizer' ) 1641 1297 )); 1642 1298 } catch (Exception $e) { … … 1645 1301 error_log('[LLMAFFTM] Generate token error: ' . $e->getMessage()); 1646 1302 } 1647 wp_send_json_error(array('message' => 'Failed to generate token: ' . $e->getMessage()));1303 wp_send_json_error(array('message' => sprintf( /* translators: %s: error message */ __( 'Failed to generate token: %s', 'llmaffiliate-ai-traffic-monetizer' ), $e->getMessage() ))); 1648 1304 } catch (Error $e) { 1649 1305 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { … … 1651 1307 error_log('[LLMAFFTM] Generate token fatal error: ' . $e->getMessage()); 1652 1308 } 1653 wp_send_json_error(array('message' => 'Fatal error generating token'));1654 } 1655 } 1656 1309 wp_send_json_error(array('message' => __( 'Fatal error generating token', 'llmaffiliate-ai-traffic-monetizer' ))); 1310 } 1311 } 1312 1657 1313 public function ajax_regenerate_token() { 1658 1314 check_ajax_referer('llmafftm_regenerate_token', 'nonce'); 1659 1315 1660 1316 if (!current_user_can('manage_options')) { 1661 wp_send_json_error(array('message' => 'Unauthorized'));1662 } 1663 1317 wp_send_json_error(array('message' => __( 'Unauthorized', 'llmaffiliate-ai-traffic-monetizer' ))); 1318 } 1319 1664 1320 try { 1665 // Force onboarding when regenerating token 1321 1666 1322 $new_token = $this->generate_token(true); 1667 1323 1668 1324 wp_send_json_success(array( 1669 1325 'token' => $new_token, 1670 'message' => 'Token regenerated and sent to onboarding endpoint'1326 'message' => __( 'Token regenerated and sent to onboarding endpoint', 'llmaffiliate-ai-traffic-monetizer' ) 1671 1327 )); 1672 1328 } catch (Exception $e) { … … 1675 1331 error_log('[LLMAFFTM] Regenerate token error: ' . $e->getMessage()); 1676 1332 } 1677 wp_send_json_error(array('message' => 'Failed to regenerate token: ' . $e->getMessage()));1333 wp_send_json_error(array('message' => sprintf( /* translators: %s: error message */ __( 'Failed to regenerate token: %s', 'llmaffiliate-ai-traffic-monetizer' ), $e->getMessage() ))); 1678 1334 } catch (Error $e) { 1679 1335 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { … … 1681 1337 error_log('[LLMAFFTM] Regenerate token fatal error: ' . $e->getMessage()); 1682 1338 } 1683 wp_send_json_error(array('message' => 'Fatal error regenerating token'));1684 } 1685 } 1686 1339 wp_send_json_error(array('message' => __( 'Fatal error regenerating token', 'llmaffiliate-ai-traffic-monetizer' ))); 1340 } 1341 } 1342 1687 1343 public function ajax_save_threshold() { 1688 1344 check_ajax_referer('llmafftm_save_threshold', 'nonce'); 1689 1345 1690 1346 if (!current_user_can('manage_options')) { 1691 wp_send_json_error(array('message' => 'Unauthorized'));1692 } 1693 1694 $threshold = isset($_POST['threshold']) ? floatval( $_POST['threshold']) : 0.6;1695 $threshold = max(0.0, min(1.0, $threshold)); // Clamp between 0 and 11696 1347 wp_send_json_error(array('message' => __( 'Unauthorized', 'llmaffiliate-ai-traffic-monetizer' ))); 1348 } 1349 1350 $threshold = isset($_POST['threshold']) ? floatval( wp_unslash( $_POST['threshold'] ) ) : 0.6; 1351 $threshold = max(0.0, min(1.0, $threshold)); 1352 1697 1353 update_option('llmafftm_threshold', $threshold); 1698 1699 // Update detector instance 1354 1700 1355 $this->detector->set_threshold($threshold); 1701 1356 1702 1357 wp_send_json_success(array( 1703 1358 'threshold' => $threshold, 1704 'message' => 'Threshold saved successfully'1359 'message' => __( 'Threshold saved successfully', 'llmaffiliate-ai-traffic-monetizer' ) 1705 1360 )); 1706 1361 } 1707 1362 1708 1363 public function ajax_save_logging() { 1709 1364 check_ajax_referer('llmafftm_save_logging', 'nonce'); 1710 1365 if ( ! current_user_can('manage_options') ) { 1711 wp_send_json_error(array('message' => 'Unauthorized'));1712 } 1713 $enabled = isset($_POST['enable_logging']) && $_POST['enable_logging']=== '1';1366 wp_send_json_error(array('message' => __( 'Unauthorized', 'llmaffiliate-ai-traffic-monetizer' ))); 1367 } 1368 $enabled = isset($_POST['enable_logging']) && sanitize_text_field( wp_unslash( $_POST['enable_logging'] ) ) === '1'; 1714 1369 update_option($this->option_enable_logging, $enabled ? '1' : '0'); 1715 1370 wp_send_json_success(array( 1716 'message' => $enabled ? 'Logging enabled.' : 'Logging disabled.',1371 'message' => $enabled ? __( 'Logging enabled.', 'llmaffiliate-ai-traffic-monetizer' ) : __( 'Logging disabled.', 'llmaffiliate-ai-traffic-monetizer' ), 1717 1372 'enable_logging' => $enabled 1718 1373 )); 1719 1374 } 1720 1375 1721 /**1722 * Save or clear the Google Search Console verification token from plugin settings.1723 *1724 * @return void1725 */1726 1376 public function ajax_save_gsc_verification() { 1727 1377 check_ajax_referer( 'llmafftm_save_gsc_verification', 'nonce' ); 1728 1378 1729 1379 if ( ! current_user_can( 'manage_options' ) ) { 1730 wp_send_json_error( array( 'message' => 'Unauthorized') );1380 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'llmaffiliate-ai-traffic-monetizer' ) ) ); 1731 1381 } 1732 1382 … … 1763 1413 public function ajax_clear_events() { 1764 1414 check_ajax_referer('llmafftm_clear_events', 'nonce'); 1765 1415 1766 1416 if (!current_user_can('manage_options')) { 1767 wp_send_json_error(array('message' => 'Unauthorized'));1768 } 1769 1417 wp_send_json_error(array('message' => __( 'Unauthorized', 'llmaffiliate-ai-traffic-monetizer' ))); 1418 } 1419 1770 1420 delete_option('llmafftm_suspicious_events'); 1771 1421 delete_option($this->option_request_logs); 1772 1422 1773 1423 wp_send_json_success(array( 1774 'message' => 'All logs cleared successfully'1424 'message' => __( 'All logs cleared successfully', 'llmaffiliate-ai-traffic-monetizer' ) 1775 1425 )); 1776 1426 } 1777 1427 1778 1428 public function add_admin_menu() { 1779 1429 add_options_page( 1780 'LLMAffiliate AI Traffic Monetizer',1781 'LLMAffiliate AI Traffic',1430 __( 'LLMAffiliate AI Traffic Monetizer', 'llmaffiliate-ai-traffic-monetizer' ), 1431 __( 'LLMAffiliate AI Traffic', 'llmaffiliate-ai-traffic-monetizer' ), 1782 1432 'manage_options', 1783 1433 'llmaffiliate-ai-traffic-monetizer', … … 1785 1435 ); 1786 1436 } 1787 1437 1788 1438 public function admin_page() { 1789 1439 $token = $this->get_token(); … … 1794 1444 $onboarding_success = $this->is_onboarding_successful(); 1795 1445 $has_token = !empty($token); 1796 1797 // Load threshold into detector 1446 1798 1447 $threshold = get_option('llmafftm_threshold', 0.6); 1799 1448 $this->detector->set_threshold($threshold); … … 1803 1452 <h1 class="llmafftm-title"> 1804 1453 <span class="llmafftm-icon">🤖</span> 1805 LLMAffiliate AI Traffic Monetizer1454 <?php esc_html_e( 'LLMAffiliate AI Traffic Monetizer', 'llmaffiliate-ai-traffic-monetizer' ); ?> 1806 1455 </h1> 1807 <p class="llmafftm-subtitle"> Enhanced Content Delivery for AI Assistants</p>1456 <p class="llmafftm-subtitle"><?php esc_html_e( 'Enhanced Content Delivery for AI Assistants', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1808 1457 </div> 1809 1458 1810 1459 <div class="llmafftm-status-card"> 1811 1460 <div class="llmafftm-status-indicator"> 1812 1461 <span class="llmafftm-status-dot active"></span> 1813 <span class="llmafftm-status-text"> Plugin Active</span>1462 <span class="llmafftm-status-text"><?php esc_html_e( 'Plugin Active', 'llmaffiliate-ai-traffic-monetizer' ); ?></span> 1814 1463 </div> 1815 1464 <div class="llmafftm-service-info"> 1816 <strong> Content Service:</strong>1465 <strong><?php esc_html_e( 'Content Service:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong> 1817 1466 <code>https://api.llmaffiliate.com</code> 1818 1467 </div> 1819 1468 <div class="llmafftm-onboarding-status"> 1820 <strong> Onboarding:</strong>1469 <strong><?php esc_html_e( 'Onboarding:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong> 1821 1470 <?php if ($onboarding_success): ?> 1822 <span style="color: green;">✅ Registered</span>1471 <span style="color: green;">✅ <?php esc_html_e( 'Registered', 'llmaffiliate-ai-traffic-monetizer' ); ?></span> 1823 1472 <?php else: ?> 1824 <span style="color: orange;">⏳ Pending</span>1473 <span style="color: orange;">⏳ <?php esc_html_e( 'Pending', 'llmaffiliate-ai-traffic-monetizer' ); ?></span> 1825 1474 <?php endif; ?> 1826 1475 </div> … … 1880 1529 </div> 1881 1530 </div> 1882 1531 1883 1532 <div class="llmafftm-token-section"> 1884 1533 <div class="llmafftm-card"> 1885 1534 <div class="llmafftm-card-header"> 1886 <h2>🔑 API Token</h2>1887 <p class="llmafftm-card-description"> Your unique authentication token for the LLM Affiliate API</p>1535 <h2>🔑 <?php esc_html_e( 'API Token', 'llmaffiliate-ai-traffic-monetizer' ); ?></h2> 1536 <p class="llmafftm-card-description"><?php esc_html_e( 'Your unique authentication token for the LLM Affiliate API', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1888 1537 </div> 1889 1538 1890 1539 <?php if ($has_token): ?> 1891 1540 <div class="llmafftm-token-display"> 1892 1541 <div class="llmafftm-token-wrapper"> 1893 <input 1894 type="text" 1895 id="llmafftm-token-input" 1896 class="llmafftm-token-input" 1897 value="<?php echo esc_attr($token); ?>" 1542 <input 1543 type="text" 1544 id="llmafftm-token-input" 1545 class="llmafftm-token-input" 1546 value="<?php echo esc_attr($token); ?>" 1898 1547 readonly 1899 1548 > 1900 <button 1901 type="button" 1902 class="llmafftm-copy-btn" 1549 <button 1550 type="button" 1551 class="llmafftm-copy-btn" 1903 1552 data-copy-target="llmafftm-token-input" 1904 title=" Copy to clipboard"1553 title="<?php echo esc_attr__( 'Copy to clipboard', 'llmaffiliate-ai-traffic-monetizer' ); ?>" 1905 1554 > 1906 1555 <span class="llmafftm-copy-icon">📋</span> 1907 <span class="llmafftm-copy-text"> Copy</span>1556 <span class="llmafftm-copy-text"><?php esc_html_e( 'Copy', 'llmaffiliate-ai-traffic-monetizer' ); ?></span> 1908 1557 </button> 1909 1558 </div> 1910 1559 <div class="llmafftm-token-actions"> 1911 <button 1912 type="button" 1560 <button 1561 type="button" 1913 1562 class="llmafftm-regenerate-btn button button-secondary" 1914 1563 data-nonce="<?php echo esc_attr($regenerate_nonce); ?>" 1915 1564 > 1916 🔄 Regenerate Token1565 🔄 <?php esc_html_e( 'Regenerate Token', 'llmaffiliate-ai-traffic-monetizer' ); ?> 1917 1566 </button> 1918 1567 </div> 1919 1568 </div> 1920 1569 1921 1570 <div class="llmafftm-token-info"> 1922 <p><strong>⚠️ Important:</strong> Keep this token secure. Use it to authenticate API requests.</p>1571 <p><strong>⚠️ <?php esc_html_e( 'Important:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong> <?php esc_html_e( 'Keep this token secure. Use it to authenticate API requests.', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1923 1572 </div> 1924 1573 <?php else: ?> 1925 1574 <div class="llmafftm-no-token"> 1926 1575 <div class="llmafftm-no-token-icon">🔑</div> 1927 <h3> No Token Generated Yet</h3>1928 <p> You need to generate a token to connect your WordPress site to LLM Affiliate.</p>1929 <button 1930 type="button" 1576 <h3><?php esc_html_e( 'No Token Generated Yet', 'llmaffiliate-ai-traffic-monetizer' ); ?></h3> 1577 <p><?php esc_html_e( 'You need to generate a token to connect your WordPress site to LLM Affiliate.', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1578 <button 1579 type="button" 1931 1580 class="llmafftm-generate-btn button button-primary button-large" 1932 1581 data-nonce="<?php echo esc_attr($generate_nonce); ?>" 1933 1582 > 1934 ✨ Generate Token1583 ✨ <?php esc_html_e( 'Generate Token', 'llmaffiliate-ai-traffic-monetizer' ); ?> 1935 1584 </button> 1936 <p class="llmafftm-generate-note"> Click the button above to generate your unique API token. It will be automatically sent to the onboarding endpoint.</p>1585 <p class="llmafftm-generate-note"><?php esc_html_e( 'Click the button above to generate your unique API token. It will be automatically sent to the onboarding endpoint.', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1937 1586 </div> 1938 1587 <?php endif; ?> 1939 1588 </div> 1940 1589 </div> 1941 1590 1942 1591 <div class="llmafftm-site-info-section"> 1943 1592 <div class="llmafftm-card"> 1944 1593 <div class="llmafftm-card-header"> 1945 <h2>🌐 Site Information</h2>1594 <h2>🌐 <?php esc_html_e( 'Site Information', 'llmaffiliate-ai-traffic-monetizer' ); ?></h2> 1946 1595 </div> 1947 1596 <div class="llmafftm-info-grid"> 1948 1597 <div class="llmafftm-info-item"> 1949 <label> Site ID:</label>1598 <label><?php esc_html_e( 'Site ID:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 1950 1599 <code class="llmafftm-code-block"><?php echo esc_html($site_id); ?></code> 1951 1600 </div> 1952 1601 <div class="llmafftm-info-item"> 1953 <label> Site URL:</label>1602 <label><?php esc_html_e( 'Site URL:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 1954 1603 <code class="llmafftm-code-block"><?php echo esc_html($site_url); ?></code> 1955 1604 </div> … … 1957 1606 </div> 1958 1607 </div> 1959 1608 1960 1609 <div class="llmafftm-setup-section"> 1961 1610 <div class="llmafftm-card"> 1962 1611 <div class="llmafftm-card-header"> 1963 <h2>🚀 Setup Instructions</h2>1964 <p class="llmafftm-card-description"> Follow these steps to connect your WordPress site to LLM Affiliate</p>1612 <h2>🚀 <?php esc_html_e( 'Setup Instructions', 'llmaffiliate-ai-traffic-monetizer' ); ?></h2> 1613 <p class="llmafftm-card-description"><?php esc_html_e( 'Follow these steps to connect your WordPress site to LLM Affiliate', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1965 1614 </div> 1966 1615 1967 1616 <div class="llmafftm-steps"> 1968 1617 <div class="llmafftm-step"> 1969 1618 <div class="llmafftm-step-number">1</div> 1970 1619 <div class="llmafftm-step-content"> 1971 <h3>Create Account on LLM Affiliate</h3> 1972 <p>Visit <a href="<?php echo esc_url( 'https://llmaffiliate.com' ); ?>" target="_blank" rel="noopener noreferrer" class="llmafftm-link">llmaffiliate.com</a> and create your account if you don't have one yet.</p> 1973 <p class="llmafftm-step-note">You'll need an account to integrate your WordPress site.</p> 1620 <h3><?php esc_html_e( 'Create Account on LLM Affiliate', 'llmaffiliate-ai-traffic-monetizer' ); ?></h3> 1621 <p><?php 1622 echo wp_kses_post( 1623 sprintf( 1624 /* translators: %s: LLMAffiliate homepage URL */ 1625 __( 'Visit <a href="%s" target="_blank" rel="noopener noreferrer">llmaffiliate.com</a> and create your account if you do not have one yet.', 'llmaffiliate-ai-traffic-monetizer' ), 1626 esc_url( 'https://llmaffiliate.com' ) 1627 ) 1628 ); 1629 ?></p> 1630 <p class="llmafftm-step-note"><?php esc_html_e( 'You will need an account to integrate your WordPress site.', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1974 1631 </div> 1975 1632 </div> 1976 1633 1977 1634 <div class="llmafftm-step"> 1978 1635 <div class="llmafftm-step-number">2</div> 1979 1636 <div class="llmafftm-step-content"> 1980 <h3> Add Your Site Integration</h3>1981 <p> After logging in, go to your dashboard and click on <strong>"Add New Integration"</strong> or <strong>"Integrate Site"</strong>.</p>1637 <h3><?php esc_html_e( 'Add Your Site Integration', 'llmaffiliate-ai-traffic-monetizer' ); ?></h3> 1638 <p><?php echo wp_kses_post( __( 'After logging in, go to your dashboard and click on <strong>"Add New Integration"</strong> or <strong>"Integrate Site"</strong>.', 'llmaffiliate-ai-traffic-monetizer' ) ); ?></p> 1982 1639 </div> 1983 1640 </div> 1984 1641 1985 1642 <div class="llmafftm-step"> 1986 1643 <div class="llmafftm-step-number">3</div> 1987 1644 <div class="llmafftm-step-content"> 1988 <h3> Enter Your Credentials</h3>1989 <p> When prompted, enter the following information:</p>1645 <h3><?php esc_html_e( 'Enter Your Credentials', 'llmaffiliate-ai-traffic-monetizer' ); ?></h3> 1646 <p><?php esc_html_e( 'When prompted, enter the following information:', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 1990 1647 <?php if ($has_token): ?> 1991 1648 <div class="llmafftm-credentials-box"> 1992 1649 <div class="llmafftm-credential-item"> 1993 <label>🔑 API Token:</label>1650 <label>🔑 <?php esc_html_e( 'API Token:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 1994 1651 <code class="llmafftm-credential-value"><?php echo esc_html($token); ?></code> 1995 <button 1996 type="button" 1997 class="llmafftm-copy-small-btn" 1652 <button 1653 type="button" 1654 class="llmafftm-copy-small-btn" 1998 1655 data-copy-text="<?php echo esc_attr($token); ?>" 1999 title=" Copy token"1656 title="<?php echo esc_attr__( 'Copy token', 'llmaffiliate-ai-traffic-monetizer' ); ?>" 2000 1657 > 2001 1658 📋 … … 2003 1660 </div> 2004 1661 <div class="llmafftm-credential-item"> 2005 <label>🌐 Site ID:</label>1662 <label>🌐 <?php esc_html_e( 'Site ID:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 2006 1663 <code class="llmafftm-credential-value"><?php echo esc_html($site_id); ?></code> 2007 <button 2008 type="button" 2009 class="llmafftm-copy-small-btn" 1664 <button 1665 type="button" 1666 class="llmafftm-copy-small-btn" 2010 1667 data-copy-text="<?php echo esc_attr($site_id); ?>" 2011 title=" Copy Site ID"1668 title="<?php echo esc_attr__( 'Copy Site ID', 'llmaffiliate-ai-traffic-monetizer' ); ?>" 2012 1669 > 2013 1670 📋 … … 2015 1672 </div> 2016 1673 <div class="llmafftm-credential-item"> 2017 <label>🔗 Site URL:</label>1674 <label>🔗 <?php esc_html_e( 'Site URL:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 2018 1675 <code class="llmafftm-credential-value"><?php echo esc_html($site_url); ?></code> 2019 <button 2020 type="button" 2021 class="llmafftm-copy-small-btn" 1676 <button 1677 type="button" 1678 class="llmafftm-copy-small-btn" 2022 1679 data-copy-text="<?php echo esc_attr($site_url); ?>" 2023 title=" Copy URL"1680 title="<?php echo esc_attr__( 'Copy URL', 'llmaffiliate-ai-traffic-monetizer' ); ?>" 2024 1681 > 2025 1682 📋 … … 2030 1687 <div class="llmafftm-credentials-box"> 2031 1688 <div class="llmafftm-credential-item"> 2032 <label>🔑 API Token:</label>2033 <code class="llmafftm-credential-value" style="color: #999; font-style: italic;"> Generate token first</code>1689 <label>🔑 <?php esc_html_e( 'API Token:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 1690 <code class="llmafftm-credential-value" style="color: #999; font-style: italic;"><?php esc_html_e( 'Generate token first', 'llmaffiliate-ai-traffic-monetizer' ); ?></code> 2034 1691 </div> 2035 1692 <div class="llmafftm-credential-item"> 2036 <label>🌐 Site ID:</label>1693 <label>🌐 <?php esc_html_e( 'Site ID:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 2037 1694 <code class="llmafftm-credential-value"><?php echo esc_html($site_id); ?></code> 2038 <button 2039 type="button" 2040 class="llmafftm-copy-small-btn" 1695 <button 1696 type="button" 1697 class="llmafftm-copy-small-btn" 2041 1698 data-copy-text="<?php echo esc_attr($site_id); ?>" 2042 title=" Copy Site ID"1699 title="<?php echo esc_attr__( 'Copy Site ID', 'llmaffiliate-ai-traffic-monetizer' ); ?>" 2043 1700 > 2044 1701 📋 … … 2046 1703 </div> 2047 1704 <div class="llmafftm-credential-item"> 2048 <label>🔗 Site URL:</label>1705 <label>🔗 <?php esc_html_e( 'Site URL:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label> 2049 1706 <code class="llmafftm-credential-value"><?php echo esc_html($site_url); ?></code> 2050 <button 2051 type="button" 2052 class="llmafftm-copy-small-btn" 1707 <button 1708 type="button" 1709 class="llmafftm-copy-small-btn" 2053 1710 data-copy-text="<?php echo esc_attr($site_url); ?>" 2054 title=" Copy URL"1711 title="<?php echo esc_attr__( 'Copy URL', 'llmaffiliate-ai-traffic-monetizer' ); ?>" 2055 1712 > 2056 1713 📋 … … 2061 1718 </div> 2062 1719 </div> 2063 1720 2064 1721 <div class="llmafftm-step"> 2065 1722 <div class="llmafftm-step-number">4</div> 2066 1723 <div class="llmafftm-step-content"> 2067 <h3> Complete Integration</h3>2068 <p> Click <strong>"Save"</strong> or <strong>"Integrate"</strong> to complete the setup. Your WordPress site will now be connected to LLM Affiliate!</p>2069 <p class="llmafftm-step-note"> ✅ Once integrated, the plugin will automatically serve enhanced content to LLM visitors.</p>1724 <h3><?php esc_html_e( 'Complete Integration', 'llmaffiliate-ai-traffic-monetizer' ); ?></h3> 1725 <p><?php echo wp_kses_post( __( 'Click <strong>"Save"</strong> or <strong>"Integrate"</strong> to complete the setup. Your WordPress site will now be connected to LLM Affiliate.', 'llmaffiliate-ai-traffic-monetizer' ) ); ?></p> 1726 <p class="llmafftm-step-note"><?php esc_html_e( 'Once integrated, the plugin will automatically serve enhanced content to LLM visitors.', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 2070 1727 </div> 2071 1728 </div> 2072 1729 </div> 2073 1730 2074 1731 <div class="llmafftm-help-box"> 2075 <h4>💡 Need Help?</h4> 2076 <p>If you encounter any issues during setup, check our <a href="<?php echo esc_url( 'https://llmaffiliate.com/docs' ); ?>" target="_blank" rel="noopener noreferrer" class="llmafftm-link">documentation</a> or contact support.</p> 1732 <h4>💡 <?php esc_html_e( 'Need Help?', 'llmaffiliate-ai-traffic-monetizer' ); ?></h4> 1733 <p><?php 1734 echo wp_kses_post( 1735 sprintf( 1736 /* translators: %s: documentation URL */ 1737 __( 'If you encounter any issues during setup, check our <a href="%s" target="_blank" rel="noopener noreferrer">documentation</a> or contact support.', 'llmaffiliate-ai-traffic-monetizer' ), 1738 esc_url( 'https://llmaffiliate.com/docs' ) 1739 ) 1740 ); 1741 ?></p> 2077 1742 </div> 2078 1743 </div> 2079 1744 </div> 2080 1745 2081 1746 <div class="llmafftm-detection-section"> 2082 1747 <div class="llmafftm-card"> 2083 1748 <div class="llmafftm-card-header"> 2084 <h2>🔍 Advanced Detection Settings</h2>2085 <p class="llmafftm-card-description"> Configure the LLM detection threshold and view suspicious events</p>1749 <h2>🔍 <?php esc_html_e( 'Advanced Detection Settings', 'llmaffiliate-ai-traffic-monetizer' ); ?></h2> 1750 <p class="llmafftm-card-description"><?php esc_html_e( 'Configure the LLM detection threshold and view request decision logs', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 2086 1751 </div> 2087 1752 2088 1753 <?php 2089 1754 $current_threshold = get_option('llmafftm_threshold', 0.6); 2090 1755 $enable_logging = (bool) get_option('llmafftm_enable_logging', true); 2091 $suspicious_events = get_option('llmafftm_suspicious_events', array());2092 $events_count = count($suspicious_events);2093 1756 $request_logs = get_option($this->option_request_logs, array()); 2094 1757 $request_logs_count = count($request_logs); 1758 delete_option( 'llmafftm_suspicious_events' ); 2095 1759 ?> 2096 1760 2097 1761 <div class="llmafftm-logging-setting" style="margin-bottom: 24px; padding: 12px; background: #f9f9f9; border-radius: 6px;"> 2098 1762 <label> 2099 1763 <input type="checkbox" id="llmafftm-enable-logging" <?php checked( $enable_logging, true ); ?>> 2100 <strong> Enable request and suspicious-event logging</strong>1764 <strong><?php esc_html_e( 'Enable request decision logging', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong> 2101 1765 </label> 2102 1766 <p class="description" style="margin: 8px 0 0 24px; color: #646970;"> 2103 When enabled, the plugin stores request decisions and suspicious-event logs in the database for debugging and analysis. Disable to reduce stored data.1767 <?php esc_html_e( 'When enabled, the plugin stores request decision logs in the database for debugging and analysis. Disable to reduce stored data.', 'llmaffiliate-ai-traffic-monetizer' ); ?> 2104 1768 </p> 2105 <button type="button" class="button button-secondary" id="llmafftm-save-logging" style="margin-top: 8px;"> Save Logging Preference</button>1769 <button type="button" class="button button-secondary" id="llmafftm-save-logging" style="margin-top: 8px;"><?php esc_html_e( 'Save Logging Preference', 'llmaffiliate-ai-traffic-monetizer' ); ?></button> 2106 1770 </div> 2107 1771 2108 1772 <div class="llmafftm-threshold-setting"> 2109 1773 <label for="llmafftm-threshold"> 2110 <strong> Detection Threshold:</strong>1774 <strong><?php esc_html_e( 'Detection Threshold:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong> 2111 1775 <span id="llmafftm-threshold-value"><?php echo esc_html($current_threshold); ?></span> 2112 1776 </label> 2113 1777 <p class="llmafftm-threshold-description"> 2114 Requests with a score above this threshold (0.0-1.0) will be treated as LLM-like. 2115 Lower values = more sensitive (catches more bots). Higher values = more strict. 1778 <?php esc_html_e( 'Requests with a score above this threshold (0.0-1.0) will be treated as LLM-like. Lower values = more sensitive (catches more bots). Higher values = more strict.', 'llmaffiliate-ai-traffic-monetizer' ); ?> 2116 1779 </p> 2117 <input 2118 type="range" 2119 id="llmafftm-threshold" 2120 min="0" 2121 max="1" 2122 step="0.05" 1780 <input 1781 type="range" 1782 id="llmafftm-threshold" 1783 min="0" 1784 max="1" 1785 step="0.05" 2123 1786 value="<?php echo esc_attr($current_threshold); ?>" 2124 1787 style="width: 100%; margin: 10px 0;" 2125 1788 > 2126 <button 2127 type="button" 2128 class="button button-primary" 1789 <button 1790 type="button" 1791 class="button button-primary" 2129 1792 id="llmafftm-save-threshold" 2130 1793 > 2131 Save Threshold1794 <?php esc_html_e( 'Save Threshold', 'llmaffiliate-ai-traffic-monetizer' ); ?> 2132 1795 </button> 2133 1796 </div> 2134 1797 2135 1798 <div class="llmafftm-events-section"> 2136 <h3> Request Decision Logs (Last 100)</h3>2137 <p> Total request decisions logged: <strong><?php echo esc_html($request_logs_count); ?></strong></p>2138 1799 <h3><?php esc_html_e( 'Request Decision Logs (Last 100)', 'llmaffiliate-ai-traffic-monetizer' ); ?></h3> 1800 <p><?php esc_html_e( 'Total request decisions logged:', 'llmaffiliate-ai-traffic-monetizer' ); ?> <strong><?php echo esc_html( (string) $request_logs_count ); ?></strong></p> 1801 2139 1802 <?php if ($request_logs_count > 0): ?> 2140 1803 <div class="llmafftm-events-table-wrapper"> … … 2142 1805 <thead> 2143 1806 <tr> 2144 <th> Time</th>2145 <th> Decision</th>2146 <th> Reason</th>2147 <th> Host</th>2148 <th> Method</th>2149 <th> Path</th>2150 <th> Profile</th>2151 <th> Score</th>2152 <th> Pattern</th>2153 <th> User-Agent</th>2154 <th> URL</th>1807 <th><?php esc_html_e( 'Time', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1808 <th><?php esc_html_e( 'Decision', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1809 <th><?php esc_html_e( 'Reason', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1810 <th><?php esc_html_e( 'Host', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1811 <th><?php esc_html_e( 'Method', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1812 <th><?php esc_html_e( 'Path', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1813 <th><?php esc_html_e( 'Profile', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1814 <th><?php esc_html_e( 'Score', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1815 <th><?php esc_html_e( 'Pattern', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1816 <th><?php esc_html_e( 'User-Agent', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 1817 <th><?php esc_html_e( 'URL', 'llmaffiliate-ai-traffic-monetizer' ); ?></th> 2155 1818 </tr> 2156 1819 </thead> 2157 1820 <tbody> 2158 <?php 1821 <?php 2159 1822 $recent_request_logs = array_slice(array_reverse($request_logs), 0, 100); 2160 foreach ($recent_request_logs as $event): 1823 foreach ($recent_request_logs as $event): 2161 1824 ?> 2162 1825 <tr> … … 2211 1874 </table> 2212 1875 </div> 2213 <button 2214 type="button" 2215 class="button button-secondary" 1876 <button 1877 type="button" 1878 class="button button-secondary" 2216 1879 id="llmafftm-clear-events" 2217 1880 > 2218 Clear All Logs1881 <?php esc_html_e( 'Clear All Logs', 'llmaffiliate-ai-traffic-monetizer' ); ?> 2219 1882 </button> 2220 1883 <?php else: ?> 2221 <p class="llmafftm-events-empty-message">No request decision logs yet.</p> 2222 <?php endif; ?> 2223 </div> 2224 2225 <div class="llmafftm-events-section"> 2226 <h3>Suspicious Events (Last 100)</h3> 2227 <p>Total suspicious events logged: <strong><?php echo esc_html($events_count); ?></strong></p> 2228 2229 <?php if ($events_count > 0): ?> 2230 <div class="llmafftm-events-table-wrapper"> 2231 <table class="llmafftm-events-table"> 2232 <thead> 2233 <tr> 2234 <th>Time</th> 2235 <th>IP</th> 2236 <th>Score</th> 2237 <th>LLM-Like</th> 2238 <th>User-Agent</th> 2239 <th>URL</th> 2240 </tr> 2241 </thead> 2242 <tbody> 2243 <?php 2244 $recent_events = array_slice(array_reverse($suspicious_events), 0, 100); 2245 foreach ($recent_events as $event): 2246 ?> 2247 <tr> 2248 <td><?php echo esc_html($event['timestamp']); ?></td> 2249 <td><code><?php echo esc_html($event['ip']); ?></code></td> 2250 <td> 2251 <strong style="color: <?php echo esc_attr( $event['score'] >= 0.7 ? '#dc3545' : ( $event['score'] >= 0.4 ? '#ffc107' : '#28a745' ) ); ?>"> 2252 <?php echo esc_html($event['score']); ?> 2253 </strong> 2254 </td> 2255 <td> 2256 <?php if ($event['is_llm_like']): ?> 2257 <span style="color: #dc3545;">✓ Yes</span> 2258 <?php else: ?> 2259 <span style="color: #6c757d;">✗ No</span> 2260 <?php endif; ?> 2261 </td> 2262 <td class="llmafftm-cell-wrap llmafftm-cell-ua"> 2263 <code style="font-size: 11px;" title="<?php echo esc_attr( $event['user_agent'] ); ?>"> 2264 <?php echo esc_html( (string) $event['user_agent'] ); ?> 2265 </code> 2266 </td> 2267 <td class="llmafftm-cell-wrap llmafftm-cell-url"> 2268 <code style="font-size: 11px;" title="<?php echo esc_attr( $event['url'] ); ?>"> 2269 <?php echo esc_html( (string) $event['url'] ); ?> 2270 </code> 2271 </td> 2272 </tr> 2273 <?php endforeach; ?> 2274 </tbody> 2275 </table> 2276 </div> 2277 <?php else: ?> 2278 <p class="llmafftm-events-empty-message">No suspicious events logged yet.</p> 1884 <p class="llmafftm-events-empty-message"><?php esc_html_e( 'No request decision logs yet.', 'llmaffiliate-ai-traffic-monetizer' ); ?></p> 2279 1885 <?php endif; ?> 2280 1886 </div> 2281 1887 </div> 2282 1888 </div> 2283 1889 2284 1890 <div class="llmafftm-footer"> 2285 <p class="llmafftm-version"> Version <?php echo esc_html( LLMAFFTM_VERSION ); ?> | <a href="<?php echo esc_url( 'https://llmaffiliate.com/docs' ); ?>" target="_blank" rel="noopener noreferrer">Documentation</a></p>1891 <p class="llmafftm-version"><?php esc_html_e( 'Version', 'llmaffiliate-ai-traffic-monetizer' ); ?> <?php echo esc_html( LLMAFFTM_VERSION ); ?> | <a href="<?php echo esc_url( 'https://llmaffiliate.com/docs' ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Documentation', 'llmaffiliate-ai-traffic-monetizer' ); ?></a></p> 2286 1892 </div> 2287 1893 </div> … … 2289 1895 } 2290 1896 2291 /**2292 * Admin settings UI behaviors (threshold, logging, clear events). Loaded via wp_add_inline_script().2293 *2294 * @return string2295 */2296 private function get_admin_settings_inline_script() {2297 ob_start();2298 ?>2299 jQuery(document).ready(function($) {2300 var thresholdSlider = $('#llmafftm-threshold');2301 var thresholdValue = $('#llmafftm-threshold-value');2302 var saveThresholdBtn = $('#llmafftm-save-threshold');2303 var saveLoggingBtn = $('#llmafftm-save-logging');2304 var clearEventsBtn = $('#llmafftm-clear-events');2305 2306 thresholdSlider.on('input', function() {2307 thresholdValue.text($(this).val());2308 });2309 2310 saveThresholdBtn.on('click', function() {2311 var $btn = $(this);2312 var originalHtml = $btn.html();2313 $btn.prop('disabled', true).html('⏳ Saving...');2314 2315 $.ajax({2316 url: ajaxurl,2317 type: 'POST',2318 data: {2319 action: 'llmafftm_save_threshold',2320 nonce: llmafftmAdmin.nonces.threshold,2321 threshold: thresholdSlider.val()2322 },2323 success: function(response) {2324 if (response.success) {2325 alert('✅ Threshold saved successfully!');2326 } else {2327 alert('❌ Error: ' + (response.data.message || 'Failed to save threshold'));2328 }2329 $btn.prop('disabled', false).html(originalHtml);2330 },2331 error: function() {2332 alert('❌ Error: Failed to save threshold. Please try again.');2333 $btn.prop('disabled', false).html(originalHtml);2334 }2335 });2336 });2337 2338 saveLoggingBtn.on('click', function() {2339 var $btn = $(this);2340 var originalHtml = $btn.html();2341 var enabled = $('#llmafftm-enable-logging').is(':checked') ? '1' : '0';2342 $btn.prop('disabled', true).html('⏳ Saving...');2343 $.ajax({2344 url: ajaxurl,2345 type: 'POST',2346 data: {2347 action: 'llmafftm_save_logging',2348 nonce: llmafftmAdmin.nonces.logging,2349 enable_logging: enabled2350 },2351 success: function(response) {2352 if (response.success) {2353 alert('✅ ' + (response.data.message || 'Logging preference saved.'));2354 } else {2355 alert('❌ Error: ' + (response.data.message || 'Failed to save'));2356 }2357 $btn.prop('disabled', false).html(originalHtml);2358 },2359 error: function() {2360 alert('❌ Error: Failed to save logging preference.');2361 $btn.prop('disabled', false).html(originalHtml);2362 }2363 });2364 });2365 2366 clearEventsBtn.on('click', function() {2367 if (!confirm('Are you sure you want to clear all suspicious events? This cannot be undone.')) {2368 return;2369 }2370 2371 var $btn = $(this);2372 var originalHtml = $btn.html();2373 $btn.prop('disabled', true).html('⏳ Clearing...');2374 2375 $.ajax({2376 url: ajaxurl,2377 type: 'POST',2378 data: {2379 action: 'llmafftm_clear_events',2380 nonce: llmafftmAdmin.nonces.clearEvents2381 },2382 success: function(response) {2383 if (response.success) {2384 alert('✅ All events cleared successfully!');2385 location.reload();2386 } else {2387 alert('❌ Error: ' + (response.data.message || 'Failed to clear events'));2388 $btn.prop('disabled', false).html(originalHtml);2389 }2390 },2391 error: function() {2392 alert('❌ Error: Failed to clear events. Please try again.');2393 $btn.prop('disabled', false).html(originalHtml);2394 }2395 });2396 });2397 2398 $('#llmafftm-save-gsc-verification').on('click', function() {2399 var $btn = $(this);2400 var $status = $('#llmafftm-gsc-status');2401 var val = $('#llmafftm-gsc-token-input').val();2402 $btn.prop('disabled', true);2403 $.ajax({2404 url: ajaxurl,2405 type: 'POST',2406 data: {2407 action: 'llmafftm_save_gsc_verification',2408 nonce: llmafftmAdmin.nonces.gscVerification,2409 gsc_token: val2410 },2411 success: function(response) {2412 if (response.success) {2413 alert(response.data.message || 'Saved.');2414 if (!val || val.trim() === '') {2415 $status.text('Status: Not configured — no verification meta tag is output.');2416 } else {2417 $status.text('Status: Active — meta tag is output in the public HTML head.');2418 }2419 } else {2420 alert((response.data && response.data.message) ? response.data.message : 'Error saving.');2421 }2422 $btn.prop('disabled', false);2423 },2424 error: function() {2425 alert('Error saving Search Console token.');2426 $btn.prop('disabled', false);2427 }2428 });2429 });2430 2431 $('#llmafftm-clear-gsc-verification').on('click', function() {2432 if (!confirm('Remove Search Console verification from this site?')) {2433 return;2434 }2435 $('#llmafftm-gsc-token-input').val('');2436 $('#llmafftm-save-gsc-verification').trigger('click');2437 });2438 });2439 <?php2440 return trim(ob_get_clean());2441 }2442 2443 private function get_admin_styles() {2444 return '2445 .llmafftm-admin-wrap {2446 max-width: 1200px;2447 margin: 20px auto;2448 }2449 .llmafftm-header {2450 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);2451 color: white;2452 padding: 30px;2453 border-radius: 8px;2454 margin-bottom: 30px;2455 box-shadow: 0 4px 6px rgba(0,0,0,0.1);2456 }2457 .llmafftm-title {2458 margin: 0 0 10px 0;2459 font-size: 32px;2460 font-weight: 600;2461 display: flex;2462 align-items: center;2463 gap: 15px;2464 }2465 .llmafftm-icon {2466 font-size: 40px;2467 }2468 .llmafftm-subtitle {2469 margin: 0;2470 font-size: 16px;2471 opacity: 0.9;2472 }2473 .llmafftm-status-card {2474 background: #f8f9fa;2475 border: 1px solid #dee2e6;2476 border-radius: 8px;2477 padding: 20px;2478 margin-bottom: 30px;2479 display: flex;2480 justify-content: space-between;2481 align-items: center;2482 flex-wrap: wrap;2483 gap: 20px;2484 }2485 .llmafftm-status-indicator {2486 display: flex;2487 align-items: center;2488 gap: 10px;2489 }2490 .llmafftm-status-dot {2491 width: 12px;2492 height: 12px;2493 border-radius: 50%;2494 background: #dc3545;2495 }2496 .llmafftm-status-dot.active {2497 background: #28a745;2498 animation: pulse 2s infinite;2499 }2500 @keyframes pulse {2501 0%, 100% { opacity: 1; }2502 50% { opacity: 0.5; }2503 }2504 .llmafftm-status-text {2505 font-weight: 600;2506 color: #495057;2507 }2508 .llmafftm-service-info, .llmafftm-onboarding-status {2509 color: #6c757d;2510 }2511 .llmafftm-gsc-prominent {2512 margin-bottom: 30px;2513 border: 3px solid #2271b1;2514 border-radius: 8px;2515 background: linear-gradient(180deg, #f0f6fc 0%, #ffffff 55%);2516 box-shadow: 0 4px 14px rgba(34, 113, 177, 0.12);2517 }2518 .llmafftm-gsc-prominent-inner {2519 padding: 28px 32px;2520 }2521 .llmafftm-gsc-title {2522 margin: 0 0 12px 0;2523 font-size: 1.35rem;2524 line-height: 1.35;2525 color: #1d2327;2526 max-width: 960px;2527 }2528 .llmafftm-gsc-lead {2529 margin: 0 0 20px 0;2530 font-size: 16px;2531 line-height: 1.55;2532 color: #2c3338;2533 max-width: 720px;2534 }2535 .llmafftm-gsc-lead a {2536 font-weight: 600;2537 }2538 .llmafftm-gsc-token-input {2539 width: 100%;2540 max-width: 680px;2541 font-size: 16px;2542 padding: 12px 14px;2543 margin-bottom: 12px;2544 border-radius: 4px;2545 border: 1px solid #8c8f94;2546 }2547 .llmafftm-gsc-status {2548 margin: 0 0 16px 0;2549 font-weight: 600;2550 color: #1d2327;2551 }2552 .llmafftm-gsc-buttons {2553 display: flex;2554 flex-wrap: wrap;2555 gap: 16px;2556 align-items: center;2557 }2558 .llmafftm-gsc-btn-save {2559 min-height: 44px !important;2560 padding: 0 28px !important;2561 font-size: 15px !important;2562 font-weight: 600 !important;2563 line-height: 42px !important;2564 border-radius: 4px !important;2565 box-shadow: 0 2px 8px rgba(34, 113, 177, 0.35) !important;2566 }2567 .llmafftm-gsc-btn-save:hover,2568 .llmafftm-gsc-btn-save:focus {2569 box-shadow: 0 2px 10px rgba(34, 113, 177, 0.45) !important;2570 }2571 .llmafftm-gsc-btn-clear {2572 min-height: 44px !important;2573 padding: 0 22px !important;2574 font-size: 14px !important;2575 font-weight: 500 !important;2576 line-height: 40px !important;2577 border-radius: 4px !important;2578 background: #fff !important;2579 color: #50575e !important;2580 border: 2px solid #c3c4c7 !important;2581 box-shadow: none !important;2582 }2583 .llmafftm-gsc-btn-clear:hover,2584 .llmafftm-gsc-btn-clear:focus {2585 background: #f6f7f7 !important;2586 color: #1d2327 !important;2587 border-color: #8c8f94 !important;2588 box-shadow: none !important;2589 }2590 .llmafftm-card {2591 background: white;2592 border: 1px solid #dee2e6;2593 border-radius: 8px;2594 padding: 30px;2595 margin-bottom: 30px;2596 box-shadow: 0 2px 4px rgba(0,0,0,0.05);2597 }2598 .llmafftm-card-header h2 {2599 margin: 0 0 10px 0;2600 font-size: 24px;2601 color: #212529;2602 }2603 .llmafftm-card-description {2604 margin: 0 0 20px 0;2605 color: #6c757d;2606 font-size: 14px;2607 }2608 .llmafftm-token-display {2609 margin: 20px 0;2610 }2611 .llmafftm-token-wrapper {2612 display: flex;2613 gap: 10px;2614 margin-bottom: 15px;2615 }2616 .llmafftm-token-input {2617 flex: 1;2618 padding: 12px 15px;2619 font-family: "Courier New", monospace;2620 font-size: 14px;2621 border: 2px solid #dee2e6;2622 border-radius: 6px;2623 background: #f8f9fa;2624 color: #495057;2625 letter-spacing: 0.5px;2626 }2627 .llmafftm-token-input:focus {2628 outline: none;2629 border-color: #667eea;2630 background: white;2631 }2632 .llmafftm-copy-btn {2633 padding: 12px 20px;2634 background: #667eea;2635 color: white;2636 border: none;2637 border-radius: 6px;2638 cursor: pointer;2639 font-weight: 600;2640 display: flex;2641 align-items: center;2642 gap: 8px;2643 transition: all 0.3s;2644 }2645 .llmafftm-copy-btn:hover {2646 background: #5568d3;2647 transform: translateY(-2px);2648 box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);2649 }2650 .llmafftm-copy-btn.copied {2651 background: #28a745;2652 }2653 .llmafftm-copy-icon {2654 font-size: 16px;2655 }2656 .llmafftm-token-actions {2657 display: flex;2658 gap: 10px;2659 }2660 .llmafftm-regenerate-btn {2661 padding: 10px 20px;2662 border-radius: 6px;2663 font-weight: 600;2664 }2665 .llmafftm-token-info {2666 background: #fff3cd;2667 border-left: 4px solid #ffc107;2668 padding: 15px;2669 border-radius: 4px;2670 margin-top: 20px;2671 }2672 .llmafftm-token-info p {2673 margin: 0;2674 color: #856404;2675 }2676 .llmafftm-info-grid {2677 display: grid;2678 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));2679 gap: 20px;2680 margin-top: 20px;2681 }2682 .llmafftm-info-item {2683 display: flex;2684 flex-direction: column;2685 gap: 8px;2686 }2687 .llmafftm-info-item label {2688 font-weight: 600;2689 color: #495057;2690 font-size: 14px;2691 }2692 .llmafftm-code-block {2693 display: block;2694 padding: 10px 15px;2695 background: #f8f9fa;2696 border: 1px solid #dee2e6;2697 border-radius: 4px;2698 font-family: "Courier New", monospace;2699 font-size: 13px;2700 color: #495057;2701 word-break: break-all;2702 }2703 .llmafftm-steps {2704 margin: 30px 0;2705 }2706 .llmafftm-step {2707 display: flex;2708 gap: 20px;2709 margin-bottom: 30px;2710 padding: 20px;2711 background: #f8f9fa;2712 border-radius: 8px;2713 border-left: 4px solid #667eea;2714 }2715 .llmafftm-step-number {2716 width: 40px;2717 height: 40px;2718 background: #667eea;2719 color: white;2720 border-radius: 50%;2721 display: flex;2722 align-items: center;2723 justify-content: center;2724 font-weight: 700;2725 font-size: 18px;2726 flex-shrink: 0;2727 }2728 .llmafftm-step-content {2729 flex: 1;2730 }2731 .llmafftm-step-content h3 {2732 margin: 0 0 10px 0;2733 color: #212529;2734 font-size: 18px;2735 }2736 .llmafftm-step-content p {2737 margin: 8px 0;2738 color: #495057;2739 line-height: 1.6;2740 }2741 .llmafftm-step-note {2742 background: #e7f3ff;2743 border-left: 3px solid #2196F3;2744 padding: 10px 15px;2745 border-radius: 4px;2746 margin-top: 10px;2747 color: #0d47a1;2748 font-size: 14px;2749 }2750 .llmafftm-credentials-box {2751 background: white;2752 border: 2px solid #dee2e6;2753 border-radius: 8px;2754 padding: 20px;2755 margin: 20px 0;2756 }2757 .llmafftm-credential-item {2758 display: flex;2759 align-items: center;2760 gap: 15px;2761 margin-bottom: 15px;2762 padding: 12px;2763 background: #f8f9fa;2764 border-radius: 6px;2765 }2766 .llmafftm-credential-item:last-child {2767 margin-bottom: 0;2768 }2769 .llmafftm-credential-item label {2770 min-width: 100px;2771 font-weight: 600;2772 color: #495057;2773 font-size: 14px;2774 }2775 .llmafftm-credential-value {2776 flex: 1;2777 padding: 8px 12px;2778 background: white;2779 border: 1px solid #dee2e6;2780 border-radius: 4px;2781 font-family: "Courier New", monospace;2782 font-size: 13px;2783 color: #495057;2784 word-break: break-all;2785 }2786 .llmafftm-copy-small-btn {2787 padding: 8px 12px;2788 background: #667eea;2789 color: white;2790 border: none;2791 border-radius: 4px;2792 cursor: pointer;2793 font-size: 14px;2794 transition: all 0.3s;2795 flex-shrink: 0;2796 }2797 .llmafftm-copy-small-btn:hover {2798 background: #5568d3;2799 transform: scale(1.05);2800 }2801 .llmafftm-copy-small-btn.copied {2802 background: #28a745;2803 }2804 .llmafftm-link {2805 color: #667eea;2806 text-decoration: none;2807 font-weight: 600;2808 }2809 .llmafftm-link:hover {2810 text-decoration: underline;2811 }2812 .llmafftm-help-box {2813 background: #e7f3ff;2814 border-left: 4px solid #2196F3;2815 padding: 20px;2816 border-radius: 6px;2817 margin-top: 30px;2818 }2819 .llmafftm-help-box h4 {2820 margin: 0 0 10px 0;2821 color: #0d47a1;2822 font-size: 16px;2823 }2824 .llmafftm-help-box p {2825 margin: 0;2826 color: #1565c0;2827 }2828 .llmafftm-footer {2829 text-align: center;2830 padding: 20px;2831 color: #6c757d;2832 border-top: 1px solid #dee2e6;2833 margin-top: 40px;2834 }2835 .llmafftm-footer a {2836 color: #667eea;2837 text-decoration: none;2838 }2839 .llmafftm-footer a:hover {2840 text-decoration: underline;2841 }2842 .llmafftm-no-token {2843 text-align: center;2844 padding: 40px 20px;2845 }2846 .llmafftm-no-token-icon {2847 font-size: 64px;2848 margin-bottom: 20px;2849 }2850 .llmafftm-no-token h3 {2851 margin: 0 0 10px 0;2852 color: #212529;2853 font-size: 24px;2854 }2855 .llmafftm-no-token p {2856 margin: 10px 0;2857 color: #6c757d;2858 font-size: 16px;2859 }2860 .llmafftm-generate-btn {2861 margin: 20px 0;2862 padding: 15px 30px;2863 font-size: 16px;2864 font-weight: 600;2865 }2866 .llmafftm-generate-note {2867 margin-top: 15px;2868 font-size: 14px;2869 color: #6c757d;2870 font-style: italic;2871 }2872 .llmafftm-threshold-setting {2873 margin: 20px 0;2874 padding: 20px;2875 background: #f8f9fa;2876 border-radius: 6px;2877 }2878 .llmafftm-threshold-description {2879 margin: 10px 0;2880 color: #6c757d;2881 font-size: 14px;2882 }2883 .llmafftm-events-section {2884 margin-top: 30px;2885 }2886 .llmafftm-events-empty-message {2887 color: #6c757d;2888 font-style: italic;2889 }2890 .llmafftm-events-table-wrapper {2891 max-height: 500px;2892 overflow-y: auto;2893 margin: 20px 0;2894 border: 1px solid #dee2e6;2895 border-radius: 6px;2896 }2897 .llmafftm-events-table {2898 width: 100%;2899 border-collapse: collapse;2900 font-size: 13px;2901 }2902 .llmafftm-events-table thead {2903 background: #f8f9fa;2904 position: sticky;2905 top: 0;2906 z-index: 10;2907 }2908 .llmafftm-events-table th {2909 padding: 12px;2910 text-align: left;2911 font-weight: 600;2912 border-bottom: 2px solid #dee2e6;2913 }2914 .llmafftm-events-table td {2915 padding: 10px 12px;2916 border-bottom: 1px solid #dee2e6;2917 }2918 .llmafftm-events-table tbody tr:hover {2919 background: #f8f9fa;2920 }2921 .llmafftm-events-table code {2922 background: #e9ecef;2923 padding: 2px 6px;2924 border-radius: 3px;2925 }2926 /* Allow long log values (User-Agent, URL, Reason, Pattern, Host, Path)2927 to wrap inside their cell instead of being silently truncated, while2928 still bounding the per-cell width so the table layout stays readable. */2929 .llmafftm-events-table .llmafftm-cell-wrap {2930 max-width: 280px;2931 white-space: normal;2932 word-break: break-word;2933 overflow-wrap: anywhere;2934 vertical-align: top;2935 }2936 .llmafftm-events-table .llmafftm-cell-wrap code {2937 display: inline-block;2938 max-width: 100%;2939 white-space: pre-wrap;2940 word-break: break-word;2941 overflow-wrap: anywhere;2942 line-height: 1.4;2943 }2944 /* The User-Agent string is the longest field in practice, so give it2945 a bit more room and show it in a slightly tighter font. */2946 .llmafftm-events-table .llmafftm-cell-ua {2947 max-width: 360px;2948 min-width: 220px;2949 }2950 .llmafftm-events-table .llmafftm-cell-ua code {2951 font-size: 11px;2952 line-height: 1.45;2953 }2954 /* URL column: full URL is informative for debugging, so allow a2955 similar width and break long path/query segments cleanly. */2956 .llmafftm-events-table .llmafftm-cell-url {2957 max-width: 320px;2958 min-width: 200px;2959 }2960 .llmafftm-events-table .llmafftm-cell-url code {2961 font-size: 11px;2962 }2963 ';2964 }2965 2966 private function get_admin_scripts() {2967 return "2968 jQuery(document).ready(function($) {2969 // Generate token functionality2970 $('.llmafftm-generate-btn').on('click', function() {2971 var \$btn = $(this);2972 var originalHtml = \$btn.html();2973 \$btn.prop('disabled', true).html('⏳ Generating...');2974 2975 $.ajax({2976 url: ajaxurl,2977 type: 'POST',2978 data: {2979 action: 'llmafftm_generate_token',2980 nonce: \$btn.data('nonce')2981 },2982 success: function(response) {2983 if (response.success) {2984 alert('✅ Token generated successfully!\\n\\nThe token has been sent to the onboarding endpoint.');2985 location.reload();2986 } else {2987 alert('❌ Error: ' + (response.data.message || 'Failed to generate token'));2988 \$btn.prop('disabled', false).html(originalHtml);2989 }2990 },2991 error: function() {2992 alert('❌ Error: Failed to generate token. Please try again.');2993 \$btn.prop('disabled', false).html(originalHtml);2994 }2995 });2996 });2997 2998 // Copy token functionality2999 $('.llmafftm-copy-btn').on('click', function() {3000 var \$btn = $(this);3001 var \$input = $('#' + \$btn.data('copy-target'));3002 var \$icon = \$btn.find('.llmafftm-copy-icon');3003 var \$text = \$btn.find('.llmafftm-copy-text');3004 3005 // Select and copy3006 \$input.select();3007 document.execCommand('copy');3008 3009 // Visual feedback3010 \$btn.addClass('copied');3011 \$icon.text('✓');3012 \$text.text('Copied!');3013 3014 setTimeout(function() {3015 \$btn.removeClass('copied');3016 \$icon.text('📋');3017 \$text.text('Copy');3018 }, 2000);3019 });3020 3021 // Regenerate token3022 $('.llmafftm-regenerate-btn').on('click', function() {3023 if (!confirm('Are you sure you want to regenerate the token? The old token will no longer work and you will need to update it on llmaffiliate.com.')) {3024 return;3025 }3026 3027 var \$btn = $(this);3028 var originalHtml = \$btn.html();3029 \$btn.prop('disabled', true).html('⏳ Regenerating...');3030 3031 $.ajax({3032 url: ajaxurl,3033 type: 'POST',3034 data: {3035 action: 'llmafftm_regenerate_token',3036 nonce: \$btn.data('nonce')3037 },3038 success: function(response) {3039 if (response.success) {3040 $('#llmafftm-token-input').val(response.data.token);3041 3042 // Update all token references in the page3043 $('.llmafftm-credential-value').first().text(response.data.token);3044 $('.llmafftm-copy-small-btn').first().data('copy-text', response.data.token);3045 3046 alert('✅ Token regenerated successfully!\\n\\n⚠️ Important: Update the new token on llmaffiliate.com in your site integration settings.');3047 3048 // Reset button3049 \$btn.prop('disabled', false).html(originalHtml);3050 } else {3051 alert('❌ Error: ' + (response.data.message || 'Failed to regenerate token'));3052 \$btn.prop('disabled', false).html(originalHtml);3053 }3054 },3055 error: function() {3056 alert('❌ Error: Failed to regenerate token. Please try again.');3057 \$btn.prop('disabled', false).html(originalHtml);3058 }3059 });3060 });3061 3062 // Copy small buttons (for credentials)3063 $('.llmafftm-copy-small-btn').on('click', function() {3064 var \$btn = $(this);3065 var textToCopy = \$btn.data('copy-text');3066 3067 // Create temporary input3068 var \$temp = $('<input>');3069 $('body').append(\$temp);3070 \$temp.val(textToCopy).select();3071 document.execCommand('copy');3072 \$temp.remove();3073 3074 // Visual feedback3075 var originalText = \$btn.text();3076 \$btn.addClass('copied').text('✓');3077 3078 setTimeout(function() {3079 \$btn.removeClass('copied').text(originalText);3080 }, 2000);3081 });3082 });3083 ";3084 }3085 3086 1897 public function maybe_intercept_request() { 3087 $current_url = $this->get_current_url();1898 $current_url = llmafftm_get_current_url(); 3088 1899 $parsed_path = wp_parse_url($current_url, PHP_URL_PATH); 3089 $normalized_path = is_string($parsed_path) && $parsed_path !== '' 3090 ? untrailingslashit($parsed_path) 3091 : '/'; 3092 3093 if ($normalized_path === '') { 3094 $normalized_path = '/'; 3095 } 1900 $normalized_path = $this->get_normalized_request_path(); 3096 1901 3097 1902 if ( $this->should_bypass_llm_interception( $normalized_path ) ) { … … 3108 1913 ); 3109 1914 3110 // Check if it's admin or API3111 1915 if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) { 3112 1916 $this->log_request_decision(array_merge($base_log_context, array( … … 3116 1920 return; 3117 1921 } 3118 3119 // Check method 1922 3120 1923 $method = strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ); 3121 1924 if ($method !== 'GET') { … … 3127 1930 } 3128 1931 3129 // Always serve /ai-terms page to both humans and LLMs (highest priority)3130 1932 if (strpos($current_url, '/ai-terms') !== false) { 3131 1933 $this->log_request_decision(array_merge($base_log_context, array( … … 3136 1938 return; 3137 1939 } 3138 1940 3139 1941 $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ); 3140 1942 … … 3148 1950 } 3149 1951 3150 // Explicit User-Agent whitelist (GPTBot, bare "Google"/Gemini, etc.): bypass the3151 // numeric threshold entirely so Gemini-style UAs stay enhanced at 0.6+ tuning.3152 1952 $match_details = $this->detect_llm_details($user_agent); 3153 1953 $llm_profile = $match_details['profile']; … … 3172 1972 } 3173 1973 3174 // Advanced detection: weighted score path (threshold applies only here).3175 1974 $detection_result = $this->detector->calculate_llm_score(); 3176 3177 // Log suspicious events (high score but not clearly LLM) 3178 if ( $this->is_logging_enabled() && $detection_result['score'] >= 0.7 && ! $detection_result['is_llm_like'] ) { 3179 $this->detector->log_suspicious_event($detection_result, array( 3180 'reason' => 'high_score_below_threshold' 3181 )); 3182 } 3183 3184 // Decision logic (score-based only; explicit UA already handled above): 3185 // 1. LLM-like score (above threshold) -> serve enhanced 3186 // 2. Otherwise -> serve normal content 3187 1975 3188 1976 $should_serve_enhanced = false; 3189 1977 $confidence = 0.5; 3190 1978 3191 1979 if ($detection_result['is_llm_like']) { 3192 // Detected as LLM-like via scoring 1980 3193 1981 $should_serve_enhanced = true; 3194 1982 $confidence = $detection_result['score']; 3195 3196 // Log the detection 3197 if ( $this->is_logging_enabled() ) { 3198 $this->detector->log_suspicious_event($detection_result, array( 3199 'action' => 'serving_enhanced_content', 3200 'confidence' => $confidence 3201 )); 3202 } 3203 } 3204 1983 } 1984 3205 1985 $detection_reasons = isset( $detection_result['reasons'] ) && is_array( $detection_result['reasons'] ) 3206 1986 ? $detection_result['reasons'] … … 3208 1988 3209 1989 if ($should_serve_enhanced) { 3210 // Use detected profile or fallback to generic-llm 1990 3211 1991 $final_profile = $llm_profile ?: 'generic-llm'; 3212 1992 $score_reason = in_array( 'datacenter_cold_fetch', $detection_reasons, true ) … … 3248 2028 } 3249 2029 } 3250 3251 /** 3252 * Register JavaScript to set human verification token (helps distinguish real browsers from bots). 3253 */ 3254 public function enqueue_human_verification_script() { 3255 if ( is_admin() || wp_doing_ajax() ) { 3256 return; 3257 } 3258 3259 $handle = 'llmafftm-human-token'; 3260 wp_register_script( 3261 $handle, 3262 '', 3263 array(), 3264 LLMAFFTM_VERSION, 3265 true 3266 ); 3267 wp_enqueue_script( $handle ); 3268 wp_add_inline_script( 3269 $handle, 3270 "(function() {\n\tvar token = 'human_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);\n\tvar expires = new Date();\n\texpires.setTime(expires.getTime() + (24 * 60 * 60 * 1000));\n\tdocument.cookie = 'llmafftm_human_token=' + token + ';expires=' + expires.toUTCString() + ';path=/;SameSite=Lax';\n})();", 3271 'after' 3272 ); 3273 } 3274 2030 3275 2031 private function detect_llm_details($user_agent) { 3276 // Gemini citation fetches sometimes send bare "Google" as User-Agent (any case). 2032 3277 2033 if ( preg_match( '/^\s*Google\s*$/i', $user_agent ) ) { 3278 2034 return array( … … 3305 2061 ], 3306 2062 'perplexity' => [ 3307 // Order matters: more specific user-agent strings come first 3308 // so the matched_pattern column in the admin logs UI shows 3309 // the most informative pattern (e.g. "Perplexity-User" rather 3310 // than the generic "Perplexity" substring). 2063 3311 2064 '/Perplexity-User\/(\d+\.\d+)/i', '/Perplexity-User/i', 3312 2065 '/PerplexityBot\/(\d+\.\d+)/i', '/PerplexityBot/i', … … 3325 2078 ], 3326 2079 ]; 3327 2080 3328 2081 foreach ($llm_patterns as $profile => $patterns) { 3329 2082 foreach ($patterns as $pattern) { … … 3337 2090 } 3338 2091 } 3339 2092 3340 2093 return array( 3341 2094 'profile' => false, … … 3345 2098 } 3346 2099 3347 private function detect_llm($user_agent) {3348 $details = $this->detect_llm_details($user_agent);3349 return $details['profile'];3350 }3351 3352 2100 private function map_llm_profile($detected_profile) { 3353 // Mapear perfis detectados para os aceitos pelo content service 3354 $profile_mapping = [ 3355 'openai' => 'openai', // ChatGPT, OpenAI 3356 'anthropic' => 'anthropic', // Claude 3357 'google' => 'google', // Gemini 3358 'perplexity' => 'perplexity', // Perplexity 3359 'mistral' => 'mistral', // Mistral 3360 'generic-llm' => 'generic-llm', // Pi, Llama, Grok, Cohere 3361 ]; 3362 3363 return $profile_mapping[$detected_profile] ?? 'generic-llm'; 3364 } 3365 2101 $allowed = array( 'openai', 'anthropic', 'google', 'perplexity', 'mistral', 'generic-llm' ); 2102 return in_array( $detected_profile, $allowed, true ) ? $detected_profile : 'generic-llm'; 2103 } 2104 3366 2105 private function detect_google_bot($user_agent) { 3367 2106 $google_patterns = [ … … 3380 2119 '/mozilla.*googlebot/i', 3381 2120 ]; 3382 2121 3383 2122 foreach ($google_patterns as $pattern) { 3384 2123 if (preg_match($pattern, $user_agent)) { … … 3386 2125 } 3387 2126 } 3388 2127 3389 2128 return false; 3390 2129 } 3391 2130 3392 2131 private function serve_enhanced_content($user_agent, $llm_profile, $confidence = 0.85, $log_context = array()) { 3393 $url = $this->get_current_url(); 3394 3395 // Mapear perfil para o formato aceito pelo content service 2132 $url = llmafftm_get_current_url(); 2133 3396 2134 $mapped_profile = $this->map_llm_profile($llm_profile); 3397 2135 3398 2136 $response = null; 3399 2137 foreach ( $this->integration_url_lookup_candidates( $url ) as $candidate_url ) { … … 3419 2157 } 3420 2158 } 3421 2159 3422 2160 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) == 200) { 3423 2161 $body = wp_remote_retrieve_body($response); … … 3430 2168 ))); 3431 2169 3432 // Strip robots noindex/nofollow <meta> tags from the body so they3433 // never leak into the enhanced page that we serve to the visiting3434 // LLM client. (Mirrors stripRobotsNoindexDirectives() in CF_worker.js.)3435 2170 $body = $this->strip_robots_noindex_directives_from_html( $body ); 3436 2171 3437 // Clear any potentially-leaked compression/length headers BEFORE we3438 // emit our own. wp_remote_retrieve_body() has already decompressed3439 // the upstream payload, but a Content-Encoding / Content-Length /3440 // Transfer-Encoding header from the *compressed* upstream response3441 // can still end up on our outgoing response if any layer3442 // (mod_php / FastCGI / a caching plugin / the web server) decided3443 // to forward it. Forwarding them as-is causes some LLM-side3444 // fetchers — most notably Perplexity — to try to gunzip a3445 // plain-text body and fail with TLS-like / "request failed in this3446 // environment" errors.3447 2172 if ( ! headers_sent() ) { 3448 2173 header_remove( 'Content-Encoding' ); … … 3450 2175 header_remove( 'Transfer-Encoding' ); 3451 2176 3452 // Drop any outgoing X-Robots-Tag that contains noindex/nofollow.3453 // SEO plugins or the origin server may have already added one;3454 // we do not want it on the enhanced page.3455 2177 if ( function_exists( 'headers_list' ) ) { 3456 2178 foreach ( headers_list() as $sent_header ) { … … 3464 2186 } 3465 2187 3466 // Headers for enhanced content. Vary on every detection-affecting3467 // request header so an intermediary / CDN does not serve enhanced3468 // HTML to a human visitor and vice-versa.3469 2188 header( 'Content-Type: text/html; charset=UTF-8' ); 3470 2189 header( 'X-LLMAFFTM-Interceptor: enhanced' ); … … 3473 2192 header( 'Vary: User-Agent, Accept, Accept-Language, Sec-CH-UA, Sec-CH-UA-Platform, Sec-CH-UA-Mobile, Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest' ); 3474 2193 3475 // Escape late: run the proxied HTML document through wp_kses() with a3476 // full-document allow-list. This is a complete HTML page from our3477 // trusted HTTPS content API, so wp_kses_post() is not appropriate3478 // (it strips <html>/<head>/<meta>/<link>/<body>/<title>).3479 2194 echo wp_kses( $body, $this->get_allowed_full_html() ); 3480 2195 exit; 3481 2196 } else { 3482 // Log error for debugging 2197 3483 2198 $error_message = ''; 3484 2199 $status_code = null; … … 3510 2225 'service_error' => $error_message, 3511 2226 ))); 3512 3513 // Fallback - add fallback header 2227 3514 2228 header('X-LLMAFFTM-Interceptor: fallback'); 3515 2229 } 3516 2230 } 3517 3518 /** 3519 * Serve the AI Terms of Use page 3520 * This page is always served to both humans and LLMs 3521 */ 2231 3522 2232 private function serve_ai_terms_page() { 3523 // Include the AI terms page template 3524 $plugin_dir = dirname(__FILE__); 3525 $ai_terms_file = $plugin_dir . '/ai-terms-page.php'; 3526 2233 2234 $ai_terms_file = plugin_dir_path( __FILE__ ) . 'ai-terms-page.php'; 2235 3527 2236 if (file_exists($ai_terms_file)) { 3528 2237 require_once $ai_terms_file; 3529 3530 // Get the HTML content 2238 3531 2239 $html = llmafftm_get_ai_terms_html(home_url()); 3532 3533 // Set appropriate headers 2240 3534 2241 header('Content-Type: text/html; charset=UTF-8'); 3535 2242 header('X-LLMAFFTM-Interceptor: ai-terms-page'); 3536 2243 header('Cache-Control: public, max-age=3600'); 3537 2244 header('Vary: User-Agent'); 3538 3539 // Escape late: every variable inside the template is already 3540 // escaped per-context via esc_html/esc_url/esc_attr, but we still 3541 // run the final document through wp_kses() with a full-document 3542 // allow-list so the output is sanitized by core before echoing. 2245 3543 2246 echo wp_kses( $html, $this->get_allowed_full_html() ); 3544 2247 exit; 3545 2248 } else { 3546 2249 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 3547 // This error_log() call is gated behind WP_DEBUG so it only runs on developer 3548 // environments; it reports a missing bundled template file (ai-terms-page.php) 3549 // which would only happen if the plugin install was corrupted. There is no 3550 // "safe" WordPress equivalent for writing a developer-only diagnostic line to 3551 // the PHP error log, which is why WordPress.PHP.DevelopmentFunctions.error_log_error_log 3552 // is suppressed here with an explicit justification. 3553 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 2250 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging for missing bundled template. 3554 2251 error_log( '[LLMAFFTM] AI Terms page file not found: ' . $ai_terms_file ); 3555 2252 } … … 3561 2258 } 3562 2259 3563 /**3564 * Allow-list of HTML tags/attributes for wp_kses() when rendering full3565 * HTML documents (the proxied enhanced-content response and our own3566 * standalone /ai-terms page). This is intentionally broad because we are3567 * serving complete HTML pages, not post content; wp_kses_post() would3568 * strip document-level tags such as <html>, <head>, <meta>, <link>,3569 * <title>, <body>, and <script>.3570 *3571 * The returned array is passed through the 'llmafftm_allowed_full_html'3572 * filter so site owners, extensions, or future plugin versions can extend3573 * it (for example, if the enhanced-content service starts emitting new3574 * tags or attributes) without editing this file. Example:3575 *3576 * add_filter( 'llmafftm_allowed_full_html', function( $tags ) {3577 * $tags['dialog'] = array( 'open' => true, 'class' => true );3578 * $tags['template'] = array( 'id' => true );3579 * return $tags;3580 * } );3581 *3582 * @return array<string, array<string, bool>>3583 */3584 /**3585 * Strip robots noindex / nofollow <meta> tags from an HTML document.3586 *3587 * Mirrors stripRobotsNoindexDirectives() in CF_worker.js. Used on the3588 * enhanced-content body returned by the LLMAffiliate render API before3589 * we forward it to the visiting LLM client, so any upstream noindex /3590 * nofollow directives (e.g. injected by an SEO plugin or CDN edge layer3591 * on the origin response) do not leak into the enhanced page.3592 *3593 * The regex tolerates either order of `name="robots"` and `content="..."`3594 * attribute pairs and handles single OR double quotes around values.3595 *3596 * @param string $html HTML document to clean.3597 * @return string HTML with robots-noindex/nofollow <meta> tags removed.3598 */3599 2260 private function strip_robots_noindex_directives_from_html( $html ) { 3600 2261 if ( ! is_string( $html ) || $html === '' ) { … … 3602 2263 } 3603 2264 3604 // Match "<meta name='robots' content='...noindex|nofollow...'>".3605 2265 $pattern_name_first = '#<meta\s+[^>]*\bname\s*=\s*["\']robots["\'][^>]*\bcontent\s*=\s*["\'][^"\']*(?:noindex|nofollow)[^"\']*["\'][^>]*>\s*#i'; 3606 2266 $cleaned = preg_replace( $pattern_name_first, '', $html ); … … 3609 2269 } 3610 2270 3611 // Match the reversed-attribute variant: "<meta content='...' name='robots'>".3612 2271 $pattern_content_first = '#<meta\s+[^>]*\bcontent\s*=\s*["\'][^"\']*(?:noindex|nofollow)[^"\']*["\'][^>]*\bname\s*=\s*["\']robots["\'][^>]*>\s*#i'; 3613 2272 $cleaned_again = preg_replace( $pattern_content_first, '', $cleaned ); … … 3712 2371 ); 3713 2372 3714 /**3715 * Filters the allow-list of tags/attributes used by wp_kses() when rendering3716 * full HTML documents (the proxied enhanced-content response and the3717 * standalone /ai-terms page).3718 *3719 * @param array<string, array<string, bool>> $allowed Associative map of tag names to allowed attributes.3720 */3721 2373 return apply_filters( 'llmafftm_allowed_full_html', $allowed ); 3722 2374 } 3723 3724 /** 3725 * URLs to try when calling the render API (with/without trailing slash). 3726 * 3727 * @param string $url Absolute page URL. 3728 * @return string[] 3729 */ 2375 3730 2376 private function integration_url_lookup_candidates( $url ) { 3731 2377 $url = trim( (string) $url ); … … 3743 2389 } 3744 2390 3745 /**3746 * Return the same absolute URL with its path trailing slash toggled.3747 *3748 * @param string $url Absolute URL.3749 * @return string Empty string when the URL cannot be parsed.3750 */3751 2391 private function toggle_url_trailing_slash( $url ) { 3752 2392 $parts = wp_parse_url( $url ); … … 3776 2416 } 3777 2417 3778 private function get_current_url() {3779 $scheme = is_ssl() ? 'https' : 'http';3780 $host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) );3781 $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ) );3782 return $scheme . '://' . $host . $uri;3783 }3784 3785 /**3786 * Output the Google Search Console site-verification <meta> tag on3787 * public frontend page renders. Skipped for admin / AJAX / REST / feed3788 * responses so the token only appears where Search Console actually3789 * looks for it.3790 *3791 * Stored value comes from **Settings → LLMAffiliate AI Traffic** (optional field).3792 * Developers may still override with the same filter.3793 *3794 * // Override with a different token:3795 * add_filter( 'llmafftm_google_site_verification', function ( $stored ) {3796 * return 'CUSTOM_TOKEN';3797 * } );3798 *3799 * // Disable the meta tag entirely:3800 * add_filter( 'llmafftm_google_site_verification', '__return_empty_string' );3801 *3802 * @return void3803 */3804 2418 public function output_google_site_verification_meta() { 3805 2419 if ( is_admin() || wp_doing_ajax() || is_feed() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { … … 3807 2421 } 3808 2422 3809 /**3810 * Value saved in plugin settings (optional), then passed through this filter3811 * so themes or must-use plugins can override or clear it.3812 *3813 * @param string $token Verification meta content, or empty string if unset.3814 */3815 2423 $stored = get_option( $this->option_gsc_verification, '' ); 3816 2424 $stored = is_string( $stored ) ? trim( $stored ) : ''; … … 3825 2433 } 3826 2434 3827 /**3828 * Paths under the site root for monetized pages (from public user-sitemaps API).3829 * Cached briefly to avoid calling the API on every robots.txt request.3830 *3831 * @return string[] Sorted unique paths like `/laptops`.3832 */3833 2435 private function get_monetized_paths_for_robots_txt() { 3834 2436 $host = wp_parse_url( home_url(), PHP_URL_HOST ); … … 3843 2445 } 3844 2446 3845 $api_url = 'https://api.llmaffiliate.com/api/v1/enhanced-content/public/user-sitemaps?host=' . rawurlencode( $host ); 3846 $response = wp_remote_get( $api_url, array( 'timeout' => 10 ) ); 2447 list( , $items ) = $this->fetch_public_user_sitemaps( 10 ); 3847 2448 3848 2449 $seen = array(); 3849 if ( ! is_wp_error( $response ) && (int) wp_remote_retrieve_response_code( $response ) === 200 ) { 3850 $data = json_decode( wp_remote_retrieve_body( $response ), true ); 3851 if ( is_array( $data ) && ! empty( $data['sitemaps'] ) && is_array( $data['sitemaps'] ) ) { 3852 foreach ( $data['sitemaps'] as $row ) { 3853 if ( ! is_array( $row ) || empty( $row['site_url'] ) ) { 3854 continue; 3855 } 3856 $path = wp_parse_url( (string) $row['site_url'], PHP_URL_PATH ); 3857 if ( ! is_string( $path ) || $path === '' || $path === '/' ) { 3858 continue; 3859 } 3860 $path = '/' . trim( untrailingslashit( $path ), '/' ); 3861 if ( $path !== '/' ) { 3862 $seen[ $path ] = true; 3863 } 3864 } 2450 foreach ( $items as $row ) { 2451 if ( ! is_array( $row ) || empty( $row['site_url'] ) ) { 2452 continue; 2453 } 2454 $path = wp_parse_url( (string) $row['site_url'], PHP_URL_PATH ); 2455 if ( ! is_string( $path ) || $path === '' || $path === '/' ) { 2456 continue; 2457 } 2458 $path = '/' . trim( untrailingslashit( $path ), '/' ); 2459 if ( $path !== '/' ) { 2460 $seen[ $path ] = true; 3865 2461 } 3866 2462 } … … 3874 2470 } 3875 2471 3876 /**3877 * Emit LLMAFFTM robots.txt: WordPress-style `User-agent: *` (wp-admin rules +3878 * monetized paths), per-bot Allow blocks, Host, and plugin sitemap lines only.3879 *3880 * Core `Sitemap: …/wp-sitemap.xml` is omitted; use LLMAFFTM `/sitemap.xml` instead.3881 *3882 * Crawler list is filterable via `llmafftm_robots_allowed_user_agents` (should not3883 * include `*`; the default `*` block is always emitted first).3884 *3885 * @param string $output Existing robots.txt output (discarded when public).3886 * @param bool $public Whether the site is public.3887 * @return string Modified robots.txt output.3888 */3889 2472 public function filter_robots_txt( $output, $public ) { 3890 2473 if ( ! $public ) { … … 3892 2475 } 3893 2476 2477 return $this->build_robots_txt(); 2478 } 2479 2480 private function build_robots_txt() { 3894 2481 $paths = $this->get_monetized_paths_for_robots_txt(); 3895 2482 3896 2483 $default_user_agents = array( 3897 2484 '*', 3898 // OpenAI. 2485 3899 2486 'GPTBot', 3900 2487 'OAI-SearchBot', 3901 2488 'ChatGPT-User', 3902 // Anthropic. 2489 3903 2490 'ClaudeBot', 3904 2491 'anthropic-ai', 3905 // Google. 2492 3906 2493 'Googlebot', 3907 2494 'Google-Extended', … … 3911 2498 'GoogleOther-Image', 3912 2499 'GoogleOther-Video', 3913 // Perplexity. 2500 3914 2501 'PerplexityBot', 3915 2502 'Perplexity-User', 3916 // Microsoft / Bing. 2503 3917 2504 'Bingbot', 3918 2505 'msnbot', 3919 // Apple. 2506 3920 2507 'Applebot', 3921 2508 'Applebot-Extended', 3922 // Meta. 2509 3923 2510 'Meta-ExternalAgent', 3924 2511 'FacebookBot', 3925 // Amazon. 2512 3926 2513 'Amazonbot', 3927 // Other AI crawlers. 2514 3928 2515 'cohere-ai', 3929 2516 'Bytespider', … … 3933 2520 ); 3934 2521 3935 /**3936 * Filters user-agent names after the default `*` block (order preserved; `*` ignored here).3937 *3938 * @param string[] $user_agents Crawler user-agent strings.3939 */3940 2522 $user_agents = apply_filters( 'llmafftm_robots_allowed_user_agents', $default_user_agents ); 3941 2523 if ( ! is_array( $user_agents ) ) { … … 3979 2561 } 3980 2562 3981 /**3982 * Cleanup plugin options on uninstall (true delete from Plugins screen).3983 */3984 2563 function llmafftm_uninstall_plugin() { 3985 2564 $option_keys = array( … … 3993 2572 'llmafftm_gsc_verification_token', 3994 2573 'llmafftm_legacy_options_migrated', 3995 // Legacy keys for cleanup parity. 2574 3996 2575 'llm_interceptor_token', 3997 2576 'llm_interceptor_site_id', … … 4009 2588 register_uninstall_hook(__FILE__, 'llmafftm_uninstall_plugin'); 4010 2589 4011 // Initialize plugin4012 2590 llmafftm_migrate_legacy_options(); 4013 2591 new LLMAFFTM_Plugin(); -
llmaffiliate-ai-traffic-monetizer/trunk/readme.txt
r3624563 r3652650 3 3 Tags: ai, llm, chatbot, crawler, content delivery 4 4 Requires at least: 6.0 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.0.1 07 Stable tag: 1.0.11 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 23 23 - Serves enhanced HTML responses via a remote content service. 24 24 - Automatically falls back to native WordPress rendering if the external service fails. 25 - Adds cache-control, licensing, and optional Text and Data Mining headers. 26 - Supports secure QA overrides using HMAC-signed query parameters. 25 - Adds cache-control and interceptor diagnostic headers. 27 26 28 27 = HOW IT WORKS = … … 48 47 = OPERATIONAL LOGGING & PRIVACY = 49 48 50 An in-plugin setting (**Settings → LLMAffiliate AI Traffic → Advanced Detection Settings**) allows you to disable request and suspicious-event logging. When disabled, no logs are stored. When enabled (default), the plugin may store operational or securitylogs. These may include:49 An in-plugin setting (**Settings → LLMAffiliate AI Traffic → Advanced Detection Settings**) allows you to disable request decision logging. When disabled, no logs are stored. When enabled (default), the plugin may store operational logs. These may include: 51 50 * IP address 52 51 * User agent … … 55 54 * Selected HTTP headers 56 55 * Detection and decision metadata 57 * Suspicious-event or requestlogs56 * Request decision logs 58 57 59 58 This logging is used for debugging, security analysis, and request classification. Data is stored within your WordPress database. Configure and retain logs in accordance with your privacy policy and applicable regulations. … … 82 81 83 82 * **Purpose:** Registers your WordPress site with the LLM Affiliate backend so the site can receive enhanced content. Think of it as a one-time (idempotent) handshake. 84 * **When it is sent:** Only when you click **Generate Token** in the plugin admin screen (Settings → LLMAffiliate AI Traffic). It is also re-sent if you regenerate/rotate the token. It is **not** sent on every page load.83 * **When it is sent:** When an administrator clicks **Generate Token** or **Regenerate Token** in Settings → LLMAffiliate AI Traffic. If a token already exists but onboarding has not yet succeeded, the plugin also retries this request from WordPress `init` until a successful response is stored. After onboarding succeeds, it is not sent again on later page loads. 85 84 * **Data sent (JSON body):** 86 85 * `token` – the authentication token generated by the plugin … … 97 96 * `llm_profile` – the detected LLM profile name (e.g., `chatgpt`, `claude`, `gemini`) 98 97 * `User-Agent` header – forwarded from the visiting LLM client 99 * `Accept-Language` header – forwarded from the visiting LLM client100 98 * `X-LLM-Profile` header – the plugin's detected profile 101 99 * `X-LLM-Confidence` header – a numeric confidence score (0–1) from the local detector … … 105 103 106 104 * **Purpose:** Returns JSON listing LLM-optimized sitemap URLs for the given site hostname so this plugin can serve `/sitemap.xml` (or the configured sitemap path) as a simple URL index for search engines. 107 * **When it is sent:** On ly on GET requests that match the plugin’s dedicated sitemap path (not on normal page views). The `host` query parameter is your site’s hostname from WordPress `home_url()` (no path, no user input from forms).105 * **When it is sent:** On GET requests that match the plugin’s dedicated sitemap path, and when the plugin builds `robots.txt` additions so it can list monetized paths. It is not sent on normal page views. The `host` query parameter is your site’s hostname from WordPress `home_url()` (no path, no user input from forms). 108 106 * **Data sent:** `host` only, in the query string. Each item in the JSON `sitemaps` array includes `site_url`, `last_modified`, `products_count`, and `total_traffic`. Non-200 responses result in an empty `<urlset>` (no URLs listed). 109 107 … … 162 160 The plugin logs the failure (if enabled), adds an `X-LLMAFFTM-Interceptor: fallback` header, and allows WordPress to render normally. 163 161 164 = How do signed overrides work? =165 Enable “Allow Signed Override” and configure an HMAC secret.166 167 Then append:168 `?llm_profile=<profile>&llm_sig=<signature>`169 170 Signature format:171 `hash_hmac('sha256', "<path>|<profile>", <secret>)`172 173 162 = Is this plugin safe for SEO? = 174 163 Yes. Googlebot and other search crawlers always receive original content, ensuring compliance with search engine guidelines. … … 181 170 182 171 == Changelog == 172 173 = 1.0.11 = 174 * Settings: Advanced Detection Settings now shows request decision logs only; suspicious-event logging and its admin table were removed. 175 * Compatibility: tested with WordPress 7.1. 176 * Assets: admin page CSS and JavaScript moved out of inline output into `assets/admin.css` and `assets/admin.js`, registered and enqueued through the WordPress asset API. 177 * Internationalization: admin screen labels and AJAX response messages are now translatable, with JavaScript strings passed through `wp_localize_script()`. 178 * Hardening: `ABSPATH` guards added to every PHP file; `$_POST` values are run through `wp_unslash()` before sanitizing. 179 * Housekeeping: removed dead code and the unused client-side human-token script; consolidated duplicated client-IP, current-URL, request-path, and llms document helpers. 183 180 184 181 = 1.0.10 = … … 222 219 * Replaced `urlencode()` in the enhanced-content URL builder with `rawurlencode()` so query-string values are encoded per RFC 3986, as recommended by the WordPress Plugin Review Team and `WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode`. 223 220 * Verified publicly reachable Terms of Service (`https://llmaffiliate.com/terms`) and Privacy Policy (`https://llmaffiliate.com/privacy`) URLs referenced from the `== External services ==` section of this readme. 224 225 221 = 1.0.6 = 226 222 * Readme: document the `public/user-sitemaps` external API used when serving the plugin sitemap URL.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)