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