Files
CodePress/cms/core/class/RequestLogger.php
T
E.Noorlander 239762fd3a CodePress CMS v1.8.0: BotGuard security engine, HAProxy docs & ARIA fix
- Implement BotGuard security engine (Bot, AI, Scraper & Empty UA blocking)
- Add Admin Security page (/admin/security) with toggles, rate limiter & block/allowlists
- Add per-IP RateLimiter handoff in index.php with HTTP 429 response
- Add dynamic /robots.txt generation and noai/noimageai meta tags
- Add RequestLogger status column and blocked badges in admin request logs
- Fix ARIAComponents.php syntax errors on lines 67, 137, 262
- Add HAProxy / PFSense bot blocking & IP forwarding guide (docs/haproxy-bot-blocking.md)
- Update version to 1.8.0 with release notes in version.php and guides
2026-07-29 14:26:48 +02:00

161 lines
5.5 KiB
PHP

<?php
class RequestLogger
{
private string $logFile;
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage, string $status = 'ok'): 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);
$line = "[{$timestamp}] [{$ip}] [{$host}] [{$acceptLanguage}] [{$page}] [{$ua}] [{$ref}] [{$status}]\n";
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
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';
$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,
];
}
}
return array_reverse($logs);
}
}