xref: /plugin/calendar/classes/AuditLogger.php (revision 2866e8271e4daef3b32eacb3a9082d02159b592b)
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