Array containing all IP detection details */ public static function getIpDebugInfo(): array { $debugInfo = [ 'detected_ip' => self::getUserIpAddress(), 'reliable_ip' => self::getReliableIpAddress(), 'is_cloudflare' => self::isCloudflare(), 'cloudflare_ip' => self::getCloudflareIp(), 'sources' => [] ]; foreach (self::IP_SOURCES as $source) { $value = self::getServerVariable($source); if (!empty($value)) { $debugInfo['sources'][$source] = $value; } } return $debugInfo; } /** * Get Cloudflare IP if request is from Cloudflare * * @return string|null The Cloudflare connecting IP or null if not valid */ private static function getCloudflareIp(): ?string { if (!self::isCloudflare()) { return null; } $cfIP = self::getServerVariable('HTTP_CF_CONNECTING_IP'); if (empty($cfIP)) { return null; } // Sanitize and validate the IP $sanitizedIp = sanitize_text_field(wp_unslash($cfIP)); return filter_var($sanitizedIp, FILTER_VALIDATE_IP) ? $sanitizedIp : null; } /** * Check if the request is from Cloudflare * * @return bool True if request is from Cloudflare */ private static function isCloudflare(): bool { // Check required Cloudflare headers first foreach (self::CLOUDFLARE_HEADERS as $header) { if (!isset($_SERVER[$header])) { return false; } } // Get the source IP for validation $sourceIp = self::getSourceIpForValidation(); if (empty($sourceIp)) { return false; } // Validate if the source IP is from Cloudflare's ranges return self::isCloudflareIp($sourceIp); } /** * Get source IP for Cloudflare validation * * @return string The source IP address */ private static function getSourceIpForValidation(): string { $sources = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR']; foreach ($sources as $source) { $ip = self::getServerVariable($source); if (!empty($ip)) { return sanitize_text_field(wp_unslash($ip)); } } return ''; } /** * Validate if IP is from Cloudflare's IP ranges * * @param string $ip The IP address to validate * @return bool True if IP is from Cloudflare */ private static function isCloudflareIp(string $ip): bool { foreach (self::CLOUDFLARE_IP_RANGES as $range) { if (self::ipInRange($ip, $range)) { return true; } } return false; } /** * Validate and clean an IP address * * @param string $ip The IP address to validate * @param bool $allowPrivate Whether to allow private IP addresses * @return string The cleaned IP address or empty string if invalid */ private static function validateAndCleanIp(string $ip, bool $allowPrivate): string { // Remove any surrounding whitespace $ip = trim($ip); // Remove port number if present (e.g., "192.168.1.1:8080") if (strpos($ip, ':') !== false && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ip = explode(':', $ip)[0]; } // Validate IP format if (!filter_var($ip, FILTER_VALIDATE_IP)) { return ''; } // Check for private IP addresses if not allowed if (!$allowPrivate && self::isPrivateIp($ip)) { return ''; } return $ip; } /** * Check if an IP address is private/local * * @param string $ip The IP address to check * @return bool True if the IP is private */ private static function isPrivateIp(string $ip): bool { // Use filter_var for private IP detection return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false; } /** * Check if an IP address is within a given CIDR range * * @param string $ip The IP address to check * @param string $range The IP range in CIDR notation * @return bool True if the IP is in the range */ private static function ipInRange(string $ip, string $range): bool { if (strpos($range, '/') === false) { $range .= '/32'; } [$subnet, $netmask] = explode('/', $range, 2); $rangeDecimal = ip2long($subnet); $ipDecimal = ip2long($ip); $wildcardDecimal = pow(2, (32 - (int)$netmask)) - 1; $netmaskDecimal = ~$wildcardDecimal; return ($ipDecimal & $netmaskDecimal) === ($rangeDecimal & $netmaskDecimal); } /** * Safely get server variable * * @param string $key The server variable key * @return string The server variable value or empty string */ private static function getServerVariable(string $key): string { return $_SERVER[$key] ?? ''; } }