array( 'reliability' => 95, 'speed' => 100, 'coverage' => 90, 'method' => 'header_based' ), 'ip_api' => array( 'reliability' => 85, 'speed' => 80, 'coverage' => 95, 'method' => 'api_based' ), 'geojs' => array( 'reliability' => 80, 'speed' => 75, 'coverage' => 85, 'method' => 'api_based' ), 'accept_language' => array( 'reliability' => 60, 'speed' => 100, 'coverage' => 100, 'method' => 'fallback' ), 'timezone' => array( 'reliability' => 70, 'speed' => 100, 'coverage' => 80, 'method' => 'client_based' ) ); /** * GDPR territories with accuracy requirements */ private $gdpr_territories = array( // EU Member States 'AT' => array('name' => 'Austria', 'confidence_required' => 90), 'BE' => array('name' => 'Belgium', 'confidence_required' => 90), 'BG' => array('name' => 'Bulgaria', 'confidence_required' => 90), 'HR' => array('name' => 'Croatia', 'confidence_required' => 90), 'CY' => array('name' => 'Cyprus', 'confidence_required' => 90), 'CZ' => array('name' => 'Czech Republic', 'confidence_required' => 90), 'DK' => array('name' => 'Denmark', 'confidence_required' => 90), 'EE' => array('name' => 'Estonia', 'confidence_required' => 90), 'FI' => array('name' => 'Finland', 'confidence_required' => 90), 'FR' => array('name' => 'France', 'confidence_required' => 90), 'DE' => array('name' => 'Germany', 'confidence_required' => 95), // High accuracy for major economy 'GR' => array('name' => 'Greece', 'confidence_required' => 90), 'HU' => array('name' => 'Hungary', 'confidence_required' => 90), 'IE' => array('name' => 'Ireland', 'confidence_required' => 90), 'IT' => array('name' => 'Italy', 'confidence_required' => 90), 'LV' => array('name' => 'Latvia', 'confidence_required' => 90), 'LT' => array('name' => 'Lithuania', 'confidence_required' => 90), 'LU' => array('name' => 'Luxembourg', 'confidence_required' => 90), 'MT' => array('name' => 'Malta', 'confidence_required' => 90), 'NL' => array('name' => 'Netherlands', 'confidence_required' => 90), 'PL' => array('name' => 'Poland', 'confidence_required' => 90), 'PT' => array('name' => 'Portugal', 'confidence_required' => 90), 'RO' => array('name' => 'Romania', 'confidence_required' => 90), 'SK' => array('name' => 'Slovakia', 'confidence_required' => 90), 'SI' => array('name' => 'Slovenia', 'confidence_required' => 90), 'ES' => array('name' => 'Spain', 'confidence_required' => 90), 'SE' => array('name' => 'Sweden', 'confidence_required' => 90), // EEA Countries 'IS' => array('name' => 'Iceland', 'confidence_required' => 85), 'LI' => array('name' => 'Liechtenstein', 'confidence_required' => 85), 'NO' => array('name' => 'Norway', 'confidence_required' => 85), // Special Cases 'CH' => array('name' => 'Switzerland', 'confidence_required' => 80), // Adequacy decision 'GB' => array('name' => 'United Kingdom', 'confidence_required' => 85), // Post-Brexit GDPR ); /** * Other privacy law territories */ private $privacy_territories = array( 'US' => array( 'states' => array( 'CA' => array('law' => 'CCPA', 'confidence_required' => 85), 'VA' => array('law' => 'VCDPA', 'confidence_required' => 80), 'CO' => array('law' => 'CPA', 'confidence_required' => 80), 'CT' => array('law' => 'CTDPA', 'confidence_required' => 80), ) ), 'BR' => array('law' => 'LGPD', 'confidence_required' => 80), 'CA' => array('law' => 'PIPEDA', 'confidence_required' => 75), 'AU' => array('law' => 'Privacy Act', 'confidence_required' => 75), 'JP' => array('law' => 'APPI', 'confidence_required' => 75), 'SG' => array('law' => 'PDPA', 'confidence_required' => 75), ); /** * Detection cache */ private $detection_cache = array(); /** * Get instance */ public static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /** * Constructor */ private function __construct() { $this->init_advanced_detection(); } /** * Initialize advanced geographic detection */ private function init_advanced_detection() { // Hook into WordPress add_action('init', array($this, 'detect_visitor_location_advanced')); add_action('wp_ajax_tigerstyle_whiskers_update_geo', array($this, 'update_client_geo_data')); add_action('wp_ajax_nopriv_tigerstyle_whiskers_update_geo', array($this, 'update_client_geo_data')); // Schedule periodic cache cleanup if (!wp_next_scheduled('tigerstyle_whiskers_geo_cache_cleanup')) { wp_schedule_event(time(), 'daily', 'tigerstyle_whiskers_geo_cache_cleanup'); } add_action('tigerstyle_whiskers_geo_cache_cleanup', array($this, 'cleanup_geo_cache')); if (defined('WP_DEBUG') && WP_DEBUG) { error_log('TigerStyle Whiskers: Advanced geo-detection whiskers are sensing with precision!'); } } /** * Detect visitor location using multiple sources */ public function detect_visitor_location_advanced() { $visitor_ip = $this->get_visitor_ip(); $cache_key = 'tigerstyle_whiskers_geo_' . hash('sha256', $visitor_ip); // Check cache first (cats remember their territory) $cached_result = $this->get_cached_detection($cache_key); if ($cached_result && $this->is_cache_valid($cached_result)) { $this->detection_cache = $cached_result; return $cached_result; } // Multi-source detection with feline precision $detection_results = array(); // Primary sources (fast and reliable) $detection_results['cloudflare'] = $this->detect_via_cloudflare(); $detection_results['headers'] = $this->detect_via_headers(); // Secondary sources (API-based, may be slower) if ($this->should_use_api_detection()) { $detection_results['ip_api'] = $this->detect_via_ip_api($visitor_ip); $detection_results['geojs'] = $this->detect_via_geojs($visitor_ip); } // Fallback sources (always available) $detection_results['accept_language'] = $this->detect_via_accept_language(); $detection_results['timezone'] = $this->detect_via_timezone(); // Calculate consensus with confidence scoring $final_result = $this->calculate_consensus($detection_results); // Cache the result (cats remember successful hunts) $this->cache_detection($cache_key, $final_result); // Store in instance cache $this->detection_cache = $final_result; // Log the detection for audit $this->log_detection_event($final_result, $detection_results); return $final_result; } /** * Detect via CloudFlare headers (most reliable when available) */ private function detect_via_cloudflare() { $result = array( 'provider' => 'cloudflare', 'country_code' => null, 'confidence' => 0, 'method' => 'header', 'timestamp' => time() ); if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) { $country = strtoupper($_SERVER['HTTP_CF_IPCOUNTRY']); if ($country !== 'XX' && strlen($country) === 2) { $result['country_code'] = $country; $result['confidence'] = 95; // CloudFlare is highly reliable // Additional CloudFlare data if available if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) { $result['original_ip'] = hash('sha256', $_SERVER['HTTP_CF_CONNECTING_IP']); } } } return $result; } /** * Detect via other headers */ private function detect_via_headers() { $result = array( 'provider' => 'headers', 'country_code' => null, 'confidence' => 0, 'method' => 'header', 'timestamp' => time() ); // Check for other proxy headers $headers_to_check = array( 'HTTP_X_COUNTRY_CODE', 'HTTP_X_GEOIP_COUNTRY', 'HTTP_GEOIP_COUNTRY_CODE' ); foreach ($headers_to_check as $header) { if (isset($_SERVER[$header])) { $country = strtoupper($_SERVER[$header]); if (strlen($country) === 2) { $result['country_code'] = $country; $result['confidence'] = 80; // Good but less reliable than CloudFlare $result['header_used'] = $header; break; } } } return $result; } /** * Detect via IP-API.com (free API with rate limits) */ private function detect_via_ip_api($ip) { $result = array( 'provider' => 'ip_api', 'country_code' => null, 'confidence' => 0, 'method' => 'api', 'timestamp' => time() ); // Check rate limiting if (!$this->can_make_api_request('ip_api')) { return $result; } $api_url = "http://ip-api.com/json/{$ip}?fields=status,country,countryCode,region,regionName,city,timezone,query"; $response = wp_remote_get($api_url, array( 'timeout' => 3, 'user-agent' => 'TigerStyle-Whiskers/' . TIGERSTYLE_WHISKERS_VERSION )); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $data = json_decode(wp_remote_retrieve_body($response), true); if ($data && $data['status'] === 'success') { $result['country_code'] = strtoupper($data['countryCode']); $result['confidence'] = 85; $result['additional_data'] = array( 'country_name' => $data['country'], 'region' => $data['region'], 'region_name' => $data['regionName'], 'city' => $data['city'], 'timezone' => $data['timezone'] ); // Update rate limiting $this->update_api_rate_limit('ip_api'); } } return $result; } /** * Detect via GeoJS (alternative API) */ private function detect_via_geojs($ip) { $result = array( 'provider' => 'geojs', 'country_code' => null, 'confidence' => 0, 'method' => 'api', 'timestamp' => time() ); // Check rate limiting if (!$this->can_make_api_request('geojs')) { return $result; } $api_url = "https://get.geojs.io/v1/ip/geo/{$ip}.json"; $response = wp_remote_get($api_url, array( 'timeout' => 3, 'user-agent' => 'TigerStyle-Whiskers/' . TIGERSTYLE_WHISKERS_VERSION )); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $data = json_decode(wp_remote_retrieve_body($response), true); if ($data && isset($data['country_code'])) { $result['country_code'] = strtoupper($data['country_code']); $result['confidence'] = 80; $result['additional_data'] = array( 'country_name' => $data['country'] ?? '', 'region' => $data['region'] ?? '', 'city' => $data['city'] ?? '', 'timezone' => $data['timezone'] ?? '' ); // Update rate limiting $this->update_api_rate_limit('geojs'); } } return $result; } /** * Detect via Accept-Language header (fallback) */ private function detect_via_accept_language() { $result = array( 'provider' => 'accept_language', 'country_code' => null, 'confidence' => 0, 'method' => 'fallback', 'timestamp' => time() ); if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $accept_language = $_SERVER['HTTP_ACCEPT_LANGUAGE']; // Parse Accept-Language header if (preg_match('/([a-z]{2})-([A-Z]{2})/', $accept_language, $matches)) { // Language-Country format (e.g., en-US) $result['country_code'] = strtoupper($matches[2]); $result['confidence'] = 60; $result['language_code'] = strtolower($matches[1]); } elseif (preg_match('/([a-z]{2})/', $accept_language, $matches)) { // Language only format (e.g., en) $language = strtolower($matches[1]); $country_map = array( 'en' => 'US', 'de' => 'DE', 'fr' => 'FR', 'es' => 'ES', 'it' => 'IT', 'pt' => 'PT', 'nl' => 'NL', 'pl' => 'PL', 'ru' => 'RU', 'ja' => 'JP', 'ko' => 'KR', 'zh' => 'CN' ); if (isset($country_map[$language])) { $result['country_code'] = $country_map[$language]; $result['confidence'] = 50; // Lower confidence for language mapping $result['language_code'] = $language; } } } return $result; } /** * Detect via timezone (client-side data) */ private function detect_via_timezone() { $result = array( 'provider' => 'timezone', 'country_code' => null, 'confidence' => 0, 'method' => 'client_based', 'timestamp' => time() ); // This will be enhanced by JavaScript on the client side // For now, we can make educated guesses based on server timezone $timezone = date_default_timezone_get(); $timezone_map = array( 'Europe/London' => 'GB', 'Europe/Paris' => 'FR', 'Europe/Berlin' => 'DE', 'Europe/Rome' => 'IT', 'Europe/Madrid' => 'ES', 'Europe/Amsterdam' => 'NL', 'America/New_York' => 'US', 'America/Los_Angeles' => 'US', 'America/Chicago' => 'US', 'Asia/Tokyo' => 'JP', 'Asia/Shanghai' => 'CN', 'Australia/Sydney' => 'AU' ); if (isset($timezone_map[$timezone])) { $result['country_code'] = $timezone_map[$timezone]; $result['confidence'] = 70; $result['timezone'] = $timezone; } return $result; } /** * Calculate consensus from multiple detection results */ private function calculate_consensus($detection_results) { $consensus = array( 'country_code' => null, 'confidence' => 0, 'sources_used' => array(), 'detection_method' => 'consensus', 'timestamp' => time(), 'privacy_laws' => array(), 'requires_consent' => false ); // Weight and score each result $weighted_results = array(); $total_weight = 0; foreach ($detection_results as $provider => $result) { if (!empty($result['country_code']) && $result['confidence'] > 0) { $provider_weight = $this->providers[$provider]['reliability'] ?? 50; $weighted_confidence = ($result['confidence'] * $provider_weight) / 100; $country = $result['country_code']; if (!isset($weighted_results[$country])) { $weighted_results[$country] = array( 'total_weight' => 0, 'total_confidence' => 0, 'sources' => array() ); } $weighted_results[$country]['total_weight'] += $provider_weight; $weighted_results[$country]['total_confidence'] += $weighted_confidence; $weighted_results[$country]['sources'][] = array( 'provider' => $provider, 'confidence' => $result['confidence'], 'weight' => $provider_weight ); $total_weight += $provider_weight; } } // Find the country with highest consensus $best_country = null; $best_score = 0; foreach ($weighted_results as $country => $data) { $consensus_score = ($data['total_confidence'] / $total_weight) * 100; if ($consensus_score > $best_score) { $best_score = $consensus_score; $best_country = $country; } } if ($best_country) { $consensus['country_code'] = $best_country; $consensus['confidence'] = round($best_score, 2); $consensus['sources_used'] = $weighted_results[$best_country]['sources']; // Determine applicable privacy laws $consensus['privacy_laws'] = $this->get_applicable_privacy_laws($best_country); $consensus['requires_consent'] = $this->requires_consent($best_country, $consensus['confidence']); } return $consensus; } /** * Get applicable privacy laws for a country */ private function get_applicable_privacy_laws($country_code) { $laws = array(); // Check GDPR territories if (isset($this->gdpr_territories[$country_code])) { $laws[] = array( 'law' => 'GDPR', 'full_name' => 'General Data Protection Regulation', 'territory' => $this->gdpr_territories[$country_code]['name'], 'confidence_required' => $this->gdpr_territories[$country_code]['confidence_required'] ); } // Check other privacy laws if (isset($this->privacy_territories[$country_code])) { $territory_data = $this->privacy_territories[$country_code]; if (isset($territory_data['law'])) { $laws[] = array( 'law' => $territory_data['law'], 'confidence_required' => $territory_data['confidence_required'] ); } // Handle US state laws if ($country_code === 'US' && isset($territory_data['states'])) { foreach ($territory_data['states'] as $state => $state_data) { $laws[] = array( 'law' => $state_data['law'], 'state' => $state, 'confidence_required' => $state_data['confidence_required'] ); } } } return $laws; } /** * Check if consent is required based on location and confidence */ private function requires_consent($country_code, $confidence) { // GDPR territories require consent if (isset($this->gdpr_territories[$country_code])) { $required_confidence = $this->gdpr_territories[$country_code]['confidence_required']; return $confidence >= $required_confidence; } // Other privacy law territories if (isset($this->privacy_territories[$country_code])) { $required_confidence = $this->privacy_territories[$country_code]['confidence_required'] ?? 80; return $confidence >= $required_confidence; } // Default: require consent if we're not sure (better safe than sorry) return $confidence < 90; } /** * API rate limiting checks */ private function can_make_api_request($provider) { $rate_limit_key = 'tigerstyle_whiskers_api_limit_' . $provider; $current_count = get_transient($rate_limit_key); // Set conservative limits to avoid hitting API rate limits $limits = array( 'ip_api' => 45, // 45 requests per minute (API allows 45) 'geojs' => 50 // 50 requests per minute (conservative) ); $limit = $limits[$provider] ?? 30; return $current_count === false || $current_count < $limit; } /** * Update API rate limiting */ private function update_api_rate_limit($provider) { $rate_limit_key = 'tigerstyle_whiskers_api_limit_' . $provider; $current_count = get_transient($rate_limit_key); if ($current_count === false) { set_transient($rate_limit_key, 1, 60); // 1 minute window } else { set_transient($rate_limit_key, $current_count + 1, 60); } } /** * Should we use API detection? */ private function should_use_api_detection() { // Don't use APIs if we already have reliable data from headers $cloudflare_result = $this->detect_via_cloudflare(); if ($cloudflare_result['confidence'] >= 90) { return false; } // Use APIs for better accuracy when needed return true; } /** * Cache detection results */ private function cache_detection($cache_key, $result) { // Cache for 1 hour for high confidence results, 15 minutes for low confidence $cache_duration = $result['confidence'] >= 80 ? 3600 : 900; $cache_data = array( 'result' => $result, 'cached_at' => time(), 'expires_at' => time() + $cache_duration ); set_transient($cache_key, $cache_data, $cache_duration); } /** * Get cached detection */ private function get_cached_detection($cache_key) { return get_transient($cache_key); } /** * Check if cache is valid */ private function is_cache_valid($cached_data) { return isset($cached_data['expires_at']) && $cached_data['expires_at'] > time(); } /** * Get visitor IP address */ private function get_visitor_ip() { // Check for IP from various sources if (!empty($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { // Handle comma-separated list of IPs $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); return trim($ips[0]); } elseif (!empty($_SERVER['HTTP_X_FORWARDED'])) { return $_SERVER['HTTP_X_FORWARDED']; } elseif (!empty($_SERVER['HTTP_X_CLUSTER_CLIENT_IP'])) { return $_SERVER['HTTP_X_CLUSTER_CLIENT_IP']; } elseif (!empty($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; } return '127.0.0.1'; } /** * Log detection event for audit */ private function log_detection_event($final_result, $all_results) { if (class_exists('TigerStyleWhiskers_AuditTrail')) { TigerStyleWhiskers_AuditTrail::log_event(array( 'event_type' => 'geo_detection', 'final_result' => $final_result, 'all_sources' => array_keys($all_results), 'confidence' => $final_result['confidence'], 'requires_consent' => $final_result['requires_consent'], 'timestamp' => time() )); } } /** * Update client-side geo data via AJAX */ public function update_client_geo_data() { if (!wp_verify_nonce($_POST['nonce'], 'tigerstyle_whiskers_geo')) { wp_send_json_error('Security check failed'); } $client_data = array( 'timezone' => sanitize_text_field($_POST['timezone'] ?? ''), 'language' => sanitize_text_field($_POST['language'] ?? ''), 'languages' => array_map('sanitize_text_field', $_POST['languages'] ?? array()) ); // Enhance detection with client-side data $enhanced_result = $this->enhance_with_client_data($this->detection_cache, $client_data); wp_send_json_success(array( 'enhanced_result' => $enhanced_result, 'requires_consent' => $enhanced_result['requires_consent'] )); } /** * Enhance detection with client-side data */ private function enhance_with_client_data($server_result, $client_data) { // Use client timezone for additional validation if (!empty($client_data['timezone'])) { $timezone_country = $this->map_timezone_to_country($client_data['timezone']); if ($timezone_country && $timezone_country === $server_result['country_code']) { // Timezone confirms server detection - increase confidence $server_result['confidence'] = min(100, $server_result['confidence'] + 5); $server_result['timezone_confirmed'] = true; } elseif ($timezone_country && $timezone_country !== $server_result['country_code']) { // Timezone conflicts - decrease confidence $server_result['confidence'] = max(0, $server_result['confidence'] - 10); $server_result['timezone_conflict'] = true; } } return $server_result; } /** * Map timezone to country */ private function map_timezone_to_country($timezone) { $timezone_map = array( 'Europe/London' => 'GB', 'Europe/Paris' => 'FR', 'Europe/Berlin' => 'DE', 'Europe/Rome' => 'IT', 'Europe/Madrid' => 'ES', 'Europe/Amsterdam' => 'NL', 'Europe/Brussels' => 'BE', 'Europe/Vienna' => 'AT', 'Europe/Zurich' => 'CH', 'Europe/Stockholm' => 'SE', 'Europe/Oslo' => 'NO', 'Europe/Copenhagen' => 'DK', 'Europe/Helsinki' => 'FI', 'Europe/Warsaw' => 'PL', 'Europe/Prague' => 'CZ', 'Europe/Budapest' => 'HU', 'Europe/Bucharest' => 'RO', 'Europe/Sofia' => 'BG', 'Europe/Athens' => 'GR', 'America/New_York' => 'US', 'America/Los_Angeles' => 'US', 'America/Chicago' => 'US', 'America/Denver' => 'US', 'America/Toronto' => 'CA', 'America/Vancouver' => 'CA', 'America/Sao_Paulo' => 'BR', 'Asia/Tokyo' => 'JP', 'Asia/Shanghai' => 'CN', 'Asia/Singapore' => 'SG', 'Australia/Sydney' => 'AU', 'Australia/Melbourne' => 'AU' ); return $timezone_map[$timezone] ?? null; } /** * Cleanup old geo cache */ public function cleanup_geo_cache() { global $wpdb; // Clean up expired transients $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_tigerstyle_whiskers_geo_%' AND option_value < " . time()); $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_tigerstyle_whiskers_geo_%' AND option_name NOT IN (SELECT REPLACE(option_name, '_timeout', '') FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_tigerstyle_whiskers_geo_%')"); } /** * Get current detection result */ public function get_current_detection() { if (empty($this->detection_cache)) { return $this->detect_visitor_location_advanced(); } return $this->detection_cache; } /** * Force re-detection (for testing) */ public function force_redetection() { $this->detection_cache = array(); // Clear relevant caches $visitor_ip = $this->get_visitor_ip(); $cache_key = 'tigerstyle_whiskers_geo_' . hash('sha256', $visitor_ip); delete_transient($cache_key); return $this->detect_visitor_location_advanced(); } /** * Get detection statistics for admin */ public function get_detection_statistics() { $stats = get_option('tigerstyle_whiskers_geo_stats', array( 'total_detections' => 0, 'high_confidence_detections' => 0, 'gdpr_territory_detections' => 0, 'api_calls_made' => 0, 'average_confidence' => 0 )); return $stats; } }