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