-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
Rumen Damyanov edited this page Jul 31, 2025
·
1 revision
Common issues, solutions, and debugging techniques for the php-geolocation package.
- Quick Diagnostics
- Common Issues
- CloudFlare-Specific Issues
- Local Development Issues
- Cookie and Session Issues
- Language Detection Issues
- Performance Issues
- Security and Validation Issues
- Debugging Tools
<?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>";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'];
}
}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
}
}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();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:
-
Enable IP Geolocation
- Go to CloudFlare Dashboard → Network tab
- Enable "IP Geolocation"
- This adds the
CF-IPCountryheader
-
Check DNS Settings
- Ensure DNS records are proxied (orange cloud)
- Verify traffic is flowing through CloudFlare
-
Test Headers
curl -H "CF-IPCountry: DE" https://yoursite.com/test.php
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();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();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";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();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);
}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();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();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();When troubleshooting isn't enough:
- 📖 Configuration Guide - Advanced configuration options
- 🔗 Pure PHP Integration - Framework-agnostic solutions
- 🌐 Multi-language Websites - Complete internationalization
- 📊 Analytics Integration - Track and monitor geolocation data
Previous: Configuration | Next: Pure PHP Integration