Skip to content

Troubleshooting

Rumen Damyanov edited this page Jul 31, 2025 · 1 revision

Troubleshooting Guide

Common issues, solutions, and debugging techniques for the php-geolocation package.

Table of Contents

Quick Diagnostics

Health Check Script

<?php
require_once 'vendor/autoload.php';

use Rumenx\Geolocation\Geolocation;
use Rumenx\Geolocation\GeolocationSimulator;

class GeolocationDiagnostics
{
    private array $results = [];

    public function runDiagnostics(): array {
        $this->checkBasicFunctionality();
        $this->checkServerHeaders();
        $this->checkSimulation();
        $this->checkLanguageDetection();
        $this->checkCookies();
        $this->checkEnvironment();

        return $this->results;
    }

    private function checkBasicFunctionality(): void {
        try {
            $geo = new Geolocation();
            $country = $geo->getCountryCode();
            $language = $geo->getLanguage();

            $this->results['basic'] = [
                'status' => 'OK',
                'country' => $country,
                'language' => $language,
                'message' => 'Basic functionality working'
            ];
        } catch (Exception $e) {
            $this->results['basic'] = [
                'status' => 'ERROR',
                'message' => $e->getMessage()
            ];
        }
    }

    private function checkServerHeaders(): void {
        $headers = [
            'HTTP_CF_IPCOUNTRY',
            'HTTP_ACCEPT_LANGUAGE',
            'HTTP_USER_AGENT',
            'REMOTE_ADDR',
            'HTTP_HOST'
        ];

        $found = [];
        $missing = [];

        foreach ($headers as $header) {
            if (isset($_SERVER[$header])) {
                $found[$header] = $_SERVER[$header];
            } else {
                $missing[] = $header;
            }
        }

        $this->results['headers'] = [
            'status' => empty($missing) ? 'OK' : 'WARNING',
            'found' => $found,
            'missing' => $missing,
            'message' => empty($missing) ? 'All headers present' : 'Missing headers: ' . implode(', ', $missing)
        ];
    }

    private function checkSimulation(): void {
        try {
            $simulator = new GeolocationSimulator();
            $countries = $simulator->getAvailableCountries();

            // Test simulation
            $testCountry = 'DE';
            $simulated = Geolocation::simulate($testCountry);

            $this->results['simulation'] = [
                'status' => 'OK',
                'available_countries' => count($countries),
                'test_simulation' => [
                    'country' => $simulated->getCountryCode(),
                    'language' => $simulated->getLanguage(),
                    'city' => $simulated->getCity()
                ],
                'message' => 'Simulation working correctly'
            ];
        } catch (Exception $e) {
            $this->results['simulation'] = [
                'status' => 'ERROR',
                'message' => $e->getMessage()
            ];
        }
    }

    private function checkLanguageDetection(): void {
        $testCases = [
            'en-US,en;q=0.9,fr;q=0.8' => 'en',
            'fr-FR,fr;q=0.9,en;q=0.8' => 'fr',
            'de-DE,de;q=0.9' => 'de',
            'es-ES,es;q=0.9' => 'es'
        ];

        $results = [];
        foreach ($testCases as $acceptLang => $expected) {
            $server = ['HTTP_ACCEPT_LANGUAGE' => $acceptLang];
            $geo = new Geolocation($server);
            $detected = $geo->getLanguage();

            $results[] = [
                'input' => $acceptLang,
                'expected' => $expected,
                'detected' => $detected,
                'match' => $detected === $expected
            ];
        }

        $allMatch = array_reduce($results, fn($carry, $item) => $carry && $item['match'], true);

        $this->results['language'] = [
            'status' => $allMatch ? 'OK' : 'WARNING',
            'tests' => $results,
            'message' => $allMatch ? 'Language detection working' : 'Some language detection issues'
        ];
    }

    private function checkCookies(): void {
        $cookieName = 'test_geolocation_cookie';
        $testValue = 'en';

        // Test cookie setting (in a real scenario)
        $canSetCookie = !headers_sent();

        $this->results['cookies'] = [
            'status' => $canSetCookie ? 'OK' : 'WARNING',
            'can_set_cookies' => $canSetCookie,
            'existing_cookies' => array_keys($_COOKIE),
            'message' => $canSetCookie ? 'Cookies can be set' : 'Headers already sent, cannot test cookies'
        ];
    }

