Files
CodePress/cms/core/class/Logger.php
T
E.Noorlander 44a1e6dc96 v1.9.1: fix world map rendering and resolve eight small TODO items
World map:
- Fix zero-padded ISO numeric ids leaving 31 countries unrendered
  (Brazil, Australia, Belgium, Austria, Algeria and more)
- Fix Russia and Fiji smearing across the full map width at the antimeridian
  by unwrapping ring longitudes and drawing them at both edges
- Crop to 84N-60S, add evenodd fill rule, 174 countries rendered

Improvements:
- Logger::tail() reads backwards in chunks instead of loading the whole file
- External links get rel=noopener noreferrer in footer and Markdown content
- formatDisplayName() cleaned up and guarded against empty input
- Export statistics as CSV (Excel BOM) or JSON
- GeoIP database auto-updates when older than 35 days
- Editor shortcuts Ctrl/Cmd+S to save and Ctrl/Cmd+N for a new page
- Live search filter in the admin content browser
- Content versioning with timestamped .bak copies in content/-backups/

Also removes eight stale TODO entries that were already implemented
2026-07-29 15:53:35 +02:00

193 lines
5.0 KiB
PHP

<?php
/**
* Simple Logger Class for CodePress CMS
*
* Provides structured logging with log levels and file output.
*
* @package CodePress
* @version 1.0.0
*/
class Logger {
const DEBUG = 'DEBUG';
const INFO = 'INFO';
const WARNING = 'WARNING';
const ERROR = 'ERROR';
private static $logFile = null;
private static $debugMode = false;
/**
* Initialize logger
*
* @param string $logFile Path to log file
* @param bool $debugMode Enable debug logging
*/
public static function init($logFile = null, $debugMode = false) {
if ($logFile === null) {
$logFile = __DIR__ . '/../../logs/codepress.log';
}
self::$logFile = $logFile;
self::$debugMode = $debugMode;
// Ensure log directory exists
$logDir = dirname(self::$logFile);
if (!is_dir($logDir)) {
@mkdir($logDir, 0755, true);
}
}
/**
* Log debug message (only in debug mode)
*
* @param string $message Message to log
* @param array $context Additional context
*/
public static function debug($message, $context = []) {
if (self::$debugMode) {
self::write(self::DEBUG, $message, $context);
}
}
/**
* Log info message
*
* @param string $message Message to log
* @param array $context Additional context
*/
public static function info($message, $context = []) {
self::write(self::INFO, $message, $context);
}
/**
* Log warning message
*
* @param string $message Message to log
* @param array $context Additional context
*/
public static function warning($message, $context = []) {
self::write(self::WARNING, $message, $context);
}
/**
* Log error message
*
* @param string $message Message to log
* @param array $context Additional context
*/
public static function error($message, $context = []) {
self::write(self::ERROR, $message, $context);
}
/**
* Write log entry to file
*
* @param string $level Log level
* @param string $message Message to log
* @param array $context Additional context
*/
private static function write($level, $message, $context = []) {
if (self::$logFile === null) {
self::init();
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$line = "[$timestamp] [$level] $message$contextStr\n";
// Write to file with error suppression (graceful degradation)
@file_put_contents(self::$logFile, $line, FILE_APPEND | LOCK_EX);
}
/**
* Get log file path
*
* @return string Log file path
*/
public static function getLogFile() {
return self::$logFile;
}
/**
* Clear log file
*
* @return bool Success status
*/
public static function clear() {
if (self::$logFile && file_exists(self::$logFile)) {
return @unlink(self::$logFile);
}
return false;
}
/**
* Get last N lines from log file
*
* Reads the file backwards in chunks so large log files never have to be
* loaded into memory in their entirety.
*
* @param int $lines Number of lines to retrieve
* @return array Log lines (including trailing newlines, oldest first)
*/
public static function tail($lines = 100) {
if (!self::$logFile || !file_exists(self::$logFile)) {
return [];
}
$lines = max(1, (int)$lines);
$handle = @fopen(self::$logFile, 'rb');
if ($handle === false) {
return [];
}
if (fseek($handle, 0, SEEK_END) !== 0) {
fclose($handle);
return [];
}
$fileSize = ftell($handle);
if ($fileSize === false || $fileSize === 0) {
fclose($handle);
return [];
}
$chunkSize = 8192;
$position = $fileSize;
$buffer = '';
$newlineCount = 0;
// Read backwards until we have enough newlines or reach the start
while ($position > 0 && $newlineCount <= $lines) {
$readSize = (int)min($chunkSize, $position);
$position -= $readSize;
if (fseek($handle, $position, SEEK_SET) !== 0) {
break;
}
$chunk = fread($handle, $readSize);
if ($chunk === false) {
break;
}
$buffer = $chunk . $buffer;
$newlineCount = substr_count($buffer, "\n");
}
fclose($handle);
if ($buffer === '') {
return [];
}
// Split while keeping the newline characters, matching file() behaviour
$result = preg_split('/(?<=\n)/', $buffer, -1, PREG_SPLIT_NO_EMPTY);
if ($result === false) {
return [];
}
return array_slice($result, -$lines);
}
}