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