    private function checkEnvironment(): void {
        $environment = [
            'PHP_VERSION' => PHP_VERSION,
            'SERVER_SOFTWARE' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
            'DOCUMENT_ROOT' => $_SERVER['DOCUMENT_ROOT'] ?? 'Unknown',
            'HTTPS' => isset($_SERVER['HTTPS']) ? 'Yes' : 'No',
            'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? 'Unknown'
        ];

        $phpVersionOk = version_compare(PHP_VERSION, '8.0', '>=');

        $this->results['environment'] = [
            'status' => $phpVersionOk ? 'OK' : 'ERROR',
            'details' => $environment,
            'message' => $phpVersionOk ? 'Environment compatible' : 'PHP 8.0+ required'
        ];
    }

    public function generateReport(): string {
        $report = "=== Geolocation Diagnostics Report ===\n\n";

        foreach ($this->results as $section => $data) {
            $report .= strtoupper($section) . ": " . $data['status'] . "\n";
            $report .= "Message: " . $data['message'] . "\n";

            if (isset($data['details'])) {
                $report .= "Details:\n";
                foreach ($data['details'] as $key => $value) {
                    $report .= "  - $key: $value\n";
                }
            }

            $report .= "\n";
        }

        return $report;
    }
}

// Usage
$diagnostics = new GeolocationDiagnostics();
$results = $diagnostics->runDiagnostics();

// Display results
foreach ($results as $section => $data) {
    echo "<h3>" . ucfirst($section) . ": " . $data['status'] . "</h3>";
    echo "<p>" . $data['message'] . "</p>";

    if ($data['status'] === 'ERROR') {
        echo "<div style='color: red;'>❌ Requires attention</div>";
    } elseif ($data['status'] === 'WARNING') {
        echo "<div style='color: orange;'>⚠️ May need configuration</div>";
    } else {
        echo "<div style='color: green;'>✅ Working correctly</div>";
    }

    echo "<hr>";
}

// Generate detailed report
echo "<pre>" . $diagnostics->generateReport() . "</pre>";

Common Issues

Issue 1: Country Code Always Returns Default

Symptoms:

  • getCountryCode() always returns 'US' or default value
  • CloudFlare headers not being read

Causes:

  • Not behind CloudFlare proxy
  • CloudFlare geolocation features not enabled
  • Headers being stripped by proxy/load balancer

Solutions:

// Debug what headers are available
function debugHeaders(): void {
    echo "Available SERVER variables:\n";
    foreach ($_SERVER as $key => $value) {
        if (strpos($key, 'HTTP_') === 0 || strpos($key, 'REMOTE_') === 0) {
            echo "$key: $value\n";
        }
    }
}

debugHeaders();

// Check specifically for CloudFlare
function checkCloudFlareIntegration(): array {
    $cfHeaders = [
        'HTTP_CF_IPCOUNTRY' => 'Country Code',
        'HTTP_CF_RAY' => 'Ray ID',
        'HTTP_CF_VISITOR' => 'Visitor Info',
        'HTTP_CF_CONNECTING_IP' => 'Real IP'
    ];

    $status = [];
    foreach ($cfHeaders as $header => $description) {
        $status[$header] = [
            'present' => isset($_SERVER[$header]),
            'value' => $_SERVER[$header] ?? null,
            'description' => $description
        ];
    }

    return $status;
}

$cfStatus = checkCloudFlareIntegration();
var_dump($cfStatus);

// Alternative: Use IP-based geolocation as fallback
class FallbackGeolocation extends Geolocation
{
    public function getCountryCode(): string {
        // Try CloudFlare first
        $country = parent::getCountryCode();

        if ($country === 'US' && !isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
            // Fallback to IP-based detection
            $country = $this->getCountryFromIP();
        }

        return $country;
    }

    private function getCountryFromIP(): string {
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';

        // Use a geolocation service as fallback
        // Note: This requires external API and should be cached
        $geoData = $this->queryGeoService($ip);

        return $geoData['country'] ?? 'US';
    }

