logFile = $logFile; } public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage, string $status = 'ok', ?string $country = null): void { $dir = dirname($this->logFile); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $timestamp = date('Y-m-d H:i:s'); $ua = substr(preg_replace('/[[:cntrl:]]/', '', $userAgent), 0, 500); $ref = substr(preg_replace('/[[:cntrl:]]/', '', $referrer), 0, 500); $cc = ($country && strlen($country) === 2) ? strtoupper($country) : ''; $line = "[{$timestamp}] [{$ip}] [{$host}] [{$acceptLanguage}] [{$page}] [{$ua}] [{$ref}] [{$status}] [{$cc}]\n"; @file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX); } /** * Mask the last octet (IPv4) or last block (IPv6) of an IP address */ public static function anonymizeIp(string $ip): string { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $parts = explode('.', $ip); if (count($parts) === 4) { $parts[3] = 'x'; return implode('.', $parts); } } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $parts = explode(':', $ip); $keep = array_slice($parts, 0, 4); return implode(':', $keep) . '::x'; } return $ip; } /** * Check whether an IP matches any entry in a list of IPs/CIDR ranges. * * Supports exact IPv4/IPv6 addresses and CIDR notation (e.g. 192.168.0.0/16). * * @param string $ip The client IP to test * @param array $list List of IPs and/or CIDR ranges * @return bool True if the IP matches any entry */ public static function ipMatchesList(string $ip, array $list): bool { $ip = trim($ip); if ($ip === '') { return false; } $isV6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; $packed = $isV6 ? inet_pton($ip) : inet_pton($ip); foreach ($list as $entry) { $entry = trim((string)$entry); if ($entry === '') { continue; } // Exact match if ($entry === $ip) { return true; } // CIDR notation if (strpos($entry, '/') !== false) { [$subnet, $bits] = array_pad(explode('/', $entry, 2), 2, null); $subnet = trim($subnet); $subnetPacked = inet_pton($subnet); if ($subnetPacked === false || $packed === false) { continue; } // Ensure both are the same address family if (strlen($subnetPacked) !== strlen($packed)) { continue; } $maxBits = strlen($packed) * 8; $bits = (int)$bits; if ($bits < 0 || $bits > $maxBits) { continue; } if ($bits === 0) { return true; } $fullBytes = intdiv($bits, 8); $remainingBits = $bits % 8; $match = true; for ($i = 0; $i < $fullBytes; $i++) { if ($subnetPacked[$i] !== $packed[$i]) { $match = false; break; } } if ($match && $remainingBits > 0) { $mask = 0xFF << (8 - $remainingBits); if ((ord($subnetPacked[$fullBytes]) & $mask) !== (ord($packed[$fullBytes]) & $mask)) { $match = false; } } if ($match) { return true; } } } return false; } public static function getClientIp(): string { $headerKeys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ]; // Pass 1: Prioritize valid PUBLIC IP addresses (skips 127.0.0.1, 10.x, 172.x, 192.168.x proxy/internal IPs) foreach ($headerKeys as $key) { if (empty($_SERVER[$key])) continue; $value = $_SERVER[$key]; $ips = str_contains($value, ',') ? explode(',', $value) : [$value]; foreach ($ips as $rawIp) { $ip = trim($rawIp); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } // Pass 2: Fallback for local development environments foreach ($headerKeys as $key) { if (empty($_SERVER[$key])) continue; $value = $_SERVER[$key]; $ips = str_contains($value, ',') ? explode(',', $value) : [$value]; foreach ($ips as $rawIp) { $ip = trim($rawIp); if (filter_var($ip, FILTER_VALIDATE_IP) !== false) { return $ip; } } } return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; } public static function detectVisitorInfo(string $ua, string $user = ''): array { if (class_exists('BotGuard')) { $id = BotGuard::identify($ua); $cat = $id['category']; $label = $id['label']; if ($cat === 'ai') { return ['type' => 'ai', 'label' => $label, 'badge' => 'danger', 'icon' => 'bi-robot']; } if ($cat === 'search') { return ['type' => 'search', 'label' => $label, 'badge' => 'primary', 'icon' => 'bi-search']; } if ($cat === 'scraper') { return ['type' => 'scraper', 'label' => $label, 'badge' => 'warning text-dark', 'icon' => 'bi-bug']; } if ($cat === 'generic') { return ['type' => 'bot', 'label' => 'Bot', 'badge' => 'secondary', 'icon' => 'bi-robot']; } if ($cat === 'empty') { return ['type' => 'empty', 'label' => 'Lege UA', 'badge' => 'secondary', 'icon' => 'bi-slash-circle']; } $userPrefix = ($user && $user !== 'Gast') ? htmlspecialchars($user) . ' (' : ''; $userSuffix = ($user && $user !== 'Gast') ? ')' : ''; return [ 'type' => 'human', 'label' => $userPrefix . 'Mens' . $userSuffix, 'badge' => 'success', 'icon' => 'bi-person-check', ]; } return ['type' => 'unknown', 'label' => 'Bezoeker', 'badge' => 'secondary', 'icon' => 'bi-person']; } public static function detectBot(): ?string { $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (class_exists('BotGuard')) { $id = BotGuard::identify($ua); if (in_array($id['category'], ['ai', 'search', 'scraper'], true)) { return strtoupper($id['category']); } } return null; } public function getLogs(int $lines = 100): array { if (!file_exists($this->logFile)) { return []; } $content = file($this->logFile); $content = array_slice($content, -$lines); $logs = []; foreach ($content as $line) { $trimmed = trim($line); if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\](?: \[([^\]]*)\])?(?: \[([^\]]*)\])?$/', $trimmed, $m)) { $user = $m[3]; if (str_contains($user, '.') || str_contains($user, ':') || $user === 'cli') { $user = 'Gast'; } $status = $m[8] ?? 'ok'; if ($status === '') $status = 'ok'; $country = $m[9] ?? ''; $visitorInfo = self::detectVisitorInfo($m[6], $user); $logs[] = [ 'time' => $m[1], 'ip' => $m[2], 'user' => $user, 'visitor_info' => $visitorInfo, 'lang' => $m[4], 'page' => $m[5], 'ua' => $m[6], 'referrer' => $m[7], 'status' => $status, 'country' => $country, ]; } } return array_reverse($logs); } }