date('Y-m-d H:i:s'), 'event' => $event, 'level' => $level, 'message' => $message, 'ip' => $context['ip'] ?? (class_exists('RequestLogger') ? RequestLogger::getClientIp() : ''), 'context' => $context, ]; $driver = self::getDriver(); // Always store locally (sqlite or file fallback) so the dynamic log // in the admin always has entries. if ($driver === 'sqlite') { self::writeSqlite($entry); } else { self::writeFile($entry); } // Additionally forward to a remote syslog server if one is configured. $syslogHost = trim(self::$config['syslog_host'] ?? ''); if ($syslogHost !== '') { self::writeSyslog($entry); } } /** * Stuurt een log-entry naar een remote syslog-server over UDP. * * Bouwt een syslog-bericht op met facility, severity en ident en stuurt * dit via een UDP-socket. Verbindings- en schrijffouten worden gesilenced. * * @since 2.6.5 * * @param array $entry De te versturen log-entry. * @return void */ private static function writeSyslog(array $entry): void { $host = trim(self::$config['syslog_host'] ?? ''); $port = (int)(self::$config['syslog_port'] ?? 514); $facility = self::syslogFacility(self::$config['syslog_facility'] ?? 'local0'); $ident = self::$config['syslog_ident'] ?? 'codepress'; $severity = self::syslogSeverity($entry['level']); $pri = ($facility * 8) + $severity; $msg = '<' . $pri . '>' . date('M d H:i:s') . ' ' . $ident . '[' . getmypid() . ']: ' . '[' . $entry['event'] . '] [' . $entry['level'] . '] ' . $entry['message']; $sock = @fsockopen('udp://' . $host, $port, $errno, $errstr, 2); if ($sock) { @fwrite($sock, $msg . "\n"); @fclose($sock); } } /** * Slaat een log-entry op in de SQLite-database. * * De context-array wordt als JSON opgeslagen. Wanneer de PDO-verbinding * niet beschikbaar is of het schrijven faalt, wordt teruggevallen op * de file-driver. * * @since 2.6.5 * * @param array $entry De op te slaan log-entry. * @return void */ private static function writeSqlite(array $entry): void { $pdo = self::getPdo(); if ($pdo === null) { // SQLite failed -> fall back to file self::writeFile($entry); return; } try { $stmt = $pdo->prepare( 'INSERT INTO logs (time, event, level, message, ip, context) VALUES (:time, :event, :level, :message, :ip, :context)' ); $stmt->execute([ ':time' => $entry['time'], ':event' => $entry['event'], ':level' => $entry['level'], ':message' => $entry['message'], ':ip' => $entry['ip'], ':context' => json_encode($entry['context']), ]); } catch (\Throwable $e) { self::writeFile($entry); } } /** * Voegt een log-entry toe aan een platte-tekstbestand (fallback-driver). * * Zorgt dat de doelmap bestaat en schrijft de entry met een vaste * opmaak. Schrijffouten worden gesilenced. * * @since 2.6.5 * * @param array $entry De toe te voegen log-entry. * @return void */ private static function writeFile(array $entry): void { $dir = dirname(self::$dbPath); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $file = $dir . '/codepress.log'; $line = '[' . $entry['time'] . '] [' . $entry['event'] . '] [' . $entry['level'] . '] [' . $entry['ip'] . '] ' . $entry['message'] . "\n"; @file_put_contents($file, $line, FILE_APPEND | LOCK_EX); } /** * Geeft (en creëert lazy) de PDO-verbinding met de SQLite-database. * * Maakt de tabel en indices aan indien deze nog niet bestaan. Bij een * fout wordt de interne verbinding op null gezet en null teruggegeven, * zodat de aanroeper kan terugvallen op de file-driver. * * @since 2.6.5 * * @return \PDO|null PDO-verbinding of null indien SQLite niet beschikbaar/faalt. */ private static function getPdo(): ?\PDO { if (self::$pdo !== null) { return self::$pdo; } if (!self::sqliteAvailable()) { return null; } $dir = dirname(self::$dbPath); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } try { self::$pdo = new \PDO('sqlite:' . self::$dbPath); self::$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); self::$pdo->exec( 'CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT NOT NULL, event TEXT NOT NULL, level TEXT NOT NULL, message TEXT NOT NULL, ip TEXT, context TEXT )' ); self::$pdo->exec('CREATE INDEX IF NOT EXISTS idx_logs_event ON logs (event)'); self::$pdo->exec('CREATE INDEX IF NOT EXISTS idx_logs_time ON logs (time)'); return self::$pdo; } catch (\Throwable $e) { self::$pdo = null; return null; } } /** * Geeft aan of de SQLite PDO-driver beschikbaar is. * * @since 2.6.5 * * @return bool True indien PDO en de sqlite-driver beschikbaar zijn. */ private static function sqliteAvailable(): bool { return class_exists('PDO') && in_array('sqlite', \PDO::getAvailableDrivers(), true); } /** * Vertaalt een facility-naam naar de bijbehorende numerieke syslog-waarde. * * Ondersteunt de standaard facilities (kern, user, mail, daemon, auth, * syslog, lpr, news, uucp, cron, authpriv, ftp) en local0 t/m local7. * Onbekende faciliteiten vallen terug op local0 (16). * * @since 2.6.5 * * @param string $facility Naam van de facility. Default 'local0' wordt door aanroeper gezet. * @return int Numerieke facility-waarde. */ private static function syslogFacility(string $facility): int { $map = [ 'kern' => 0, 'user' => 1, 'mail' => 2, 'daemon' => 3, 'auth' => 4, 'syslog' => 5, 'lpr' => 6, 'news' => 7, 'uucp' => 8, 'cron' => 9, 'authpriv' => 10, 'ftp' => 11, 'local0' => 16, 'local1' => 17, 'local2' => 18, 'local3' => 19, 'local4' => 20, 'local5' => 21, 'local6' => 22, 'local7' => 23, ]; return $map[$facility] ?? 16; } /** * Vertaalt een loglevel naar de bijbehorende syslog-severity. * * Ondersteunt debug t/m emergency. Onbekende levels vallen terug op * info (6). De invoer wordt case-insensitive vergeleken. * * @since 2.6.5 * * @param string $level Loglevel (debug, info, notice, warning, error, ...). * @return int Numerieke severity-waarde (0 t/m 7). */ private static function syslogSeverity(string $level): int { $map = [ 'debug' => 7, 'info' => 6, 'notice' => 5, 'warning' => 4, 'error' => 3, 'critical' => 2, 'alert' => 1, 'emergency' => 0, ]; return $map[strtolower($level)] ?? 6; } /** * Zoekt recente log-entries op uit de actieve opslag. * * Bij de SQLite-driver wordt een geparametriseerde query uitgevoerd met * optionele filters op event, level en een zoekterm op het bericht. * Bij de file-driver wordt het logbestand uitgelezen en met een regex * geparseerd; de resultaten worden gefilterd en oudste eerst teruggegeven. * Resultaten worden altijd nieuwste eerst geretourneerd (SQLite via * ORDER BY id DESC, file via array_reverse). * * @since 2.6.5 * * @param int $limit Aantal terug te geven entries. Default 200. * @param string|null $event Optioneel event-filter. Default null (geen filter). * @param string|null $level Optioneel levelfilter (info, warning, error, ...). Default null. * @param string|null $search Optionele tekstzoekopdracht op het bericht. Default null. * @return array> Lijst met log-entries (nieuwste eerst). */ public static function getLogs(int $limit = 200, ?string $event = null, ?string $level = null, ?string $search = null): array { $driver = self::getDriver(); if ($driver === 'sqlite') { $pdo = self::getPdo(); if ($pdo !== null) { try { $sql = 'SELECT time, event, level, message, ip FROM logs'; $conds = []; $params = []; if ($event !== null && $event !== '') { $conds[] = 'event = :event'; $params[':event'] = $event; } if ($level !== null && $level !== '') { $conds[] = 'level = :level'; $params[':level'] = $level; } if ($search !== null && $search !== '') { $conds[] = 'message LIKE :search'; $params[':search'] = '%' . $search . '%'; } if (!empty($conds)) { $sql .= ' WHERE ' . implode(' AND ', $conds); } $sql .= ' ORDER BY id DESC LIMIT ' . (int)$limit; $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(\PDO::FETCH_ASSOC); } catch (\Throwable $e) { return []; } } } // File fallback $dir = dirname(self::$dbPath); $file = $dir . '/codepress.log'; if (!file_exists($file)) { return []; } $lines = file($file); $lines = array_slice($lines, -$limit); $logs = []; foreach ($lines as $line) { if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) { if ($event !== null && $event !== '' && $m[2] !== $event) { continue; } if ($level !== null && $level !== '' && $m[3] !== $level) { continue; } if ($search !== null && $search !== '' && stripos($m[5], $search) === false) { continue; } $logs[] = [ 'time' => $m[1], 'event' => $m[2], 'level' => $m[3], 'ip' => $m[4], 'message' => $m[5], ]; } } return array_reverse($logs); } /** * Wist alle opgeslagen log-entries. * * Bij de SQLite-driver worden alle rijen uit de logs-tabel verwijderd. * Bij de file-driver wordt het logbestand leeggemaakt. Schrijffouten * worden gesilenced. * * @since 2.6.5 * * @return void */ public static function clear(): void { $driver = self::getDriver(); if ($driver === 'sqlite') { $pdo = self::getPdo(); if ($pdo !== null) { try { $pdo->exec('DELETE FROM logs'); return; } catch (\Throwable $e) { // fall through to file } } } $dir = dirname(self::$dbPath); $file = $dir . '/codepress.log'; if (file_exists($file)) { @file_put_contents($file, ''); } } }