    private function queryGeoService(string $ip): array {
        // Example with ip-api.com (free tier available)
        $url = "http://ip-api.com/json/$ip?fields=country,countryCode";

        $context = stream_context_create([
            'http' => [
                'timeout' => 2, // Quick timeout
                'ignore_errors' => true
            ]
        ]);

        $response = @file_get_contents($url, false, $context);

        if ($response) {
            $data = json_decode($response, true);
            return [
                'country' => $data['countryCode'] ?? 'US'
            ];
        }

        return ['country' => 'US'];
    }
}

Issue 2: Language Detection Not Working

Symptoms:

  • Always returns default language
  • Doesn't respect Accept-Language header

Causes:

  • Invalid Accept-Language header format
  • Language not in available languages list
  • Encoding issues

Solutions:

class LanguageDebugger
{
    public static function debugLanguageDetection(): void {
        echo "=== Language Detection Debug ===\n";

        $acceptLang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'Not set';
        echo "Accept-Language header: $acceptLang\n";

        if ($acceptLang !== 'Not set') {
            $parsed = self::parseAcceptLanguage($acceptLang);
            echo "Parsed languages:\n";
            foreach ($parsed as $lang => $quality) {
                echo "  $lang (quality: $quality)\n";
            }
        }

        // Test with Geolocation
        $geo = new Geolocation();
        echo "Detected language: " . $geo->getLanguage() . "\n";

        // Test with custom mapping
        $customMapping = [
            'US' => ['en'],
            'FR' => ['fr'],
            'DE' => ['de']
        ];

        $geoCustom = new Geolocation($_SERVER, $customMapping);
        echo "With custom mapping: " . $geoCustom->getLanguage() . "\n";
    }

    private static function parseAcceptLanguage(string $acceptLanguage): array {
        $languages = [];

        foreach (explode(',', $acceptLanguage) as $lang) {
            $parts = explode(';q=', $lang);
            $code = trim($parts[0]);
            $quality = isset($parts[1]) ? (float)$parts[1] : 1.0;

            // Extract main language code
            $mainLang = explode('-', $code)[0];
            $languages[$mainLang] = $quality;
        }

        arsort($languages); // Sort by quality
        return $languages;
    }
}

LanguageDebugger::debugLanguageDetection();

// Fix for corrupted Accept-Language headers
class RobustLanguageDetection extends Geolocation
{
    protected function getLanguageFromHeader(): string {
        $acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';

        // Clean up the header
        $acceptLanguage = $this->cleanAcceptLanguage($acceptLanguage);

        if (empty($acceptLanguage)) {
            return $this->getDefaultLanguage();
        }

        return parent::getLanguageFromHeader();
    }

    private function cleanAcceptLanguage(string $acceptLanguage): string {
        // Remove any non-ASCII characters
        $clean = preg_replace('/[^\x20-\x7E]/', '', $acceptLanguage);

        // Validate format
        if (!preg_match('/^[a-zA-Z\-,;=\.\s\d]+$/', $clean)) {
            return '';
        }

        return $clean;
    }

    private function getDefaultLanguage(): string {
        return 'en'; // or get from configuration
    }
}

Issue 3: Cookies Not Persisting

Symptoms:

  • Language selection not remembered
  • Cookies not being set
  • Cookies reset on each request

Causes:

  • Headers already sent
  • Incorrect cookie domain/path
  • SameSite cookie issues
  • HTTPS/secure cookie issues

Solutions:

class CookieDebugger
{
    public static function debugCookieIssues(): array {
        $issues = [];

        // Check if headers already sent
        if (headers_sent($file, $line)) {
            $issues[] = [
                'type' => 'headers_sent',
                'message' => "Headers already sent in $file on line $line",
                'severity' => 'error'
            ];
        }

        // Check cookie configuration
        $issues = array_merge($issues, self::checkCookieConfig());

        // Check existing cookies
        $issues = array_merge($issues, self::checkExistingCookies());

        return $issues;
    }

    private static function checkCookieConfig(): array {
        $issues = [];

        // Check domain
        $domain = $_SERVER['HTTP_HOST'] ?? '';
        if (empty($domain)) {
            $issues[] = [
                'type' => 'no_domain',
                'message' => 'No HTTP_HOST available for cookie domain',
                'severity' => 'warning'
            ];
        }

        // Check HTTPS
        $isHttps = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
        if (!$isHttps) {
            $issues[] = [
                'type' => 'no_https',
                'message' => 'Not using HTTPS - secure cookies unavailable',
                'severity' => 'info'
            ];
        }

        return $issues;
    }

