1*815440faSAtari911<?php 2*815440faSAtari911/** 3*815440faSAtari911 * Calendar Plugin - Audit Logger 4*815440faSAtari911 * 5*815440faSAtari911 * Logs all event modifications for compliance and debugging. 6*815440faSAtari911 * Log files are stored in data/cache/calendar/audit/ 7*815440faSAtari911 * 8*815440faSAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9*815440faSAtari911 * @author DokuWiki Community 10*815440faSAtari911 * @version 7.0.8 11*815440faSAtari911 */ 12*815440faSAtari911 13*815440faSAtari911if (!defined('DOKU_INC')) die(); 14*815440faSAtari911 15*815440faSAtari911class CalendarAuditLogger { 16*815440faSAtari911 17*815440faSAtari911 /** @var string Base directory for audit logs */ 18*815440faSAtari911 private $logDir; 19*815440faSAtari911 20*815440faSAtari911 /** @var bool Whether audit logging is enabled */ 21*815440faSAtari911 private $enabled = true; 22*815440faSAtari911 23*815440faSAtari911 /** @var int Maximum log file size in bytes (5MB) */ 24*815440faSAtari911 const MAX_LOG_SIZE = 5242880; 25*815440faSAtari911 26*815440faSAtari911 /** @var int Number of rotated log files to keep */ 27*815440faSAtari911 const MAX_LOG_FILES = 10; 28*815440faSAtari911 29*815440faSAtari911 /** 30*815440faSAtari911 * Constructor 31*815440faSAtari911 */ 32*815440faSAtari911 public function __construct() { 33*815440faSAtari911 global $conf; 34*815440faSAtari911 $this->logDir = $conf['cachedir'] . '/calendar/audit'; 35*815440faSAtari911 $this->ensureLogDir(); 36*815440faSAtari911 } 37*815440faSAtari911 38*815440faSAtari911 /** 39*815440faSAtari911 * Ensure the audit log directory exists 40*815440faSAtari911 */ 41*815440faSAtari911 private function ensureLogDir() { 42*815440faSAtari911 if (!is_dir($this->logDir)) { 43*815440faSAtari911 @mkdir($this->logDir, 0775, true); 44*815440faSAtari911 } 45*815440faSAtari911 } 46*815440faSAtari911 47*815440faSAtari911 /** 48*815440faSAtari911 * Log an event action 49*815440faSAtari911 * 50*815440faSAtari911 * @param string $action The action performed (create, update, delete, etc.) 51*815440faSAtari911 * @param array $data Additional data about the action 52*815440faSAtari911 * @param string|null $user The user who performed the action (null = current user) 53*815440faSAtari911 */ 54*815440faSAtari911 public function log($action, $data = [], $user = null) { 55*815440faSAtari911 if (!$this->enabled) return; 56*815440faSAtari911 57*815440faSAtari911 global $INFO; 58*815440faSAtari911 59*815440faSAtari911 // Get user info 60*815440faSAtari911 if ($user === null) { 61*815440faSAtari911 $user = isset($INFO['client']) ? $INFO['client'] : 'anonymous'; 62*815440faSAtari911 } 63*815440faSAtari911 64*815440faSAtari911 // Build log entry 65*815440faSAtari911 $entry = [ 66*815440faSAtari911 'timestamp' => date('Y-m-d H:i:s'), 67*815440faSAtari911 'unix_time' => time(), 68*815440faSAtari911 'action' => $action, 69*815440faSAtari911 'user' => $user, 70*815440faSAtari911 'ip' => $this->getClientIP(), 71*815440faSAtari911 'data' => $data 72*815440faSAtari911 ]; 73*815440faSAtari911 74*815440faSAtari911 // Write to log file 75*815440faSAtari911 $this->writeLog($entry); 76*815440faSAtari911 } 77*815440faSAtari911 78*815440faSAtari911 /** 79*815440faSAtari911 * Log event creation 80*815440faSAtari911 */ 81*815440faSAtari911 public function logCreate($namespace, $date, $eventId, $title, $user = null) { 82*815440faSAtari911 $this->log('create', [ 83*815440faSAtari911 'namespace' => $namespace, 84*815440faSAtari911 'date' => $date, 85*815440faSAtari911 'event_id' => $eventId, 86*815440faSAtari911 'title' => $title 87*815440faSAtari911 ], $user); 88*815440faSAtari911 } 89*815440faSAtari911 90*815440faSAtari911 /** 91*815440faSAtari911 * Log event update 92*815440faSAtari911 */ 93*815440faSAtari911 public function logUpdate($namespace, $date, $eventId, $title, $changes = [], $user = null) { 94*815440faSAtari911 $this->log('update', [ 95*815440faSAtari911 'namespace' => $namespace, 96*815440faSAtari911 'date' => $date, 97*815440faSAtari911 'event_id' => $eventId, 98*815440faSAtari911 'title' => $title, 99*815440faSAtari911 'changes' => $changes 100*815440faSAtari911 ], $user); 101*815440faSAtari911 } 102*815440faSAtari911 103*815440faSAtari911 /** 104*815440faSAtari911 * Log event deletion 105*815440faSAtari911 */ 106*815440faSAtari911 public function logDelete($namespace, $date, $eventId, $title = '', $user = null) { 107*815440faSAtari911 $this->log('delete', [ 108*815440faSAtari911 'namespace' => $namespace, 109*815440faSAtari911 'date' => $date, 110*815440faSAtari911 'event_id' => $eventId, 111*815440faSAtari911 'title' => $title 112*815440faSAtari911 ], $user); 113*815440faSAtari911 } 114*815440faSAtari911 115*815440faSAtari911 /** 116*815440faSAtari911 * Log event move (date change) 117*815440faSAtari911 */ 118*815440faSAtari911 public function logMove($namespace, $oldDate, $newDate, $eventId, $title, $user = null) { 119*815440faSAtari911 $this->log('move', [ 120*815440faSAtari911 'namespace' => $namespace, 121*815440faSAtari911 'old_date' => $oldDate, 122*815440faSAtari911 'new_date' => $newDate, 123*815440faSAtari911 'event_id' => $eventId, 124*815440faSAtari911 'title' => $title 125*815440faSAtari911 ], $user); 126*815440faSAtari911 } 127*815440faSAtari911 128*815440faSAtari911 /** 129*815440faSAtari911 * Log task completion toggle 130*815440faSAtari911 */ 131*815440faSAtari911 public function logTaskToggle($namespace, $date, $eventId, $title, $completed, $user = null) { 132*815440faSAtari911 $this->log('task_toggle', [ 133*815440faSAtari911 'namespace' => $namespace, 134*815440faSAtari911 'date' => $date, 135*815440faSAtari911 'event_id' => $eventId, 136*815440faSAtari911 'title' => $title, 137*815440faSAtari911 'completed' => $completed 138*815440faSAtari911 ], $user); 139*815440faSAtari911 } 140*815440faSAtari911 141*815440faSAtari911 /** 142*815440faSAtari911 * Log bulk operations 143*815440faSAtari911 */ 144*815440faSAtari911 public function logBulk($operation, $count, $details = [], $user = null) { 145*815440faSAtari911 $this->log('bulk_' . $operation, [ 146*815440faSAtari911 'count' => $count, 147*815440faSAtari911 'details' => $details 148*815440faSAtari911 ], $user); 149*815440faSAtari911 } 150*815440faSAtari911 151*815440faSAtari911 /** 152*815440faSAtari911 * Write log entry to file 153*815440faSAtari911 * 154*815440faSAtari911 * @param array $entry Log entry data 155*815440faSAtari911 */ 156*815440faSAtari911 private function writeLog($entry) { 157*815440faSAtari911 $logFile = $this->logDir . '/calendar_audit.log'; 158*815440faSAtari911 159*815440faSAtari911 // Rotate log if needed 160*815440faSAtari911 $this->rotateLogIfNeeded($logFile); 161*815440faSAtari911 162*815440faSAtari911 // Format log line 163*815440faSAtari911 $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; 164*815440faSAtari911 165*815440faSAtari911 // Append to log file with locking 166*815440faSAtari911 $fp = @fopen($logFile, 'a'); 167*815440faSAtari911 if ($fp) { 168*815440faSAtari911 if (flock($fp, LOCK_EX)) { 169*815440faSAtari911 fwrite($fp, $line); 170*815440faSAtari911 fflush($fp); 171*815440faSAtari911 flock($fp, LOCK_UN); 172*815440faSAtari911 } 173*815440faSAtari911 fclose($fp); 174*815440faSAtari911 } 175*815440faSAtari911 } 176*815440faSAtari911 177*815440faSAtari911 /** 178*815440faSAtari911 * Rotate log file if it exceeds maximum size 179*815440faSAtari911 * 180*815440faSAtari911 * @param string $logFile Path to log file 181*815440faSAtari911 */ 182*815440faSAtari911 private function rotateLogIfNeeded($logFile) { 183*815440faSAtari911 if (!file_exists($logFile)) return; 184*815440faSAtari911 185*815440faSAtari911 $size = @filesize($logFile); 186*815440faSAtari911 if ($size < self::MAX_LOG_SIZE) return; 187*815440faSAtari911 188*815440faSAtari911 // Rotate existing numbered logs 189*815440faSAtari911 for ($i = self::MAX_LOG_FILES - 1; $i >= 1; $i--) { 190*815440faSAtari911 $oldFile = $logFile . '.' . $i; 191*815440faSAtari911 $newFile = $logFile . '.' . ($i + 1); 192*815440faSAtari911 if (file_exists($oldFile)) { 193*815440faSAtari911 if ($i + 1 > self::MAX_LOG_FILES) { 194*815440faSAtari911 @unlink($oldFile); 195*815440faSAtari911 } else { 196*815440faSAtari911 @rename($oldFile, $newFile); 197*815440faSAtari911 } 198*815440faSAtari911 } 199*815440faSAtari911 } 200*815440faSAtari911 201*815440faSAtari911 // Rotate current log 202*815440faSAtari911 @rename($logFile, $logFile . '.1'); 203*815440faSAtari911 } 204*815440faSAtari911 205*815440faSAtari911 /** 206*815440faSAtari911 * Get client IP address 207*815440faSAtari911 * 208*815440faSAtari911 * @return string 209*815440faSAtari911 */ 210*815440faSAtari911 private function getClientIP() { 211*815440faSAtari911 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 212*815440faSAtari911 $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); 213*815440faSAtari911 return trim($ips[0]); 214*815440faSAtari911 } 215*815440faSAtari911 if (!empty($_SERVER['HTTP_X_REAL_IP'])) { 216*815440faSAtari911 return $_SERVER['HTTP_X_REAL_IP']; 217*815440faSAtari911 } 218*815440faSAtari911 return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; 219*815440faSAtari911 } 220*815440faSAtari911 221*815440faSAtari911 /** 222*815440faSAtari911 * Get recent audit entries 223*815440faSAtari911 * 224*815440faSAtari911 * @param int $limit Number of entries to return 225*815440faSAtari911 * @param string|null $action Filter by action type 226*815440faSAtari911 * @return array 227*815440faSAtari911 */ 228*815440faSAtari911 public function getRecentEntries($limit = 100, $action = null) { 229*815440faSAtari911 $logFile = $this->logDir . '/calendar_audit.log'; 230*815440faSAtari911 if (!file_exists($logFile)) return []; 231*815440faSAtari911 232*815440faSAtari911 $entries = []; 233*815440faSAtari911 $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 234*815440faSAtari911 235*815440faSAtari911 if (!$lines) return []; 236*815440faSAtari911 237*815440faSAtari911 // Read from end (most recent first) 238*815440faSAtari911 $lines = array_reverse($lines); 239*815440faSAtari911 240*815440faSAtari911 foreach ($lines as $line) { 241*815440faSAtari911 $entry = json_decode($line, true); 242*815440faSAtari911 if (!$entry) continue; 243*815440faSAtari911 244*815440faSAtari911 if ($action !== null && $entry['action'] !== $action) { 245*815440faSAtari911 continue; 246*815440faSAtari911 } 247*815440faSAtari911 248*815440faSAtari911 $entries[] = $entry; 249*815440faSAtari911 250*815440faSAtari911 if (count($entries) >= $limit) break; 251*815440faSAtari911 } 252*815440faSAtari911 253*815440faSAtari911 return $entries; 254*815440faSAtari911 } 255*815440faSAtari911 256*815440faSAtari911 /** 257*815440faSAtari911 * Get audit entries for a specific date range 258*815440faSAtari911 * 259*815440faSAtari911 * @param string $startDate Start date (Y-m-d) 260*815440faSAtari911 * @param string $endDate End date (Y-m-d) 261*815440faSAtari911 * @return array 262*815440faSAtari911 */ 263*815440faSAtari911 public function getEntriesByDateRange($startDate, $endDate) { 264*815440faSAtari911 $logFile = $this->logDir . '/calendar_audit.log'; 265*815440faSAtari911 if (!file_exists($logFile)) return []; 266*815440faSAtari911 267*815440faSAtari911 $startTime = strtotime($startDate . ' 00:00:00'); 268*815440faSAtari911 $endTime = strtotime($endDate . ' 23:59:59'); 269*815440faSAtari911 270*815440faSAtari911 $entries = []; 271*815440faSAtari911 $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 272*815440faSAtari911 273*815440faSAtari911 if (!$lines) return []; 274*815440faSAtari911 275*815440faSAtari911 foreach ($lines as $line) { 276*815440faSAtari911 $entry = json_decode($line, true); 277*815440faSAtari911 if (!$entry) continue; 278*815440faSAtari911 279*815440faSAtari911 $entryTime = $entry['unix_time'] ?? strtotime($entry['timestamp']); 280*815440faSAtari911 281*815440faSAtari911 if ($entryTime >= $startTime && $entryTime <= $endTime) { 282*815440faSAtari911 $entries[] = $entry; 283*815440faSAtari911 } 284*815440faSAtari911 } 285*815440faSAtari911 286*815440faSAtari911 return $entries; 287*815440faSAtari911 } 288*815440faSAtari911 289*815440faSAtari911 /** 290*815440faSAtari911 * Enable or disable audit logging 291*815440faSAtari911 * 292*815440faSAtari911 * @param bool $enabled 293*815440faSAtari911 */ 294*815440faSAtari911 public function setEnabled($enabled) { 295*815440faSAtari911 $this->enabled = (bool)$enabled; 296*815440faSAtari911 } 297*815440faSAtari911 298*815440faSAtari911 /** 299*815440faSAtari911 * Check if audit logging is enabled 300*815440faSAtari911 * 301*815440faSAtari911 * @return bool 302*815440faSAtari911 */ 303*815440faSAtari911 public function isEnabled() { 304*815440faSAtari911 return $this->enabled; 305*815440faSAtari911 } 306*815440faSAtari911 307*815440faSAtari911 /** 308*815440faSAtari911 * Get the audit log directory path 309*815440faSAtari911 * 310*815440faSAtari911 * @return string 311*815440faSAtari911 */ 312*815440faSAtari911 public function getLogDir() { 313*815440faSAtari911 return $this->logDir; 314*815440faSAtari911 } 315*815440faSAtari911 316*815440faSAtari911 /** 317*815440faSAtari911 * Get total size of all audit logs 318*815440faSAtari911 * 319*815440faSAtari911 * @return int Size in bytes 320*815440faSAtari911 */ 321*815440faSAtari911 public function getTotalLogSize() { 322*815440faSAtari911 $total = 0; 323*815440faSAtari911 $files = glob($this->logDir . '/calendar_audit.log*'); 324*815440faSAtari911 foreach ($files as $file) { 325*815440faSAtari911 $total += filesize($file); 326*815440faSAtari911 } 327*815440faSAtari911 return $total; 328*815440faSAtari911 } 329*815440faSAtari911 330*815440faSAtari911 /** 331*815440faSAtari911 * Clear all audit logs (use with caution) 332*815440faSAtari911 * 333*815440faSAtari911 * @return bool 334*815440faSAtari911 */ 335*815440faSAtari911 public function clearLogs() { 336*815440faSAtari911 $files = glob($this->logDir . '/calendar_audit.log*'); 337*815440faSAtari911 foreach ($files as $file) { 338*815440faSAtari911 @unlink($file); 339*815440faSAtari911 } 340*815440faSAtari911 341*815440faSAtari911 // Log the clear action itself 342*815440faSAtari911 $this->log('audit_cleared', ['cleared_files' => count($files)]); 343*815440faSAtari911 344*815440faSAtari911 return true; 345*815440faSAtari911 } 346*815440faSAtari911} 347