Plugin Directory

Changeset 3652650


Ignore:
Timestamp:
08/18/2026 11:15:54 AM (2 months ago)
Author:
llmaffiliate01
Message:

Release version 1.0.11

Location:
llmaffiliate-ai-traffic-monetizer/trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • llmaffiliate-ai-traffic-monetizer/trunk/index.php

    r3522098 r3652650  
    11<?php
    2 // Silence is golden.
     2if ( ! defined( 'ABSPATH' ) ) {
     3        exit;
     4}
  • llmaffiliate-ai-traffic-monetizer/trunk/llm-interceptor.php

    r3624563 r3652650  
    33 * Plugin Name: LLMAffiliate AI Traffic Monetizer
    44 * Description: Detects LLM user agents, enforces Google parity, and serves enhanced content from a remote service.
    5  * Version: 1.0.10
     5 * Version: 1.0.11
    66 * Requires at least: 6.0
    77 * Requires PHP: 7.4
    … …  
    1616}
    1717
    18 define( 'LLMAFFTM_VERSION', '1.0.10' );
    19 
    20 /**
    21  * Copy options from legacy llm_interceptor_* keys to llmafftm_* (one-time).
    22  */
     18define( 'LLMAFFTM_VERSION', '1.0.11' );
     19
     20
     21function 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
     46function 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
    2354function llmafftm_migrate_legacy_options() {
    2455    if ( get_option( 'llmafftm_legacy_options_migrated', '' ) === '1' ) {
    … …  
    4778}
    4879
    49 // Check minimum PHP version
    50 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 loaded
    58 if (! function_exists('add_action')) {
    59     return;
    60 }
    61 
    62 /**
    63  * Advanced LLM Detection Class
    64  *
    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 patterns
    69  * - IP-based rate limiting
    70  */
    7180class 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
    8286    public function calculate_llm_score() {
    8387        $signals = array();
    8488        $total_weight = 0;
    8589        $weighted_score = 0;
    86        
    87         // Signal 1: User-Agent analysis (weight: 0.30)
     90
    8891        $ua_score = $this->analyze_user_agent();
    8992        $signals['user_agent'] = $ua_score;
    … …  
    9194        $weighted_score += $ua_score * $weight;
    9295        $total_weight += $weight;
    93        
    94         // Signal 2: Browser headers presence (weight: 0.25)
     96
    9597        $headers_score = $this->analyze_browser_headers();
    9698        $signals['browser_headers'] = $headers_score;
    … …  
    98100        $weighted_score += $headers_score * $weight;
    99101        $total_weight += $weight;
    100        
    101         // Signal 3: Cookie and session analysis (weight: 0.20)
     102
    102103        $cookie_score = $this->analyze_cookies();
    103104        $signals['cookies'] = $cookie_score;
    … …  
    105106        $weighted_score += $cookie_score * $weight;
    106107        $total_weight += $weight;
    107        
    108         // Signal 4: Request behavior patterns (weight: 0.15)
     108
    109109        $behavior_score = $this->analyze_behavior();
    110110        $signals['behavior'] = $behavior_score;
    … …  
    112112        $weighted_score += $behavior_score * $weight;
    113113        $total_weight += $weight;
    114        
    115         // Signal 5: Referer and navigation patterns (weight: 0.10)
     114
    116115        $referer_score = $this->analyze_referer();
    117116        $signals['referer'] = $referer_score;
    … …  
    119118        $weighted_score += $referer_score * $weight;
    120119        $total_weight += $weight;
    121        
    122         // Normalize score
     120
    123121        $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 1
     122        $final_score = max(0.0, min(1.0, $final_score));
    125123
    126124        $is_llm_like = $final_score >= $this->threshold;
    127125        $reasons     = array();
    128126
    129         // Soft escalation (refined from CF_worker.js `datacenter_cold_fetch`):
    130         // a request originating from a known cloud / datacenter IP range
    131         // that arrives with NO cookies AND NO referer is suspicious — but
    132         // not enough on its own to justify a hard "force enhanced" override,
    133         // because legitimate humans behind corporate VPNs, NAT exits, or
    134         // cloud-hosted privacy proxies can also match this pattern.
    135         //
    136         // Instead we combine the network signal with the existing per-signal
    137         // scores:
    138         //   * If at least one of the other detectors is already strongly
    139         //     bot-leaning (programmatic UA, missing browser headers, or no
    140         //     cookies at all), we DO mark the request as LLM-like and raise
    141         //     the effective score to >= 0.9. This catches DeepSeek-style
    142         //     fetchers reliably (programmatic UA + datacenter cold fetch).
    143         //   * Otherwise we only nudge the score up to 0.65 — slightly above
    144         //     the default 0.6 threshold. The configured threshold and the
    145         //     other signals still get the final say, which lets site owners
    146         //     tune sensitivity by adjusting the threshold rather than fight
    147         //     a hardcoded override.
    148127        $network = $this->classify_network_origin();
    149128        $signals['network_datacenter'] = $network['is_datacenter'] ? 1.0 : 0.0;
    … …  
    155134            $reasons[] = 'datacenter_cold_fetch';
    156135
    157             // curl/wget from a cloud VM is normal dev traffic — do not apply the cold-fetch
    158             // score bump used for opaque LLM fetchers (fixes false positives at threshold 0.5–0.6).
    159136            $ua_cf           = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
    160137            $is_cli_http_tool = (bool) preg_match( '/^\s*(?:curl|wget)\/\d/i', $ua_cf );
    … …  
    183160        );
    184161    }
    185    
    186     /**
    187      * Analyze User-Agent string
    188      * Returns: 0.0 (human-like) to 1.0 (bot-like)
    189      */
     162
    190163    private function analyze_user_agent() {
    191164        $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
    192        
     165
    193166        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
    199170        if ( preg_match( '/^\s*(?:curl|wget)\/\d/i', $user_agent ) ) {
    200171            return 0.42;
    201172        }
    202173
    203         // Known LLM/bot patterns (high score).
    204         //
    205         // Note on DeepSeek-style fetchers: DeepSeek and similar LLM agents
    206         // frequently use generic browser-like or generic programmatic UAs
    207         // (python, Go-http-client, httpx, aiohttp, etc.) without a
    208         // 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 in
    214         //     calculate_llm_score().
    215174        $bot_patterns = array(
    216175            '/GPTBot/i', '/OAI-SearchBot/i', '/ChatGPT/i', '/OpenAI/i',
    … …  
    220179            '/Mistral/i', '/MistralBot/i',
    221180            '/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
    224182            '/python(?:-requests|-urllib)?/i',
    225183            '/httpx/i', '/aiohttp/i',
    … …  
    228186            '/\bruby\b/i', '/node-fetch/i', '/axios/i',
    229187            '/scrapy/i', '/phantomjs/i', '/headlesschrome/i',
    230             // Generic fallbacks.
     188
    231189            '/bot/i', '/crawler/i', '/spider/i',
    232190        );
    233        
     191
    234192        foreach ($bot_patterns as $pattern) {
    235193            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
    241198        $browser_patterns = array(
    242199            '/Mozilla\/5\.0.*Chrome/i',
    … …  
    245202            '/Mozilla\/5\.0.*Edge/i'
    246203        );
    247        
     204
    248205        $has_browser_pattern = false;
    249206        foreach ($browser_patterns as $pattern) {
    … …  
    253210            }
    254211        }
    255        
    256         // Check for browser version numbers (indicates real browser)
     212
    257213        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
    262217        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
    273224    private function analyze_browser_headers() {
    274225        $score = 0.0;
    275226        $signals = 0;
    276        
    277         // sec-ch-ua headers (modern browsers send these)
     227
    278228        $has_sec_ch_ua = ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA'] ?? '' ) ) );
    279229        $has_sec_ch_ua_mobile = ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ?? '' ) ) );
    280230        $has_sec_ch_ua_platform = ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '' ) ) );
    281        
     231
    282232        if ($has_sec_ch_ua && $has_sec_ch_ua_mobile && $has_sec_ch_ua_platform) {
    283             $score += 0.1; // Strong human signal
     233            $score += 0.1;
    284234            $signals++;
    285235        } elseif (!$has_sec_ch_ua && !$has_sec_ch_ua_mobile && !$has_sec_ch_ua_platform) {
    286             $score += 0.6; // Missing modern browser headers
     236            $score += 0.6;
    287237            $signals++;
    288238        }
    289        
    290         // Accept-Language header
     239
    291240        $accept_language = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '' ) );
    292241        if (!empty($accept_language)) {
    293             // Real browsers usually send language preferences
     242
    294243            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 format
     244                $score += 0.1;
    296245            } else {
    297                 $score += 0.4; // Suspicious format
     246                $score += 0.4;
    298247            }
    299248            $signals++;
    300249        } else {
    301             $score += 0.7; // Missing Accept-Language
     250            $score += 0.7;
    302251            $signals++;
    303252        }
    304        
    305         // Accept header
     253
    306254        $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ?? '' ) );
    307255        if (!empty($accept) && preg_match('/text\/html|application\/xhtml\+xml/i', $accept)) {
    308             $score += 0.1; // Proper Accept header
     256            $score += 0.1;
    309257            $signals++;
    310258        } else {
    311             $score += 0.5; // Missing or suspicious Accept
     259            $score += 0.5;
    312260            $signals++;
    313261        }
    314        
    315         // DNT (Do Not Track) - real browsers may send this
     262
    316263        if ( ! empty( sanitize_text_field( wp_unslash( $_SERVER['HTTP_DNT'] ?? '' ) ) ) ) {
    317             $score += 0.1; // Human signal
     264            $score += 0.1;
    318265            $signals++;
    319266        }
    320        
     267
    321268        return $signals > 0 ? ($score / $signals) : 0.5;
    322269    }
    323    
    324     /**
    325      * Analyze cookies and session
    326      * Returns: 0.0 (human-like) to 1.0 (bot-like)
    327      */
     270
    328271    private function analyze_cookies() {
    329272        $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
    341274        if (empty($cookies)) {
    342             return 0.8; // Very suspicious
    343         }
    344        
    345         // WordPress session cookies (wp-settings, etc.)
     275            return 0.8;
     276        }
     277
    346278        $has_wp_cookies = false;
    347279        foreach (array_keys($cookies) as $name) {
    … …  
    351283            }
    352284        }
    353        
     285
    354286        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
    366293    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
    371297        $rate_score = $this->check_rate_limit($client_ip);
    372        
    373         // Check if accessing robots.txt (common bot behavior)
     298
    374299        $is_robots = (strpos($current_url, '/robots.txt') !== false);
    375300        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
    382304        return $rate_score;
    383305    }
    384    
    385     /**
    386      * Check rate limiting for IP
    387      */
     306
    388307    private function check_rate_limit($ip) {
    389308        $transient_key = 'llmafftm_rate_' . md5($ip);
    390309        $requests = get_transient($transient_key);
    391        
     310
    392311        if ($requests === false) {
    393             // First request in window
     312
    394313            set_transient($transient_key, 1, $this->rate_limit_window);
    395             return 0.2; // Normal
    396         }
    397        
     314            return 0.2;
     315        }
     316
    398317        $request_count = (int) $requests;
    399318        $request_count++;
    400        
     319
    401320        if ($request_count > $this->rate_limit_max_requests) {
    402321            set_transient($transient_key, $request_count, $this->rate_limit_window);
    403             return 0.95; // Rate limit exceeded - very bot-like
    404         }
    405        
     322            return 0.95;
     323        }
     324
    406325        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;
    410328        if ($frequency > 0.5) {
    411             return 0.8; // Too fast
     329            return 0.8;
    412330        } 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
    423337    private function analyze_referer() {
    424338        $referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) );
    425        
    426         // No referer on direct access is normal
     339
    427340        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
    432344        $current_host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) );
    433345        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
    438349        $search_engines = array('google.com', 'bing.com', 'yahoo.com', 'duckduckgo.com');
    439350        foreach ($search_engines as $engine) {
    440351            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
    446356        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
    512363    private function get_datacenter_cidrs() {
    513364        $defaults = array(
    514             // Huawei Cloud (incl. HK region — frequently observed in
    515             // DeepSeek-side fetches).
     365
    516366            '114.119.128.0/17',
    517367            '119.8.0.0/16',
    … …  
    519369            '139.9.0.0/16',
    520370            '159.138.0.0/16',
    521             // Alibaba Cloud / Aliyun (publicly published ranges).
     371
    522372            '47.74.0.0/15',
    523373            '47.88.0.0/14',
    524374            '47.244.0.0/15',
    525375            '8.208.0.0/12',
    526             // Tencent Cloud.
     376
    527377            '49.51.0.0/16',
    528378            '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
    532380            '3.0.0.0/15',
    533381            '13.32.0.0/15',
    … …  
    535383            '52.0.0.0/8',
    536384            '54.0.0.0/8',
    537             // Google Cloud.
     385
    538386            '34.64.0.0/10',
    539387            '35.184.0.0/13',
    … …  
    546394            '35.224.0.0/12',
    547395            '35.240.0.0/13',
    548             // Microsoft / Azure.
     396
    549397            '13.64.0.0/11',
    550398            '20.0.0.0/8',
    … …  
    552400            '52.224.0.0/11',
    553401            '104.40.0.0/13',
    554             // DigitalOcean.
     402
    555403            '64.225.0.0/16',
    556404            '67.207.64.0/19',
    … …  
    565413            '167.99.0.0/16',
    566414            '178.62.0.0/16',
    567             // Linode / Akamai.
     415
    568416            '45.33.0.0/16',
    569417            '45.56.64.0/18',
    … …  
    575423            '173.255.192.0/18',
    576424            '192.46.208.0/20',
    577             // OVH.
     425
    578426            '51.38.0.0/16',
    579427            '51.68.0.0/16',
    … …  
    591439            '167.114.0.0/16',
    592440            '198.27.64.0/18',
    593             // Hetzner.
     441
    594442            '5.9.0.0/16',
    595443            '78.46.0.0/15',
    … …  
    609457            '195.201.0.0/16',
    610458            '213.133.96.0/19',
    611             // Vultr / Choopa.
     459
    612460            '45.32.0.0/16',
    613461            '45.63.0.0/16',
    … …  
    625473            '207.148.0.0/16',
    626474            '208.85.16.0/20',
    627             // Scaleway.
     475
    628476            '51.15.0.0/16',
    629477            '51.158.0.0/15',
    … …  
    632480            '212.47.224.0/19',
    633481            '212.83.128.0/19',
    634             // Contabo.
     482
    635483            '109.123.224.0/19',
    636484            '161.97.128.0/17',
    637485            '173.212.192.0/18',
    638486            '207.180.192.0/18',
    639             // IBM Cloud / SoftLayer.
     487
    640488            '50.22.0.0/16',
    641489            '50.23.0.0/16',
    … …  
    650498        );
    651499
    652         /**
    653          * Filters the list of datacenter / cloud IP CIDR ranges used to detect
    654          * 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          */
    658500        $filtered = apply_filters( 'llmafftm_datacenter_cidrs', $defaults );
    659501
    … …  
    661503    }
    662504
    663     /**
    664      * Check whether the given IPv4 address falls inside any of the configured
    665      * datacenter CIDR ranges.
    666      *
    667      * @param string $ip IPv4 address.
    668      * @return bool
    669      */
    670505    private function ip_in_datacenter_range( $ip ) {
    671506        if ( ! is_string( $ip ) || $ip === '' ) {
    … …  
    674509
    675510        if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
    676             // IPv6 ranges are not in the default list; opt-in via filter.
     511
    677512            return false;
    678513        }
    … …  
    703538    }
    704539
    705     /**
    706      * Returns true when the current request looks like a "cold" LLM-side
    707      * 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      */
    717540    private function classify_network_origin() {
    718         $client_ip   = $this->get_client_ip();
     541        $client_ip   = llmafftm_get_client_ip();
    719542        $is_dc       = $this->ip_in_datacenter_range( $client_ip );
    720543        $cookie_hdr  = sanitize_text_field( wp_unslash( $_SERVER['HTTP_COOKIE'] ?? '' ) );
    … …  
    728551    }
    729552
    730     /**
    731      * Set detection threshold
    732      */
    733553    public function set_threshold($threshold) {
    734554        $this->threshold = max(0.0, min(1.0, (float) $threshold));
    735555    }
    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
    768557}
    769558
    770 // Basic functional plugin
    771559class LLMAFFTM_Plugin {
    772    
     560
    773561    private $option_name = 'llmafftm_token';
    774562    private $option_site_id = 'llmafftm_site_id';
    … …  
    776564    private $option_request_logs = 'llmafftm_request_logs';
    777565    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
    783567    private $option_gsc_verification = 'llmafftm_gsc_verification_token';
    784568    private $detector;
    785    
     569
    786570    public function __construct() {
    787571        $this->detector = new LLMAFFTM_Detector();
    788        
    789         // Load threshold from options
     572
    790573        $threshold = get_option('llmafftm_threshold', 0.6);
    791574        $this->detector->set_threshold($threshold);
    792575
    793576        add_action( 'init', array( $this, 'maybe_serve_plugin_sitemap' ), 0 );
     577        add_action( 'init', array( $this, 'maybe_serve_plugin_robots' ), 0 );
    794578        add_action( 'init', array( $this, 'maybe_serve_llms_txt' ), 0 );
    795579        add_action( 'init', array( $this, 'maybe_serve_llms_full_txt' ), 0 );
    … …  
    807591        add_filter('robots_txt', array($this, 'filter_robots_txt'), 10, 2);
    808592
    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 the
    813         // public frontend. The default token is filterable via
    814         // 'llmafftm_google_site_verification' so site owners can override
    815         // (or disable, by returning ''') without editing this file.
    816593        add_action( 'wp_head', array( $this, 'output_google_site_verification_meta' ), 1 );
    817594    }
    818    
     595
    819596    public function init() {
    820         // Generate site ID if not exists (but NOT token - user must generate it manually)
    821597        if (!get_option($this->option_site_id)) {
    822598            $this->generate_site_id();
    823599        }
    824600
    825         // Reconcile existing/migrated state: token + site_id but onboarding not completed yet.
    826601        $this->maybe_reconcile_onboarding();
    827602    }
    828603
    829     /**
    830      * Serve /sitemap.xml from the public LLMAffiliate API (host query only).
    831      *
    832      * @return void
    833      */
    834604    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() ) {
    840606            return;
    841607        }
    842608
    843609        $norm = $this->get_normalized_request_path();
    844         if ( ! $this->is_plugin_sitemap_path( $norm ) ) {
     610        if ( ! $this->is_home_relative_path( $norm, '/sitemap.xml' ) ) {
    845611            return;
    846612        }
    … …  
    852618        echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
    853619
    854         $host = wp_parse_url( home_url(), PHP_URL_HOST );
     620        list( $host, $items ) = $this->fetch_public_user_sitemaps( 15 );
    855621        if ( ! is_string( $host ) || $host === '' ) {
    856622            echo '</urlset>';
    … …  
    858624        }
    859625
     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
    860689        $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 ) );
    862691
    863692        $items = array();
    … …  
    869698        }
    870699
    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
    910703    private function format_sitemap_lastmod_w3c( $raw ) {
    911704        if ( ! is_string( $raw ) || trim( $raw ) === '' ) {
    … …  
    940733    }
    941734
    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      */
    949735    private function compute_sitemap_priority( $products_count, $total_traffic ) {
    950736        if ( $products_count >= 20 || $total_traffic >= 100 ) {
    … …  
    963749    }
    964750
    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      */
    971751    private function compute_sitemap_changefreq( $products_count ) {
    972752        if ( $products_count >= 5 ) {
    … …  
    979759    }
    980760
    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
    984771    private function get_normalized_request_path() {
    985         $current_url = $this->get_current_url();
     772        $current_url = llmafftm_get_current_url();
    986773        $parsed_path = wp_parse_url( $current_url, PHP_URL_PATH );
    987774        $normalized_path = is_string( $parsed_path ) && $parsed_path !== ''
    … …  
    996783    }
    997784
    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 );
    1004787        if ( ! is_string( $expected ) || $expected === '' ) {
    1005788            return false;
    1006789        }
    1007 
    1008790        return untrailingslashit( $normalized_path ) === untrailingslashit( $expected );
    1009791    }
    1010792
    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
    1048794    private function match_buy_redirect_asin( $normalized_path ) {
    1049795        if ( ! is_string( $normalized_path ) || $normalized_path === '' ) {
    … …  
    1066812    }
    1067813
    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 void
    1075      */
    1076814    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() ) {
    1082816            return;
    1083817        }
    … …  
    1130864    }
    1131865
    1132     /**
    1133      * @param string $location Absolute redirect target URL.
    1134      * @param string $interceptor_header Value for X-LLMAFFTM-Interceptor.
    1135      * @return void
    1136      */
    1137866    private function redirect_buy_target( $location, $interceptor_header ) {
    1138867        nocache_headers();
    … …  
    1143872    }
    1144873
    1145     /**
    1146      * @param string $asin Normalized ASIN.
    1147      * @param string $interceptor_header Value for X-LLMAFFTM-Interceptor.
    1148      * @return void
    1149      */
    1150874    private function redirect_buy_fallback( $asin, $interceptor_header ) {
    1151875        $fallback = 'https://www.amazon.com/dp/' . rawurlencode( $asin );
    … …  
    1153877    }
    1154878
    1155     /**
    1156      * Serve /llms.txt from the internal LLMAffiliate API.
    1157      *
    1158      * @return void
    1159      */
    1160879    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() ) {
    1166899            return;
    1167900        }
    1168901
    1169902        $norm = $this->get_normalized_request_path();
    1170         if ( ! $this->is_llms_txt_path( $norm ) ) {
     903        if ( ! $this->is_home_relative_path( $norm, $relative_path ) ) {
    1171904            return;
    1172905        }
    … …  
    1174907        $host = wp_parse_url( home_url(), PHP_URL_HOST );
    1175908        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 );
    1181914        $response = wp_remote_get( $api_url, array( 'timeout' => 15 ) );
    1182915
    … …  
    1187920
    1188921        if ( $text === '' ) {
    1189             $this->serve_llms_txt_fallback();
     922            call_user_func( $fallback );
    1190923            return;
    1191924        }
    … …  
    1193926        nocache_headers();
    1194927        header( 'Content-Type: text/plain; charset=UTF-8', true );
    1195         header( 'X-LLMAFFTM-Interceptor: llms-txt', true );
     928        header( 'X-LLMAFFTM-Interceptor: ' . $interceptor, true );
    1196929        header( 'Cache-Control: public, max-age=300', true );
    1197930
    … …  
    1201934    }
    1202935
    1203     /**
    1204      * Fallback llms.txt when the API is unreachable.
    1205      *
    1206      * @return void
    1207      */
    1208936    private function serve_llms_txt_fallback() {
    1209937        $host     = wp_parse_url( home_url(), PHP_URL_HOST );
    … …  
    1223951    }
    1224952
    1225     /**
    1226      * Serve /llms-full.txt from the internal LLMAffiliate API.
    1227      *
    1228      * @return void
    1229      */
    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 void
    1277      */
    1278953    private function serve_llms_full_txt_fallback() {
    1279954        $host     = wp_parse_url( home_url(), PHP_URL_HOST );
    … …  
    1292967    }
    1293968
    1294     /**
    1295      * Sitemap and favicon: no LLM detection, no request logging.
    1296      *
    1297      * @param string $normalized_path From get_normalized_request_path().
    1298      * @return bool
    1299      */
    1300969    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' ) ) {
    1302971            return true;
    1303972        }
    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' ) ) {
    1305977            return true;
    1306978        }
    … …  
    1320992        }
    1321993
    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__ ),
    13311003            array( 'jquery' ),
    13321004            LLMAFFTM_VERSION,
    13331005            true
    13341006        );
    1335         wp_enqueue_script( $admin_script_handle );
    13361007        wp_localize_script(
    1337             $admin_script_handle,
     1008            'llmafftm-admin',
    13381009            'llmafftmAdmin',
    13391010            array(
    13401011                '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' ),
    13451048                ),
    13461049            )
    13471050        );
    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
    13521053    private function generate_token($force_onboarding = false) {
    13531054        $token = 'llmafftm_' . bin2hex(random_bytes(32));
    13541055        update_option($this->option_name, $token);
    1355        
    1356         // Reset onboarding success state when token is regenerated
     1056
    13571057        if ($force_onboarding) {
    13581058            delete_option($this->option_onboarding_success);
    13591059        }
    1360        
    1361         // Send to onboarding endpoint (only if not already successful or forced)
     1060
    13621061        $this->send_to_onboarding($token, $this->get_site_id(), home_url(), $force_onboarding);
    1363        
     1062
    13641063        return $token;
    13651064    }
    1366    
     1065
    13671066    private function generate_site_id() {
    13681067        $site_id = 'site_' . bin2hex(random_bytes(16));
    … …  
    13701069        return $site_id;
    13711070    }
    1372    
     1071
    13731072    private function get_token() {
    13741073        $token = get_option($this->option_name);
    13751074        return $token ? $token : null;
    13761075    }
    1377    
     1076
    13781077    private function get_site_id() {
    13791078        $site_id = get_option($this->option_site_id);
    … …  
    13841083    }
    13851084
    1386     /**
    1387      * Self-heal onboarding when local token/site_id exist but backend onboarding was never completed.
    1388      */
    13891085    private function maybe_reconcile_onboarding() {
    13901086        if ( get_option($this->option_onboarding_success) === 'yes' ) {
    … …  
    14021098        }
    14031099
    1404         // Reuse send_to_onboarding guard logic; do not force duplicate calls.
    14051100        $this->send_to_onboarding($token, $site_id, home_url(), false);
    14061101    }
    14071102
    1408     /**
    1409      * Whether request and suspicious-event logging is enabled.
    1410      *
    1411      * @return bool
    1412      */
    14131103    private function is_logging_enabled() {
    14141104        return (bool) get_option($this->option_enable_logging, true);
    … …  
    14441134    }
    14451135
    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 
    14721136    private function log_request_decision($data = array()) {
    1473         $current_url = $this->get_current_url();
     1137        $current_url = llmafftm_get_current_url();
    14741138        $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();
    14821140
    14831141        $entry = array_merge(
    14841142            array(
    14851143                '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),
    14871145                'url' => $this->truncate_log_text($current_url, 500),
    14881146                'method' => $this->truncate_log_text(strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) ), 16),
    … …  
    15091167                'sec_ch_ua' => $this->truncate_log_text( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_CH_UA'] ?? '' ) ), 300),
    15101168                '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']),
    15121169                'signals' => array(),
    15131170            ),
    … …  
    15461203        );
    15471204    }
    1548    
     1205
    15491206    private function send_to_onboarding($token, $site_id, $host, $force = false) {
    1550         // Check if already successfully sent (unless forced)
     1207
    15511208        if (!$force && get_option($this->option_onboarding_success) === 'yes') {
    15521209            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    … …  
    15561213            return true;
    15571214        }
    1558        
     1215
    15591216        $onboarding_url = 'https://api.llmaffiliate.com/api/v1/enhanced-content/onboarding/';
    1560        
     1217
    15611218        try {
    15621219            $response = wp_remote_post($onboarding_url, array(
    … …  
    15711228                )),
    15721229            ));
    1573            
     1230
    15741231            if (is_wp_error($response)) {
    15751232                $error_message = $response->get_error_message();
    … …  
    15801237                return false;
    15811238            }
    1582            
     1239
    15831240            $status_code = wp_remote_retrieve_response_code($response);
    1584            
     1241
    15851242            if ($status_code >= 200 && $status_code < 300) {
    1586                 // Mark as successful
     1243
    15871244                update_option($this->option_onboarding_success, 'yes');
    15881245                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    … …  
    15991256                return false;
    16001257            }
    1601            
     1258
    16021259        } catch (Exception $e) {
    16031260            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    … …  
    16141271        }
    16151272    }
    1616    
     1273
    16171274    private function is_onboarding_successful() {
    16181275        return get_option($this->option_onboarding_success) === 'yes';
    16191276    }
    1620    
     1277
    16211278    public function ajax_generate_token() {
    16221279        check_ajax_referer('llmafftm_generate_token', 'nonce');
    1623        
     1280
    16241281        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
    16281285        try {
    1629             // Check if token already exists
     1286
    16301287            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' )));
    16321289                return;
    16331290            }
    1634            
    1635             // Generate new token and force onboarding for first-time token creation.
     1291
    16361292            $new_token = $this->generate_token(true);
    1637            
     1293
    16381294            wp_send_json_success(array(
    16391295                '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' )
    16411297            ));
    16421298        } catch (Exception $e) {
    … …  
    16451301                error_log('[LLMAFFTM] Generate token error: ' . $e->getMessage());
    16461302            }
    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() )));
    16481304        } catch (Error $e) {
    16491305            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    … …  
    16511307                error_log('[LLMAFFTM] Generate token fatal error: ' . $e->getMessage());
    16521308            }
    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
    16571313    public function ajax_regenerate_token() {
    16581314        check_ajax_referer('llmafftm_regenerate_token', 'nonce');
    1659        
     1315
    16601316        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
    16641320        try {
    1665             // Force onboarding when regenerating token
     1321
    16661322            $new_token = $this->generate_token(true);
    1667            
     1323
    16681324            wp_send_json_success(array(
    16691325                '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' )
    16711327            ));
    16721328        } catch (Exception $e) {
    … …  
    16751331                error_log('[LLMAFFTM] Regenerate token error: ' . $e->getMessage());
    16761332            }
    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() )));
    16781334        } catch (Error $e) {
    16791335            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    … …  
    16811337                error_log('[LLMAFFTM] Regenerate token fatal error: ' . $e->getMessage());
    16821338            }
    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
    16871343    public function ajax_save_threshold() {
    16881344        check_ajax_referer('llmafftm_save_threshold', 'nonce');
    1689        
     1345
    16901346        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 1
    1696        
     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
    16971353        update_option('llmafftm_threshold', $threshold);
    1698        
    1699         // Update detector instance
     1354
    17001355        $this->detector->set_threshold($threshold);
    1701        
     1356
    17021357        wp_send_json_success(array(
    17031358            'threshold' => $threshold,
    1704             'message' => 'Threshold saved successfully'
     1359            'message' => __( 'Threshold saved successfully', 'llmaffiliate-ai-traffic-monetizer' )
    17051360        ));
    17061361    }
    1707    
     1362
    17081363    public function ajax_save_logging() {
    17091364        check_ajax_referer('llmafftm_save_logging', 'nonce');
    17101365        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';
    17141369        update_option($this->option_enable_logging, $enabled ? '1' : '0');
    17151370        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' ),
    17171372            'enable_logging' => $enabled
    17181373        ));
    17191374    }
    17201375
    1721     /**
    1722      * Save or clear the Google Search Console verification token from plugin settings.
    1723      *
    1724      * @return void
    1725      */
    17261376    public function ajax_save_gsc_verification() {
    17271377        check_ajax_referer( 'llmafftm_save_gsc_verification', 'nonce' );
    17281378
    17291379        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' ) ) );
    17311381        }
    17321382
    … …  
    17631413    public function ajax_clear_events() {
    17641414        check_ajax_referer('llmafftm_clear_events', 'nonce');
    1765        
     1415
    17661416        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
    17701420        delete_option('llmafftm_suspicious_events');
    17711421        delete_option($this->option_request_logs);
    1772        
     1422
    17731423        wp_send_json_success(array(
    1774             'message' => 'All logs cleared successfully'
     1424            'message' => __( 'All logs cleared successfully', 'llmaffiliate-ai-traffic-monetizer' )
    17751425        ));
    17761426    }
    1777    
     1427
    17781428    public function add_admin_menu() {
    17791429        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' ),
    17821432            'manage_options',
    17831433            'llmaffiliate-ai-traffic-monetizer',
    … …  
    17851435        );
    17861436    }
    1787    
     1437
    17881438    public function admin_page() {
    17891439        $token = $this->get_token();
    … …  
    17941444        $onboarding_success = $this->is_onboarding_successful();
    17951445        $has_token = !empty($token);
    1796        
    1797         // Load threshold into detector
     1446
    17981447        $threshold = get_option('llmafftm_threshold', 0.6);
    17991448        $this->detector->set_threshold($threshold);
    … …  
    18031452                <h1 class="llmafftm-title">
    18041453                    <span class="llmafftm-icon">🤖</span>
    1805                     LLMAffiliate AI Traffic Monetizer
     1454                    <?php esc_html_e( 'LLMAffiliate AI Traffic Monetizer', 'llmaffiliate-ai-traffic-monetizer' ); ?>
    18061455                </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>
    18081457            </div>
    1809            
     1458
    18101459            <div class="llmafftm-status-card">
    18111460                <div class="llmafftm-status-indicator">
    18121461                    <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>
    18141463                </div>
    18151464                <div class="llmafftm-service-info">
    1816                     <strong>Content Service:</strong>
     1465                    <strong><?php esc_html_e( 'Content Service:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong>
    18171466                    <code>https://api.llmaffiliate.com</code>
    18181467                </div>
    18191468                <div class="llmafftm-onboarding-status">
    1820                     <strong>Onboarding:</strong>
     1469                    <strong><?php esc_html_e( 'Onboarding:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong>
    18211470                    <?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>
    18231472                    <?php else: ?>
    1824                         <span style="color: orange;">⏳ Pending</span>
     1473                        <span style="color: orange;">⏳ <?php esc_html_e( 'Pending', 'llmaffiliate-ai-traffic-monetizer' ); ?></span>
    18251474                    <?php endif; ?>
    18261475                </div>
    … …  
    18801529                </div>
    18811530            </div>
    1882            
     1531
    18831532            <div class="llmafftm-token-section">
    18841533                <div class="llmafftm-card">
    18851534                    <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>
    18881537        </div>
    1889                    
     1538
    18901539                    <?php if ($has_token): ?>
    18911540                        <div class="llmafftm-token-display">
    18921541                            <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); ?>"
    18981547                                    readonly
    18991548                                >
    1900                                 <button 
    1901                                     type="button" 
    1902                                     class="llmafftm-copy-btn" 
     1549                                <button
     1550                                    type="button"
     1551                                    class="llmafftm-copy-btn"
    19031552                                    data-copy-target="llmafftm-token-input"
    1904                                     title="Copy to clipboard"
     1553                                    title="<?php echo esc_attr__( 'Copy to clipboard', 'llmaffiliate-ai-traffic-monetizer' ); ?>"
    19051554                                >
    19061555                                    <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>
    19081557                                </button>
    19091558                            </div>
    19101559                            <div class="llmafftm-token-actions">
    1911                                 <button 
    1912                                     type="button" 
     1560                                <button
     1561                                    type="button"
    19131562                                    class="llmafftm-regenerate-btn button button-secondary"
    19141563                                    data-nonce="<?php echo esc_attr($regenerate_nonce); ?>"
    19151564                                >
    1916                                     🔄 Regenerate Token
     1565                                    🔄 <?php esc_html_e( 'Regenerate Token', 'llmaffiliate-ai-traffic-monetizer' ); ?>
    19171566                                </button>
    19181567                            </div>
    19191568                        </div>
    1920                        
     1569
    19211570                        <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>
    19231572                        </div>
    19241573                    <?php else: ?>
    19251574                        <div class="llmafftm-no-token">
    19261575                            <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"
    19311580                                class="llmafftm-generate-btn button button-primary button-large"
    19321581                                data-nonce="<?php echo esc_attr($generate_nonce); ?>"
    19331582                            >
    1934                                 ✨ Generate Token
     1583                                ✨ <?php esc_html_e( 'Generate Token', 'llmaffiliate-ai-traffic-monetizer' ); ?>
    19351584                            </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>
    19371586                        </div>
    19381587                    <?php endif; ?>
    19391588                </div>
    19401589            </div>
    1941            
     1590
    19421591            <div class="llmafftm-site-info-section">
    19431592                <div class="llmafftm-card">
    19441593                    <div class="llmafftm-card-header">
    1945                         <h2>🌐 Site Information</h2>
     1594                        <h2>🌐 <?php esc_html_e( 'Site Information', 'llmaffiliate-ai-traffic-monetizer' ); ?></h2>
    19461595                    </div>
    19471596                    <div class="llmafftm-info-grid">
    19481597                        <div class="llmafftm-info-item">
    1949                             <label>Site ID:</label>
     1598                            <label><?php esc_html_e( 'Site ID:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    19501599                            <code class="llmafftm-code-block"><?php echo esc_html($site_id); ?></code>
    19511600                        </div>
    19521601                        <div class="llmafftm-info-item">
    1953                             <label>Site URL:</label>
     1602                            <label><?php esc_html_e( 'Site URL:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    19541603                            <code class="llmafftm-code-block"><?php echo esc_html($site_url); ?></code>
    19551604                        </div>
    … …  
    19571606                </div>
    19581607            </div>
    1959            
     1608
    19601609            <div class="llmafftm-setup-section">
    19611610                <div class="llmafftm-card">
    19621611                    <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>
    19651614                    </div>
    1966                    
     1615
    19671616                    <div class="llmafftm-steps">
    19681617                        <div class="llmafftm-step">
    19691618                            <div class="llmafftm-step-number">1</div>
    19701619                            <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>
    19741631                            </div>
    19751632                        </div>
    1976                        
     1633
    19771634                        <div class="llmafftm-step">
    19781635                            <div class="llmafftm-step-number">2</div>
    19791636                            <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>
    19821639                            </div>
    19831640                        </div>
    1984                        
     1641
    19851642                        <div class="llmafftm-step">
    19861643                            <div class="llmafftm-step-number">3</div>
    19871644                            <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>
    19901647                                <?php if ($has_token): ?>
    19911648                                    <div class="llmafftm-credentials-box">
    19921649                                        <div class="llmafftm-credential-item">
    1993                                             <label>🔑 API Token:</label>
     1650                                            <label>🔑 <?php esc_html_e( 'API Token:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    19941651                                            <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"
    19981655                                                data-copy-text="<?php echo esc_attr($token); ?>"
    1999                                                 title="Copy token"
     1656                                                title="<?php echo esc_attr__( 'Copy token', 'llmaffiliate-ai-traffic-monetizer' ); ?>"
    20001657                                            >
    20011658                                                📋
    … …  
    20031660                                        </div>
    20041661                                        <div class="llmafftm-credential-item">
    2005                                             <label>🌐 Site ID:</label>
     1662                                            <label>🌐 <?php esc_html_e( 'Site ID:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    20061663                                            <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"
    20101667                                                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' ); ?>"
    20121669                                            >
    20131670                                                📋
    … …  
    20151672                                        </div>
    20161673                                        <div class="llmafftm-credential-item">
    2017                                             <label>🔗 Site URL:</label>
     1674                                            <label>🔗 <?php esc_html_e( 'Site URL:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    20181675                                            <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"
    20221679                                                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' ); ?>"
    20241681                                            >
    20251682                                                📋
    … …  
    20301687                                    <div class="llmafftm-credentials-box">
    20311688                                        <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>
    20341691                                        </div>
    20351692                                        <div class="llmafftm-credential-item">
    2036                                             <label>🌐 Site ID:</label>
     1693                                            <label>🌐 <?php esc_html_e( 'Site ID:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    20371694                                            <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"
    20411698                                                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' ); ?>"
    20431700                                            >
    20441701                                                📋
    … …  
    20461703                                        </div>
    20471704                                        <div class="llmafftm-credential-item">
    2048                                             <label>🔗 Site URL:</label>
     1705                                            <label>🔗 <?php esc_html_e( 'Site URL:', 'llmaffiliate-ai-traffic-monetizer' ); ?></label>
    20491706                                            <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"
    20531710                                                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' ); ?>"
    20551712                                            >
    20561713                                                📋
    … …  
    20611718                            </div>
    20621719                        </div>
    2063                        
     1720
    20641721                        <div class="llmafftm-step">
    20651722                            <div class="llmafftm-step-number">4</div>
    20661723                            <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>
    20701727                            </div>
    20711728                        </div>
    20721729                    </div>
    2073                    
     1730
    20741731                    <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>
    20771742                    </div>
    20781743                </div>
    20791744            </div>
    2080            
     1745
    20811746            <div class="llmafftm-detection-section">
    20821747                <div class="llmafftm-card">
    20831748                    <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>
    20861751                    </div>
    2087                    
     1752
    20881753        <?php
    20891754                    $current_threshold = get_option('llmafftm_threshold', 0.6);
    20901755                    $enable_logging = (bool) get_option('llmafftm_enable_logging', true);
    2091                     $suspicious_events = get_option('llmafftm_suspicious_events', array());
    2092                     $events_count = count($suspicious_events);
    20931756                    $request_logs = get_option($this->option_request_logs, array());
    20941757                    $request_logs_count = count($request_logs);
     1758                    delete_option( 'llmafftm_suspicious_events' );
    20951759                    ?>
    2096                    
     1760
    20971761                    <div class="llmafftm-logging-setting" style="margin-bottom: 24px; padding: 12px; background: #f9f9f9; border-radius: 6px;">
    20981762                        <label>
    20991763                            <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>
    21011765                        </label>
    21021766                        <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' ); ?>
    21041768                        </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>
    21061770                    </div>
    2107                    
     1771
    21081772                    <div class="llmafftm-threshold-setting">
    21091773                        <label for="llmafftm-threshold">
    2110                             <strong>Detection Threshold:</strong>
     1774                            <strong><?php esc_html_e( 'Detection Threshold:', 'llmaffiliate-ai-traffic-monetizer' ); ?></strong>
    21111775                            <span id="llmafftm-threshold-value"><?php echo esc_html($current_threshold); ?></span>
    21121776                        </label>
    21131777                        <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' ); ?>
    21161779                        </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"
    21231786                            value="<?php echo esc_attr($current_threshold); ?>"
    21241787                            style="width: 100%; margin: 10px 0;"
    21251788                        >
    2126                         <button 
    2127                             type="button" 
    2128                             class="button button-primary" 
     1789                        <button
     1790                            type="button"
     1791                            class="button button-primary"
    21291792                            id="llmafftm-save-threshold"
    21301793                        >
    2131                             Save Threshold
     1794                            <?php esc_html_e( 'Save Threshold', 'llmaffiliate-ai-traffic-monetizer' ); ?>
    21321795                        </button>
    21331796                    </div>
    2134                    
     1797
    21351798                    <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
    21391802                        <?php if ($request_logs_count > 0): ?>
    21401803                            <div class="llmafftm-events-table-wrapper">
    … …  
    21421805                                    <thead>
    21431806                                        <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>
    21551818                                        </tr>
    21561819                                    </thead>
    21571820                                    <tbody>
    2158                                         <?php 
     1821                                        <?php
    21591822                                        $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):
    21611824                                        ?>
    21621825                                            <tr>
    … …  
    22111874                                </table>
    22121875                            </div>
    2213                             <button 
    2214                                 type="button" 
    2215                                 class="button button-secondary" 
     1876                            <button
     1877                                type="button"
     1878                                class="button button-secondary"
    22161879                                id="llmafftm-clear-events"
    22171880                            >
    2218                                 Clear All Logs
     1881                                <?php esc_html_e( 'Clear All Logs', 'llmaffiliate-ai-traffic-monetizer' ); ?>
    22191882                            </button>
    22201883                        <?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>
    22791885                        <?php endif; ?>
    22801886                    </div>
    22811887                </div>
    22821888            </div>
    2283            
     1889
    22841890            <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>
    22861892            </div>
    22871893        </div>
    … …  
    22891895    }
    22901896
    2291     /**
    2292      * Admin settings UI behaviors (threshold, logging, clear events). Loaded via wp_add_inline_script().
    2293      *
    2294      * @return string
    2295      */
    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: enabled
    2350             },
    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.clearEvents
    2381             },
    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: val
    2410             },
    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 <?php
    2440         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, while
    2928            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 it
    2945            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 a
    2955            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 functionality
    2970             $('.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 functionality
    2999             $('.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 copy
    3006                 \$input.select();
    3007                 document.execCommand('copy');
    3008                
    3009                 // Visual feedback
    3010                 \$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 token
    3022             $('.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 page
    3043                             $('.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 button
    3049                             \$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 input
    3068                 var \$temp = $('<input>');
    3069                 $('body').append(\$temp);
    3070                 \$temp.val(textToCopy).select();
    3071                 document.execCommand('copy');
    3072                 \$temp.remove();
    3073                
    3074                 // Visual feedback
    3075                 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    
    30861897    public function maybe_intercept_request() {
    3087         $current_url = $this->get_current_url();
     1898        $current_url = llmafftm_get_current_url();
    30881899        $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();
    30961901
    30971902        if ( $this->should_bypass_llm_interception( $normalized_path ) ) {
    … …  
    31081913        );
    31091914
    3110         // Check if it's admin or API
    31111915        if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
    31121916            $this->log_request_decision(array_merge($base_log_context, array(
    … …  
    31161920            return;
    31171921        }
    3118        
    3119         // Check method
     1922
    31201923        $method = strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) );
    31211924        if ($method !== 'GET') {
    … …  
    31271930        }
    31281931
    3129         // Always serve /ai-terms page to both humans and LLMs (highest priority)
    31301932        if (strpos($current_url, '/ai-terms') !== false) {
    31311933            $this->log_request_decision(array_merge($base_log_context, array(
    … …  
    31361938            return;
    31371939        }
    3138        
     1940
    31391941        $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
    31401942
    … …  
    31481950        }
    31491951
    3150         // Explicit User-Agent whitelist (GPTBot, bare "Google"/Gemini, etc.): bypass the
    3151         // numeric threshold entirely so Gemini-style UAs stay enhanced at 0.6+ tuning.
    31521952        $match_details = $this->detect_llm_details($user_agent);
    31531953        $llm_profile   = $match_details['profile'];
    … …  
    31721972        }
    31731973
    3174         // Advanced detection: weighted score path (threshold applies only here).
    31751974        $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
    31881976        $should_serve_enhanced = false;
    31891977        $confidence = 0.5;
    3190        
     1978
    31911979        if ($detection_result['is_llm_like']) {
    3192             // Detected as LLM-like via scoring
     1980
    31931981            $should_serve_enhanced = true;
    31941982            $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
    32051985        $detection_reasons = isset( $detection_result['reasons'] ) && is_array( $detection_result['reasons'] )
    32061986            ? $detection_result['reasons']
    … …  
    32081988
    32091989        if ($should_serve_enhanced) {
    3210             // Use detected profile or fallback to generic-llm
     1990
    32111991            $final_profile = $llm_profile ?: 'generic-llm';
    32121992            $score_reason = in_array( 'datacenter_cold_fetch', $detection_reasons, true )
    … …  
    32482028        }
    32492029    }
    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
    32752031    private function detect_llm_details($user_agent) {
    3276         // Gemini citation fetches sometimes send bare "Google" as User-Agent (any case).
     2032
    32772033        if ( preg_match( '/^\s*Google\s*$/i', $user_agent ) ) {
    32782034            return array(
    … …  
    33052061            ],
    33062062            '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
    33112064                '/Perplexity-User\/(\d+\.\d+)/i', '/Perplexity-User/i',
    33122065                '/PerplexityBot\/(\d+\.\d+)/i', '/PerplexityBot/i',
    … …  
    33252078            ],
    33262079        ];
    3327        
     2080
    33282081        foreach ($llm_patterns as $profile => $patterns) {
    33292082            foreach ($patterns as $pattern) {
    … …  
    33372090            }
    33382091        }
    3339        
     2092
    33402093        return array(
    33412094            'profile' => false,
    … …  
    33452098    }
    33462099
    3347     private function detect_llm($user_agent) {
    3348         $details = $this->detect_llm_details($user_agent);
    3349         return $details['profile'];
    3350     }
    3351    
    33522100    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
    33662105    private function detect_google_bot($user_agent) {
    33672106        $google_patterns = [
    … …  
    33802119            '/mozilla.*googlebot/i',
    33812120        ];
    3382        
     2121
    33832122        foreach ($google_patterns as $pattern) {
    33842123            if (preg_match($pattern, $user_agent)) {
    … …  
    33862125            }
    33872126        }
    3388        
     2127
    33892128        return false;
    33902129    }
    3391    
     2130
    33922131    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
    33962134        $mapped_profile = $this->map_llm_profile($llm_profile);
    3397        
     2135
    33982136        $response = null;
    33992137        foreach ( $this->integration_url_lookup_candidates( $url ) as $candidate_url ) {
    … …  
    34192157            }
    34202158        }
    3421        
     2159
    34222160        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) == 200) {
    34232161            $body = wp_remote_retrieve_body($response);
    … …  
    34302168            )));
    34312169
    3432             // Strip robots noindex/nofollow <meta> tags from the body so they
    3433             // never leak into the enhanced page that we serve to the visiting
    3434             // LLM client. (Mirrors stripRobotsNoindexDirectives() in CF_worker.js.)
    34352170            $body = $this->strip_robots_noindex_directives_from_html( $body );
    34362171
    3437             // Clear any potentially-leaked compression/length headers BEFORE we
    3438             // emit our own. wp_remote_retrieve_body() has already decompressed
    3439             // the upstream payload, but a Content-Encoding / Content-Length /
    3440             // Transfer-Encoding header from the *compressed* upstream response
    3441             // can still end up on our outgoing response if any layer
    3442             // (mod_php / FastCGI / a caching plugin / the web server) decided
    3443             // to forward it. Forwarding them as-is causes some LLM-side
    3444             // fetchers — most notably Perplexity — to try to gunzip a
    3445             // plain-text body and fail with TLS-like / "request failed in this
    3446             // environment" errors.
    34472172            if ( ! headers_sent() ) {
    34482173                header_remove( 'Content-Encoding' );
    … …  
    34502175                header_remove( 'Transfer-Encoding' );
    34512176
    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.
    34552177                if ( function_exists( 'headers_list' ) ) {
    34562178                    foreach ( headers_list() as $sent_header ) {
    … …  
    34642186            }
    34652187
    3466             // Headers for enhanced content. Vary on every detection-affecting
    3467             // request header so an intermediary / CDN does not serve enhanced
    3468             // HTML to a human visitor and vice-versa.
    34692188            header( 'Content-Type: text/html; charset=UTF-8' );
    34702189            header( 'X-LLMAFFTM-Interceptor: enhanced' );
    … …  
    34732192            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' );
    34742193
    3475             // Escape late: run the proxied HTML document through wp_kses() with a
    3476             // full-document allow-list. This is a complete HTML page from our
    3477             // trusted HTTPS content API, so wp_kses_post() is not appropriate
    3478             // (it strips <html>/<head>/<meta>/<link>/<body>/<title>).
    34792194            echo wp_kses( $body, $this->get_allowed_full_html() );
    34802195            exit;
    34812196        } else {
    3482             // Log error for debugging
     2197
    34832198            $error_message = '';
    34842199            $status_code = null;
    … …  
    35102225                'service_error' => $error_message,
    35112226            )));
    3512            
    3513             // Fallback - add fallback header
     2227
    35142228            header('X-LLMAFFTM-Interceptor: fallback');
    35152229        }
    35162230    }
    3517    
    3518     /**
    3519      * Serve the AI Terms of Use page
    3520      * This page is always served to both humans and LLMs
    3521      */
     2231
    35222232    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
    35272236        if (file_exists($ai_terms_file)) {
    35282237            require_once $ai_terms_file;
    3529            
    3530             // Get the HTML content
     2238
    35312239            $html = llmafftm_get_ai_terms_html(home_url());
    3532            
    3533             // Set appropriate headers
     2240
    35342241            header('Content-Type: text/html; charset=UTF-8');
    35352242            header('X-LLMAFFTM-Interceptor: ai-terms-page');
    35362243            header('Cache-Control: public, max-age=3600');
    35372244            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
    35432246            echo wp_kses( $html, $this->get_allowed_full_html() );
    35442247            exit;
    35452248        } else {
    35462249            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.
    35542251                error_log( '[LLMAFFTM] AI Terms page file not found: ' . $ai_terms_file );
    35552252            }
    … …  
    35612258    }
    35622259
    3563     /**
    3564      * Allow-list of HTML tags/attributes for wp_kses() when rendering full
    3565      * HTML documents (the proxied enhanced-content response and our own
    3566      * standalone /ai-terms page). This is intentionally broad because we are
    3567      * serving complete HTML pages, not post content; wp_kses_post() would
    3568      * 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 extend
    3573      * it (for example, if the enhanced-content service starts emitting new
    3574      * 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 the
    3588      * enhanced-content body returned by the LLMAffiliate render API before
    3589      * 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 layer
    3591      * 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      */
    35992260    private function strip_robots_noindex_directives_from_html( $html ) {
    36002261        if ( ! is_string( $html ) || $html === '' ) {
    … …  
    36022263        }
    36032264
    3604         // Match "<meta name='robots' content='...noindex|nofollow...'>".
    36052265        $pattern_name_first = '#<meta\s+[^>]*\bname\s*=\s*["\']robots["\'][^>]*\bcontent\s*=\s*["\'][^"\']*(?:noindex|nofollow)[^"\']*["\'][^>]*>\s*#i';
    36062266        $cleaned = preg_replace( $pattern_name_first, '', $html );
    … …  
    36092269        }
    36102270
    3611         // Match the reversed-attribute variant: "<meta content='...' name='robots'>".
    36122271        $pattern_content_first = '#<meta\s+[^>]*\bcontent\s*=\s*["\'][^"\']*(?:noindex|nofollow)[^"\']*["\'][^>]*\bname\s*=\s*["\']robots["\'][^>]*>\s*#i';
    36132272        $cleaned_again = preg_replace( $pattern_content_first, '', $cleaned );
    … …  
    37122371        );
    37132372
    3714         /**
    3715          * Filters the allow-list of tags/attributes used by wp_kses() when rendering
    3716          * full HTML documents (the proxied enhanced-content response and the
    3717          * standalone /ai-terms page).
    3718          *
    3719          * @param array<string, array<string, bool>> $allowed Associative map of tag names to allowed attributes.
    3720          */
    37212373        return apply_filters( 'llmafftm_allowed_full_html', $allowed );
    37222374    }
    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
    37302376    private function integration_url_lookup_candidates( $url ) {
    37312377        $url = trim( (string) $url );
    … …  
    37432389    }
    37442390
    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      */
    37512391    private function toggle_url_trailing_slash( $url ) {
    37522392        $parts = wp_parse_url( $url );
    … …  
    37762416    }
    37772417
    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 on
    3787      * public frontend page renders. Skipped for admin / AJAX / REST / feed
    3788      * responses so the token only appears where Search Console actually
    3789      * 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 void
    3803      */
    38042418    public function output_google_site_verification_meta() {
    38052419        if ( is_admin() || wp_doing_ajax() || is_feed() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
    … …  
    38072421        }
    38082422
    3809         /**
    3810          * Value saved in plugin settings (optional), then passed through this filter
    3811          * 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          */
    38152423        $stored = get_option( $this->option_gsc_verification, '' );
    38162424        $stored = is_string( $stored ) ? trim( $stored ) : '';
    … …  
    38252433    }
    38262434
    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      */
    38332435    private function get_monetized_paths_for_robots_txt() {
    38342436        $host = wp_parse_url( home_url(), PHP_URL_HOST );
    … …  
    38432445        }
    38442446
    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 );
    38472448
    38482449        $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;
    38652461            }
    38662462        }
    … …  
    38742470    }
    38752471
    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 not
    3883      * 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      */
    38892472    public function filter_robots_txt( $output, $public ) {
    38902473        if ( ! $public ) {
    … …  
    38922475        }
    38932476
     2477        return $this->build_robots_txt();
     2478    }
     2479
     2480    private function build_robots_txt() {
    38942481        $paths = $this->get_monetized_paths_for_robots_txt();
    38952482
    38962483        $default_user_agents = array(
    38972484            '*',
    3898             // OpenAI.
     2485
    38992486            'GPTBot',
    39002487            'OAI-SearchBot',
    39012488            'ChatGPT-User',
    3902             // Anthropic.
     2489
    39032490            'ClaudeBot',
    39042491            'anthropic-ai',
    3905             // Google.
     2492
    39062493            'Googlebot',
    39072494            'Google-Extended',
    … …  
    39112498            'GoogleOther-Image',
    39122499            'GoogleOther-Video',
    3913             // Perplexity.
     2500
    39142501            'PerplexityBot',
    39152502            'Perplexity-User',
    3916             // Microsoft / Bing.
     2503
    39172504            'Bingbot',
    39182505            'msnbot',
    3919             // Apple.
     2506
    39202507            'Applebot',
    39212508            'Applebot-Extended',
    3922             // Meta.
     2509
    39232510            'Meta-ExternalAgent',
    39242511            'FacebookBot',
    3925             // Amazon.
     2512
    39262513            'Amazonbot',
    3927             // Other AI crawlers.
     2514
    39282515            'cohere-ai',
    39292516            'Bytespider',
    … …  
    39332520        );
    39342521
    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          */
    39402522        $user_agents = apply_filters( 'llmafftm_robots_allowed_user_agents', $default_user_agents );
    39412523        if ( ! is_array( $user_agents ) ) {
    … …  
    39792561}
    39802562
    3981 /**
    3982  * Cleanup plugin options on uninstall (true delete from Plugins screen).
    3983  */
    39842563function llmafftm_uninstall_plugin() {
    39852564    $option_keys = array(
    … …  
    39932572        'llmafftm_gsc_verification_token',
    39942573        'llmafftm_legacy_options_migrated',
    3995         // Legacy keys for cleanup parity.
     2574
    39962575        'llm_interceptor_token',
    39972576        'llm_interceptor_site_id',
    … …  
    40092588register_uninstall_hook(__FILE__, 'llmafftm_uninstall_plugin');
    40102589
    4011 // Initialize plugin
    40122590llmafftm_migrate_legacy_options();
    40132591new LLMAFFTM_Plugin();
  • llmaffiliate-ai-traffic-monetizer/trunk/readme.txt

    r3624563 r3652650  
    33Tags: ai, llm, chatbot, crawler, content delivery
    44Requires at least: 6.0
    5 Tested up to: 7.0
     5Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.10
     7Stable tag: 1.0.11
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2323- Serves enhanced HTML responses via a remote content service.
    2424- 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.
    2726
    2827= HOW IT WORKS =
    … …  
    4847= OPERATIONAL LOGGING & PRIVACY =
    4948
    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 security logs. These may include:
     49An 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:
    5150* IP address
    5251* User agent
    … …  
    5554* Selected HTTP headers
    5655* Detection and decision metadata
    57 * Suspicious-event or request logs
     56* Request decision logs
    5857
    5958This 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.
    … …  
    8281
    8382    * **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.
    8584    * **Data sent (JSON body):**
    8685        * `token` – the authentication token generated by the plugin
    … …  
    9796        * `llm_profile` – the detected LLM profile name (e.g., `chatgpt`, `claude`, `gemini`)
    9897        * `User-Agent` header – forwarded from the visiting LLM client
    99         * `Accept-Language` header – forwarded from the visiting LLM client
    10098        * `X-LLM-Profile` header – the plugin's detected profile
    10199        * `X-LLM-Confidence` header – a numeric confidence score (0–1) from the local detector
    … …  
    105103
    106104    * **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:** Only 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).
    108106    * **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).
    109107
    … …  
    162160The plugin logs the failure (if enabled), adds an `X-LLMAFFTM-Interceptor: fallback` header, and allows WordPress to render normally.
    163161
    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 
    173162= Is this plugin safe for SEO? =
    174163Yes. Googlebot and other search crawlers always receive original content, ensuring compliance with search engine guidelines.
    … …  
    181170
    182171== 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.
    183180
    184181= 1.0.10 =
    … …  
    222219* 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`.
    223220* 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 
    225221= 1.0.6 =
    226222* 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.