    private static function checkExistingCookies(): array {
        $issues = [];

        if (empty($_COOKIE)) {
            $issues[] = [
                'type' => 'no_cookies',
                'message' => 'No cookies present in request',
                'severity' => 'info'
            ];
        } else {
            $cookieCount = count($_COOKIE);
            $issues[] = [
                'type' => 'cookies_present',
                'message' => "$cookieCount cookies found: " . implode(', ', array_keys($_COOKIE)),
                'severity' => 'info'
            ];
        }

        return $issues;
    }

    public static function testCookieSettings(): void {
        $testName = 'geolocation_test_' . time();
        $testValue = 'test_value_' . mt_rand();

        echo "Testing cookie: $testName = $testValue\n";

        if (headers_sent()) {
            echo "❌ Cannot test - headers already sent\n";
            return;
        }

        // Test basic cookie
        $result = setcookie($testName, $testValue, time() + 60);
        echo $result ? "✅ Cookie set successfully\n" : "❌ Cookie setting failed\n";

        // Test with options
        $resultWithOptions = setcookie(
            $testName . '_opts',
            $testValue,
            [
                'expires' => time() + 60,
                'path' => '/',
                'secure' => false,
                'httponly' => true,
                'samesite' => 'Lax'
            ]
        );

        echo $resultWithOptions ? "✅ Cookie with options set\n" : "❌ Cookie with options failed\n";
    }
}

// Fixed cookie management class
class ReliableCookieManager
{
    private string $name;
    private array $options;

    public function __construct(string $name, array $options = []) {
        $this->name = $name;
        $this->options = array_merge([
            'lifetime' => 2592000, // 30 days
            'path' => '/',
            'domain' => null,
            'secure' => $this->isHttps(),
            'httponly' => true,
            'samesite' => 'Lax'
        ], $options);
    }

    public function set(string $value): bool {
        if (headers_sent()) {
            error_log("Cannot set cookie '$this->name' - headers already sent");
            return false;
        }

        return setcookie(
            $this->name,
            $value,
            [
                'expires' => time() + $this->options['lifetime'],
                'path' => $this->options['path'],
                'domain' => $this->options['domain'],
                'secure' => $this->options['secure'],
                'httponly' => $this->options['httponly'],
                'samesite' => $this->options['samesite']
            ]
        );
    }

    public function get(): ?string {
        return $_COOKIE[$this->name] ?? null;
    }

    public function delete(): bool {
        if (headers_sent()) {
            return false;
        }

        unset($_COOKIE[$this->name]);

        return setcookie(
            $this->name,
            '',
            [
                'expires' => time() - 3600,
                'path' => $this->options['path'],
                'domain' => $this->options['domain']
            ]
        );
    }

