1815440faSAtari911<?php 2815440faSAtari911/** 3815440faSAtari911 * Calendar Plugin - Audit Logger 4815440faSAtari911 * 5815440faSAtari911 * Logs all event modifications for compliance and debugging. 6815440faSAtari911 * Log files are stored in data/cache/calendar/audit/ 7815440faSAtari911 * 8815440faSAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9815440faSAtari911 * @author DokuWiki Community 10*2866e827SAtari911 * @version 7.2.6 11815440faSAtari911 */ 12815440faSAtari911 13815440faSAtari911if (!defined('DOKU_INC')) die(); 14815440faSAtari911 15815440faSAtari911class CalendarAuditLogger { 16815440faSAtari911 17815440faSAtari911 /** @var string Base directory for audit logs */ 18815440faSAtari911 private $logDir; 19815440faSAtari911 20815440faSAtari911 /** @var bool Whether audit logging is enabled */ 21815440faSAtari911 private $enabled = true; 22815440faSAtari911 23815440faSAtari911 /** @var int Maximum log file size in bytes (5MB) */ 24815440faSAtari911 const MAX_LOG_SIZE = 5242880; 25815440faSAtari911 26815440faSAtari911 /** @var int Number of rotated log files to keep */ 27815440faSAtari911 const MAX_LOG_FILES = 10; 28815440faSAtari911 29815440faSAtari911 /** 30815440faSAtari911 * Constructor 31815440faSAtari911 */ 32815440faSAtari911 public function __construct() { 33815440faSAtari911 global $conf; 34815440faSAtari911 $this->logDir = $conf['cachedir'] . '/calendar/audit'; 35815440faSAtari911 $this->ensureLogDir(); 36815440faSAtari911 } 37815440faSAtari911 38815440faSAtari911 /** 39815440faSAtari911 * Ensure the audit log directory exists 40815440faSAtari911 */ 41815440faSAtari911 private function ensureLogDir() { 42815440faSAtari911 if (!is_dir($this->logDir)) { 43815440faSAtari911 @mkdir($this->logDir, 0775, true); 44815440faSAtari911 } 45815440faSAtari911 } 46815440faSAtari911 47815440faSAtari911 /** 48815440faSAtari911 * Log an event action 49815440faSAtari911 * 50815440faSAtari911 * @param string $action The action performed (create, update, delete, etc.) 51815440faSAtari911 * @param array $data Additional data about the action 52815440faSAtari911 * @param string|null $user The user who performed the action (null = current user) 53815440faSAtari911 */ 54815440faSAtari911 public function log($action, $data = [], $user = null) { 55815440faSAtari911 if (!$this->enabled) return; 56815440faSAtari911 57815440faSAtari911 global $INFO; 58815440faSAtari911 59815440faSAtari911 // Get user info 60815440faSAtari911 if ($user === null) { 61815440faSAtari911 $user = isset($INFO['client']) ? $INFO['client'] : 'anonymous'; 62815440faSAtari911 } 63815440faSAtari911 64815440faSAtari911 // Build log entry 65815440faSAtari911 $entry = [ 66815440faSAtari911 'timestamp' => date('Y-m-d H:i:s'), 67815440faSAtari911 'unix_time' => time(), 68815440faSAtari911 'action' => $action, 69815440faSAtari911 'user' => $user, 70815440faSAtari911 'ip' => $this->getClientIP(), 71815440faSAtari911 'data' => $data 72815440faSAtari911 ]; 73815440faSAtari911 74815440faSAtari911 // Write to log file 75815440faSAtari911 $this->writeLog($entry); 76815440faSAtari911 } 77815440faSAtari911 78815440faSAtari911 /** 79815440faSAtari911 * Log event creation 80815440faSAtari911 */ 81815440faSAtari911 public function logCreate($namespace, $date, $eventId, $title, $user = null) { 82815440faSAtari911 $this->log('create', [ 83815440faSAtari911 'namespace' => $namespace, 84815440faSAtari911 'date' => $date, 85815440faSAtari911 'event_id' => $eventId, 86815440faSAtari911 'title' => $title 87815440faSAtari911 ], $user); 88815440faSAtari911 } 89815440faSAtari911 90815440faSAtari911 /** 91815440faSAtari911 * Log event update 92815440faSAtari911 */ 93815440faSAtari911 public function logUpdate($namespace, $date, $eventId, $title, $changes = [], $user = null) { 94815440faSAtari911 $this->log('update', [ 95815440faSAtari911 'namespace' => $namespace, 96815440faSAtari911 'date' => $date, 97815440faSAtari911 'event_id' => $eventId, 98815440faSAtari911 'title' => $title, 99815440faSAtari911 'changes' => $changes 100815440faSAtari911 ], $user); 101815440faSAtari911 } 102815440faSAtari911 103815440faSAtari911 /** 104815440faSAtari911 * Log event deletion 105815440faSAtari911 */ 106815440faSAtari911 public function logDelete($namespace, $date, $eventId, $title = '', $user = null) { 107815440faSAtari911 $this->log('delete', [ 108815440faSAtari911 'namespace' => $namespace, 109815440faSAtari911 'date' => $date, 110815440faSAtari911 'event_id' => $eventId, 111815440faSAtari911 'title' => $title 112815440faSAtari911 ], $user); 113815440faSAtari911 } 114815440faSAtari911 115815440faSAtari911 /** 116815440faSAtari911 * Log event move (date change) 117815440faSAtari911 */ 118815440faSAtari911 public function logMove($namespace, $oldDate, $newDate, $eventId, $title, $user = null) { 119815440faSAtari911 $this->log('move', [ 120815440faSAtari911 'namespace' => $namespace, 121815440faSAtari911 'old_date' => $oldDate, 122815440faSAtari911 'new_date' => $newDate, 123815440faSAtari911 'event_id' => $eventId, 124815440faSAtari911 'title' => $title 125815440faSAtari911 ], $user); 126815440faSAtari911 } 127815440faSAtari911 128815440faSAtari911 /** 129815440faSAtari911 * Log task completion toggle 130815440faSAtari911 */ 131815440faSAtari911 public function logTaskToggle($namespace, $date, $eventId, $title, $completed, $user = null) { 132815440faSAtari911 $this->log('task_toggle', [ 133815440faSAtari911 'namespace' => $namespace, 134815440faSAtari911 'date' => $date, 135815440faSAtari911 'event_id' => $eventId, 136815440faSAtari911 'title' => $title, 137815440faSAtari911 'completed' => $completed 138815440faSAtari911 ], $user); 139815440faSAtari911 } 140815440faSAtari911 141815440faSAtari911 /** 142815440faSAtari911 * Log bulk operations 143815440faSAtari911 */ 144815440faSAtari911 public function logBulk($operation, $count, $details = [], $user = null) { 145815440faSAtari911 $this->log('bulk_' . $operation, [ 146815440faSAtari911 'count' => $count, 147815440faSAtari911 'details' => $details 148815440faSAtari911 ], $user); 149815440faSAtari911 } 150815440faSAtari911 151815440faSAtari911 /** 152815440faSAtari911 * Write log entry to file 153815440faSAtari911 * 154815440faSAtari911 * @param array $entry Log entry data 155815440faSAtari911 */ 156815440faSAtari911 private function writeLog($entry) { 157815440faSAtari911 $logFile = $this->logDir . '/calendar_audit.log'; 158815440faSAtari911 159815440faSAtari911 // Rotate log if needed 160815440faSAtari911 $this->rotateLogIfNeeded($logFile); 161815440faSAtari911 162815440faSAtari911 // Format log line 163815440faSAtari911 $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; 164815440faSAtari911 165815440faSAtari911 // Append to log file with locking 166815440faSAtari911 $fp = @fopen($logFile, 'a'); 167815440faSAtari911 if ($fp) { 168815440faSAtari911 if (flock($fp, LOCK_EX)) { 169815440faSAtari911 fwrite($fp, $line); 170815440faSAtari911 fflush($fp); 171815440faSAtari911 flock($fp, LOCK_UN); 172815440faSAtari911 } 173815440faSAtari911 fclose($fp); 174815440faSAtari911 } 175815440faSAtari911 } 176815440faSAtari911 177815440faSAtari911 /** 178815440faSAtari911 * Rotate log file if it exceeds maximum size 179815440faSAtari911 * 180815440faSAtari911 * @param string $logFile Path to log file 181815440faSAtari911 */ 182815440faSAtari911 private function rotateLogIfNeeded($logFile) { 183815440faSAtari911 if (!file_exists($logFile)) return; 184815440faSAtari911 185815440faSAtari911 $size = @filesize($logFile); 186815440faSAtari911 if ($size < self::MAX_LOG_SIZE) return; 187815440faSAtari911 188815440faSAtari911 // Rotate existing numbered logs 189815440faSAtari911 for ($i = self::MAX_LOG_FILES - 1; $i >= 1; $i--) { 190815440faSAtari911 $oldFile = $logFile . '.' . $i; 191815440faSAtari911 $newFile = $logFile . '.' . ($i + 1); 192815440faSAtari911 if (file_exists($oldFile)) { 193815440faSAtari911 if ($i + 1 > self::MAX_LOG_FILES) { 194815440faSAtari911 @unlink($oldFile); 195815440faSAtari911 } else { 196815440faSAtari911 @rename($oldFile, $newFile); 197815440faSAtari911 } 198815440faSAtari911 } 199815440faSAtari911 } 200815440faSAtari911 201815440faSAtari911 // Rotate current log 202815440faSAtari911 @rename($logFile, $logFile . '.1'); 203815440faSAtari911 } 204815440faSAtari911 205815440faSAtari911 /** 206815440faSAtari911 * Get client IP address 207815440faSAtari911 * 208815440faSAtari911 * @return string 209815440faSAtari911 */ 210815440faSAtari911 private function getClientIP() { 211815440faSAtari911 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 212815440faSAtari911 $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); 213815440faSAtari911 return trim($ips[0]); 214815440faSAtari911 } 215815440faSAtari911 if (!empty($_SERVER['HTTP_X_REAL_IP'])) { 216815440faSAtari911 return $_SERVER['HTTP_X_REAL_IP']; 217815440faSAtari911 } 218815440faSAtari911 return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; 219815440faSAtari911 } 220815440faSAtari911 221815440faSAtari911 /** 222815440faSAtari911 * Get recent audit entries 223815440faSAtari911 * 224815440faSAtari911 * @param int $limit Number of entries to return 225815440faSAtari911 * @param string|null $action Filter by action type 226815440faSAtari911 * @return array 227815440faSAtari911 */ 228815440faSAtari911 public function getRecentEntries($limit = 100, $action = null) { 229815440faSAtari911 $logFile = $this->logDir . '/calendar_audit.log'; 230815440faSAtari911 if (!file_exists($logFile)) return []; 231815440faSAtari911 232815440faSAtari911 $entries = []; 233815440faSAtari911 $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 234815440faSAtari911 235815440faSAtari911 if (!$lines) return []; 236815440faSAtari911 237815440faSAtari911 // Read from end (most recent first) 238815440faSAtari911 $lines = array_reverse($lines); 239815440faSAtari911 240815440faSAtari911 foreach ($lines as $line) { 241815440faSAtari911 $entry = json_decode($line, true); 242815440faSAtari911 if (!$entry) continue; 243815440faSAtari911 244815440faSAtari911 if ($action !== null && $entry['action'] !== $action) { 245815440faSAtari911 continue; 246815440faSAtari911 } 247815440faSAtari911 248815440faSAtari911 $entries[] = $entry; 249815440faSAtari911 250815440faSAtari911 if (count($entries) >= $limit) break; 251815440faSAtari911 } 252815440faSAtari911 253815440faSAtari911 return $entries; 254815440faSAtari911 } 255815440faSAtari911 256815440faSAtari911 /** 257815440faSAtari911 * Get audit entries for a specific date range 258815440faSAtari911 * 259815440faSAtari911 * @param string $startDate Start date (Y-m-d) 260815440faSAtari911 * @param string $endDate End date (Y-m-d) 261815440faSAtari911 * @return array 262815440faSAtari911 */ 263815440faSAtari911 public function getEntriesByDateRange($startDate, $endDate) { 264815440faSAtari911 $logFile = $this->logDir . '/calendar_audit.log'; 265815440faSAtari911 if (!file_exists($logFile)) return []; 266815440faSAtari911 267815440faSAtari911 $startTime = strtotime($startDate . ' 00:00:00'); 268815440faSAtari911 $endTime = strtotime($endDate . ' 23:59:59'); 269815440faSAtari911 270815440faSAtari911 $entries = []; 271815440faSAtari911 $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 272815440faSAtari911 273815440faSAtari911 if (!$lines) return []; 274815440faSAtari911 275815440faSAtari911 foreach ($lines as $line) { 276815440faSAtari911 $entry = json_decode($line, true); 277815440faSAtari911 if (!$entry) continue; 278815440faSAtari911 279815440faSAtari911 $entryTime = $entry['unix_time'] ?? strtotime($entry['timestamp']); 280815440faSAtari911 281815440faSAtari911 if ($entryTime >= $startTime && $entryTime <= $endTime) { 282815440faSAtari911 $entries[] = $entry; 283815440faSAtari911 } 284815440faSAtari911 } 285815440faSAtari911 286815440faSAtari911 return $entries; 287815440faSAtari911 } 288815440faSAtari911 289815440faSAtari911 /** 290815440faSAtari911 * Enable or disable audit logging 291815440faSAtari911 * 292815440faSAtari911 * @param bool $enabled 293815440faSAtari911 */ 294815440faSAtari911 public function setEnabled($enabled) { 295815440faSAtari911 $this->enabled = (bool)$enabled; 296815440faSAtari911 } 297815440faSAtari911 298815440faSAtari911 /** 299815440faSAtari911 * Check if audit logging is enabled 300815440faSAtari911 * 301815440faSAtari911 * @return bool 302815440faSAtari911 */ 303815440faSAtari911 public function isEnabled() { 304815440faSAtari911 return $this->enabled; 305815440faSAtari911 } 306815440faSAtari911 307815440faSAtari911 /** 308815440faSAtari911 * Get the audit log directory path 309815440faSAtari911 * 310815440faSAtari911 * @return string 311815440faSAtari911 */ 312815440faSAtari911 public function getLogDir() { 313815440faSAtari911 return $this->logDir; 314815440faSAtari911 } 315815440faSAtari911 316815440faSAtari911 /** 317815440faSAtari911 * Get total size of all audit logs 318815440faSAtari911 * 319815440faSAtari911 * @return int Size in bytes 320815440faSAtari911 */ 321815440faSAtari911 public function getTotalLogSize() { 322815440faSAtari911 $total = 0; 323815440faSAtari911 $files = glob($this->logDir . '/calendar_audit.log*'); 324815440faSAtari911 foreach ($files as $file) { 325815440faSAtari911 $total += filesize($file); 326815440faSAtari911 } 327815440faSAtari911 return $total; 328815440faSAtari911 } 329815440faSAtari911 330815440faSAtari911 /** 331815440faSAtari911 * Clear all audit logs (use with caution) 332815440faSAtari911 * 333815440faSAtari911 * @return bool 334815440faSAtari911 */ 335815440faSAtari911 public function clearLogs() { 336815440faSAtari911 $files = glob($this->logDir . '/calendar_audit.log*'); 337815440faSAtari911 foreach ($files as $file) { 338815440faSAtari911 @unlink($file); 339815440faSAtari911 } 340815440faSAtari911 341815440faSAtari911 // Log the clear action itself 342815440faSAtari911 $this->log('audit_cleared', ['cleared_files' => count($files)]); 343815440faSAtari911 344815440faSAtari911 return true; 345815440faSAtari911 } 346815440faSAtari911} 347