tigerstyle-whiskers/includes/whiskers/class-advanced-geo-detector.php
Ryan Malloy adbdae19c8 Initial commit: TigerStyle Whiskers v1.0.0
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.
2026-05-27 14:31:51 -06:00

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;
}
}