    private function isHttps(): bool {
        return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
               (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
               (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on');
    }
}

// Usage
$cookieManager = new ReliableCookieManager('app_language');

// Debug cookies
$issues = CookieDebugger::debugCookieIssues();
foreach ($issues as $issue) {
    echo "[{$issue['severity']}] {$issue['message']}\n";
}

// Test cookie functionality
CookieDebugger::testCookieSettings();

CloudFlare-Specific Issues

Issue: CloudFlare Headers Missing

Diagnostic Script:

class CloudFlareDebugger
{
    public static function checkCloudFlareStatus(): array {
        $status = [
            'behind_cloudflare' => false,
            'geolocation_enabled' => false,
            'headers' => [],
            'recommendations' => []
        ];

        // Check for CloudFlare presence
        $cfHeaders = [
            'HTTP_CF_RAY',
            'HTTP_CF_IPCOUNTRY',
            'HTTP_CF_VISITOR',
            'HTTP_CF_CONNECTING_IP'
        ];

        foreach ($cfHeaders as $header) {
            if (isset($_SERVER[$header])) {
                $status['headers'][$header] = $_SERVER[$header];
                $status['behind_cloudflare'] = true;

                if ($header === 'HTTP_CF_IPCOUNTRY') {
                    $status['geolocation_enabled'] = true;
                }
            }
        }

        // Recommendations
        if (!$status['behind_cloudflare']) {
            $status['recommendations'][] = 'Site is not behind CloudFlare proxy';
            $status['recommendations'][] = 'Consider using IP-based geolocation as fallback';
        } elseif (!$status['geolocation_enabled']) {
            $status['recommendations'][] = 'CloudFlare detected but geolocation not enabled';
            $status['recommendations'][] = 'Enable "IP Geolocation" in CloudFlare dashboard';
        }

        return $status;
    }

    public static function simulateCloudFlareHeaders(): void {
        if (!isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
            echo "Simulating CloudFlare headers for testing:\n";

            // Add simulated headers based on IP or random for testing
            $testCountries = ['US', 'GB', 'DE', 'FR', 'CA', 'AU', 'JP'];
            $randomCountry = $testCountries[array_rand($testCountries)];

            $_SERVER['HTTP_CF_IPCOUNTRY'] = $randomCountry;
            $_SERVER['HTTP_CF_RAY'] = 'test-ray-' . time();
            $_SERVER['HTTP_CF_VISITOR'] = '{"scheme":"https"}';

            echo "Added HTTP_CF_IPCOUNTRY: $randomCountry\n";
        }
    }
}

// Check CloudFlare status
$cfStatus = CloudFlareDebugger::checkCloudFlareStatus();
echo "CloudFlare Status:\n";
echo "Behind CloudFlare: " . ($cfStatus['behind_cloudflare'] ? 'Yes' : 'No') . "\n";
echo "Geolocation Enabled: " . ($cfStatus['geolocation_enabled'] ? 'Yes' : 'No') . "\n";

if (!empty($cfStatus['recommendations'])) {
    echo "Recommendations:\n";
    foreach ($cfStatus['recommendations'] as $rec) {
        echo "- $rec\n";
    }
}

// For local testing
CloudFlareDebugger::simulateCloudFlareHeaders();

CloudFlare Configuration Checklist:

  1. Enable IP Geolocation

    • Go to CloudFlare Dashboard → Network tab
    • Enable "IP Geolocation"
    • This adds the CF-IPCountry header
  2. Check DNS Settings

    • Ensure DNS records are proxied (orange cloud)
    • Verify traffic is flowing through CloudFlare
  3. Test Headers

    curl -H "CF-IPCountry: DE" https://yoursite.com/test.php

Local Development Issues

Issue: Simulation Not Working

class SimulationDebugger
{
    public static function testSimulation(): void {
        echo "=== Simulation Testing ===\n";

        try {
            $simulator = new GeolocationSimulator();
            $countries = $simulator->getAvailableCountries();

            echo "Available countries: " . count($countries) . "\n";
            echo "Countries: " . implode(', ', $countries) . "\n\n";

            // Test each country
            foreach (['US', 'DE', 'FR', 'JP'] as $country) {
                echo "Testing $country:\n";

                $simulated = Geolocation::simulate($country);
                echo "  Country: " . $simulated->getCountryCode() . "\n";
                echo "  Language: " . $simulated->getLanguage() . "\n";
                echo "  City: " . $simulated->getCity() . "\n";
                echo "  Region: " . $simulated->getRegion() . "\n\n";
            }

        } catch (Exception $e) {
            echo "❌ Simulation failed: " . $e->getMessage() . "\n";
        }
    }

    public static function testCustomSimulation(): void {
        echo "=== Custom Simulation Testing ===\n";

        $customData = [
            'HTTP_CF_IPCOUNTRY' => 'ES',
            'HTTP_ACCEPT_LANGUAGE' => 'es-ES,es;q=0.9,en;q=0.8',
            'REMOTE_ADDR' => '192.168.1.100'
        ];

        $geo = new Geolocation($customData);
        echo "Custom simulation:\n";
        echo "  Country: " . $geo->getCountryCode() . "\n";
        echo "  Language: " . $geo->getLanguage() . "\n";
    }
}

SimulationDebugger::testSimulation();
SimulationDebugger::testCustomSimulation();

Issue: Local Environment Detection

class LocalEnvironmentFixer
{
    public static function setupLocalDevelopment(): void {
        // Detect local environment
        $isLocal = self::isLocalEnvironment();

        if ($isLocal) {
            echo "Local environment detected. Setting up simulation...\n";

            // Set default simulation country from environment or config
            $defaultCountry = $_ENV['DEV_SIMULATE_COUNTRY'] ?? 'US';

            if (!isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
                $_SERVER['HTTP_CF_IPCOUNTRY'] = $defaultCountry;
                echo "Set HTTP_CF_IPCOUNTRY to $defaultCountry\n";
            }

            // Set default language header if missing
            if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
                $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9';
                echo "Set default Accept-Language header\n";
            }
        }
    }

    private static function isLocalEnvironment(): bool {
        $localIndicators = [
            'localhost',
            '127.0.0.1',
            '::1',
            '.local',
            '.dev',
            '.test'
        ];

        $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';

        foreach ($localIndicators as $indicator) {
            if (strpos($host, $indicator) !== false) {
                return true;
            }
        }

        return false;
    }
}

// Setup local development
LocalEnvironmentFixer::setupLocalDevelopment();

Cookie and Session Issues

Cookie Domain Issues

class CookieDomainFixer
{
    public static function getCorrectDomain(): ?string {
        $host = $_SERVER['HTTP_HOST'] ?? '';

        if (empty($host)) {
            return null;
        }

        // Remove port if present
        $host = explode(':', $host)[0];

        // For localhost/IP addresses, don't set domain
        if (in_array($host, ['localhost', '127.0.0.1', '::1']) ||
            filter_var($host, FILTER_VALIDATE_IP)) {
            return null;
        }

        // For subdomains, use parent domain for sharing
        $parts = explode('.', $host);
        if (count($parts) > 2) {
            // Return parent domain for subdomain sharing
            return '.' . implode('.', array_slice($parts, -2));
        }

        return null; // Let browser handle it
    }

