Navigate privacy laws with feline precision — detect every boundary, respect every territory! GDPR compliance and privacy protection for WordPress. - Cookie consent management - Privacy boundary detection - GDPR-compliant analytics gating - Cross-plugin consent coordination (integrates with TigerStyle Heat) - Visitor preference tracking - Configurable cookie categories Includes build.sh and .distignore for WordPress-installable release ZIPs.
826 lines
30 KiB
PHP
826 lines
30 KiB
PHP
<?php
|
|
/**
|
|
* Advanced Geographic Detection Whisker for TigerStyle Whiskers
|
|
*
|
|
* Multi-source geolocation with feline precision - like a cat using all its senses
|
|
* to understand its territory with maximum accuracy
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class TigerStyleWhiskers_AdvancedGeoDetector {
|
|
|
|
/**
|
|
* Single instance
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Geolocation providers and their reliability scores
|
|
*/
|
|
private $providers = array(
|
|
'cloudflare' => 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;
|
|
}
|
|
} |