    public static function createDomainAwareCookie(string $name, string $value): bool {
        $domain = self::getCorrectDomain();

        return setcookie($name, $value, [
            'expires' => time() + 2592000,
            'path' => '/',
            'domain' => $domain,
            'secure' => self::isHttps(),
            'httponly' => true,
            'samesite' => 'Lax'
        ]);
    }

    private static function isHttps(): bool {
        return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
               (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
    }
}

// Usage
$success = CookieDomainFixer::createDomainAwareCookie('app_language', 'en');
echo $success ? "Cookie set successfully" : "Cookie setting failed";

Language Detection Issues

Accept-Language Parser Issues

class LanguageParserFixer
{
    public static function robustParseAcceptLanguage(string $acceptLanguage): array {
        $languages = [];

        // Clean the input
        $acceptLanguage = trim($acceptLanguage);
        if (empty($acceptLanguage)) {
            return $languages;
        }

        // Split by comma and process each language
        $langParts = explode(',', $acceptLanguage);

        foreach ($langParts as $langPart) {
            $langPart = trim($langPart);

            // Parse language and quality
            if (strpos($langPart, ';q=') !== false) {
                [$lang, $qPart] = explode(';q=', $langPart, 2);
                $quality = (float)trim($qPart);
            } else {
                $lang = $langPart;
                $quality = 1.0;
            }

            // Extract main language code
            $lang = trim($lang);
            if (strpos($lang, '-') !== false) {
                $lang = explode('-', $lang)[0];
            }

            // Validate language code
            if (preg_match('/^[a-z]{2,3}$/i', $lang)) {
                $lang = strtolower($lang);
                $languages[$lang] = max($quality, $languages[$lang] ?? 0);
            }
        }

        // Sort by quality (highest first)
        arsort($languages);

        return $languages;
    }

    public static function testLanguageParsing(): void {
        $testCases = [
            'en-US,en;q=0.9,fr;q=0.8,de;q=0.7',
            'fr-FR,fr;q=0.9,en;q=0.8',
            'invalid;format,en',
            'en-US,en;q=0.9,*;q=0.1',
            ''
        ];

        foreach ($testCases as $test) {
            echo "Input: '$test'\n";
            $parsed = self::robustParseAcceptLanguage($test);
            echo "Parsed: " . json_encode($parsed) . "\n\n";
        }
    }
}

LanguageParserFixer::testLanguageParsing();

Performance Issues

Caching Implementation

class PerformanceOptimizer
{
    private $cache;

    public function __construct($cache = null) {
        $this->cache = $cache ?: new SimpleFileCache();
    }

    public function getCachedGeolocation(string $cacheKey): ?Geolocation {
        $cached = $this->cache->get($cacheKey);

        if ($cached) {
            return $this->deserializeGeolocation($cached);
        }

        return null;
    }

    public function cacheGeolocation(string $cacheKey, Geolocation $geo): void {
        $serialized = $this->serializeGeolocation($geo);
        $this->cache->set($cacheKey, $serialized, 3600);
    }

    private function serializeGeolocation(Geolocation $geo): array {
        return [
            'country' => $geo->getCountryCode(),
            'language' => $geo->getLanguage(),
            'city' => $geo->getCity(),
            'region' => $geo->getRegion(),
            'timestamp' => time()
        ];
    }

    private function deserializeGeolocation(array $data): Geolocation {
        $server = [
            'HTTP_CF_IPCOUNTRY' => $data['country'],
            'HTTP_ACCEPT_LANGUAGE' => $data['language']
        ];

        return new Geolocation($server);
    }

    public function generateCacheKey(): string {
        $factors = [
            $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '',
            $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
            $_SERVER['REMOTE_ADDR'] ?? ''
        ];

        return 'geo:' . hash('crc32', implode('|', $factors));
    }
}

class SimpleFileCache
{
    private string $cacheDir;

    public function __construct(string $cacheDir = null) {
        $this->cacheDir = $cacheDir ?: sys_get_temp_dir() . '/geolocation_cache';

        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
    }

    public function get(string $key) {
        $file = $this->getCacheFile($key);

        if (!file_exists($file)) {
            return null;
        }

        $data = json_decode(file_get_contents($file), true);

        if ($data['expires'] < time()) {
            unlink($file);
            return null;
        }

        return $data['value'];
    }

    public function set(string $key, $value, int $ttl): void {
        $file = $this->getCacheFile($key);
        $data = [
            'value' => $value,
            'expires' => time() + $ttl
        ];

        file_put_contents($file, json_encode($data), LOCK_EX);
    }

    private function getCacheFile(string $key): string {
        return $this->cacheDir . '/' . md5($key) . '.cache';
    }
}

// Usage
$optimizer = new PerformanceOptimizer();
$cacheKey = $optimizer->generateCacheKey();

$geo = $optimizer->getCachedGeolocation($cacheKey);
if (!$geo) {
    $geo = new Geolocation();
    $optimizer->cacheGeolocation($cacheKey, $geo);
}

Security and Validation Issues

Input Sanitization

class SecurityValidator
{
    public static function validateServerInput(array $server): array {
        $clean = [];

        // Validate country code
        if (isset($server['HTTP_CF_IPCOUNTRY'])) {
            $country = strtoupper(trim($server['HTTP_CF_IPCOUNTRY']));
            if (preg_match('/^[A-Z]{2}$/', $country)) {
                $clean['HTTP_CF_IPCOUNTRY'] = $country;
            }
        }

        // Validate IP address
        if (isset($server['REMOTE_ADDR'])) {
            $ip = filter_var($server['REMOTE_ADDR'], FILTER_VALIDATE_IP);
            if ($ip) {
                $clean['REMOTE_ADDR'] = $ip;
            }
        }

        // Validate and sanitize Accept-Language
        if (isset($server['HTTP_ACCEPT_LANGUAGE'])) {
            $acceptLang = self::sanitizeAcceptLanguage($server['HTTP_ACCEPT_LANGUAGE']);
            if ($acceptLang) {
                $clean['HTTP_ACCEPT_LANGUAGE'] = $acceptLang;
            }
        }

        // Validate User-Agent
        if (isset($server['HTTP_USER_AGENT'])) {
            $userAgent = self::sanitizeUserAgent($server['HTTP_USER_AGENT']);
            if ($userAgent) {
                $clean['HTTP_USER_AGENT'] = $userAgent;
            }
        }

        return $clean;
    }

    private static function sanitizeAcceptLanguage(string $acceptLanguage): ?string {
        // Remove dangerous characters
        $clean = preg_replace('/[^\w\-,;=\.\s]/', '', $acceptLanguage);

        // Limit length
        $clean = substr($clean, 0, 200);

        // Validate format
        if (!preg_match('/^[\w\-,;=\.\s]+$/', $clean)) {
            return null;
        }

        return $clean;
    }

    private static function sanitizeUserAgent(string $userAgent): ?string {
        // Remove potentially dangerous characters
        $clean = strip_tags($userAgent);
        $clean = preg_replace('/[<>"\']/', '', $clean);

        // Limit length
        $clean = substr(trim($clean), 0, 500);

        return empty($clean) ? null : $clean;
    }

    public static function createSecureGeolocation(): Geolocation {
        $cleanServer = self::validateServerInput($_SERVER);
        return new Geolocation($cleanServer);
    }
}

// Usage
$geo = SecurityValidator::createSecureGeolocation();

Debugging Tools

Comprehensive Debug Output

class GeolocationDebugTool
{
    public static function fullDebugOutput(): string {
        $output = "=== GEOLOCATION DEBUG OUTPUT ===\n";
        $output .= "Generated: " . date('Y-m-d H:i:s') . "\n\n";

        // Environment info
        $output .= "=== ENVIRONMENT ===\n";
        $output .= "PHP Version: " . PHP_VERSION . "\n";
        $output .= "Server Software: " . ($_SERVER['SERVER_SOFTWARE'] ?? 'Unknown') . "\n";
        $output .= "Request Method: " . ($_SERVER['REQUEST_METHOD'] ?? 'Unknown') . "\n";
        $output .= "HTTPS: " . (isset($_SERVER['HTTPS']) ? 'Yes' : 'No') . "\n\n";

        // Headers
        $output .= "=== RELEVANT HEADERS ===\n";
        $relevantHeaders = [
            'HTTP_CF_IPCOUNTRY',
            'HTTP_CF_RAY',
            'HTTP_ACCEPT_LANGUAGE',
            'HTTP_USER_AGENT',
            'REMOTE_ADDR',
            'HTTP_HOST',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP'
        ];

        foreach ($relevantHeaders as $header) {
            $value = $_SERVER[$header] ?? 'Not set';
            $output .= "$header: $value\n";
        }
        $output .= "\n";

        // Cookies
        $output .= "=== COOKIES ===\n";
        if (empty($_COOKIE)) {
            $output .= "No cookies present\n";
        } else {
            foreach ($_COOKIE as $name => $value) {
                $output .= "$name: $value\n";
            }
        }
        $output .= "\n";

        // Geolocation results
        $output .= "=== GEOLOCATION RESULTS ===\n";
        try {
            $geo = new Geolocation();
            $output .= "Country Code: " . $geo->getCountryCode() . "\n";
            $output .= "Language: " . $geo->getLanguage() . "\n";
            $output .= "City: " . $geo->getCity() . "\n";
            $output .= "Region: " . $geo->getRegion() . "\n";
        } catch (Exception $e) {
            $output .= "ERROR: " . $e->getMessage() . "\n";
        }
        $output .= "\n";

        // Simulation test
        $output .= "=== SIMULATION TEST ===\n";
        try {
            $simulated = Geolocation::simulate('DE');
            $output .= "Simulated Country: " . $simulated->getCountryCode() . "\n";
            $output .= "Simulated Language: " . $simulated->getLanguage() . "\n";
        } catch (Exception $e) {
            $output .= "Simulation ERROR: " . $e->getMessage() . "\n";
        }

        return $output;
    }

    public static function logDebugInfo(): void {
        $debug = self::fullDebugOutput();
        error_log($debug);

        // Also save to file for easy viewing
        $debugFile = sys_get_temp_dir() . '/geolocation_debug_' . time() . '.txt';
        file_put_contents($debugFile, $debug);

        echo "Debug info logged to: $debugFile\n";
    }
}

// Generate debug output
echo "<pre>" . GeolocationDebugTool::fullDebugOutput() . "</pre>";

// Or log to file
GeolocationDebugTool::logDebugInfo();

Debug Headers for Browser

function addDebugHeaders(): void {
    if (!headers_sent()) {
        $geo = new Geolocation();

        header('X-Debug-Country: ' . $geo->getCountryCode());
        header('X-Debug-Language: ' . $geo->getLanguage());
        header('X-Debug-CF-Country: ' . ($_SERVER['HTTP_CF_IPCOUNTRY'] ?? 'Not set'));
        header('X-Debug-Accept-Lang: ' . ($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'Not set'));
    }
}

// Call early in your application
addDebugHeaders();

Next Steps

When troubleshooting isn't enough:


Previous: Configuration | Next: Pure PHP Integration

Clone this wiki locally