1<?php
2
3namespace dokuwiki;
4
5use dokuwiki\Extension\Event;
6
7/**
8 * Log messages to a daily log file
9 */
10class Logger
11{
12    public const LOG_ERROR = 'error';
13    public const LOG_DEPRECATED = 'deprecated';
14    public const LOG_DEBUG = 'debug';
15
16    /** @var Logger[] */
17    protected static $instances;
18
19    /** @var string what kind of log is this */
20    protected $facility;
21
22    protected $isLogging = true;
23
24    /**
25     * Logger constructor.
26     *
27     * @param string $facility The type of log
28     */
29    protected function __construct($facility)
30    {
31        global $conf;
32        $this->facility = $facility;
33
34        // Should logging be disabled for this facility?
35        $dontlog = explode(',', $conf['dontlog']);
36        $dontlog = array_map('trim', $dontlog);
37        if (in_array($facility, $dontlog)) $this->isLogging = false;
38    }
39
40    /**
41     * Return a Logger instance for the given facility
42     *
43     * @param string $facility The type of log
44     * @return Logger
45     */
46    public static function getInstance($facility = self::LOG_ERROR)
47    {
48        if (empty(self::$instances[$facility])) {
49            self::$instances[$facility] = new Logger($facility);
50        }
51        return self::$instances[$facility];
52    }
53
54    /**
55     * Convenience method to directly log to the error log
56     *
57     * @param string $message The log message
58     * @param mixed $details Any details that should be added to the log entry
59     * @param string $file A source filename if this is related to a source position
60     * @param int $line A line number for the above file
61     * @return bool has a log been written?
62     */
63    public static function error($message, $details = null, $file = '', $line = 0)
64    {
65        return self::getInstance(self::LOG_ERROR)->log(
66            $message,
67            $details,
68            $file,
69            $line
70        );
71    }
72
73    /**
74     * Convenience method to directly log to the debug log
75     *
76     * @param string $message The log message
77     * @param mixed $details Any details that should be added to the log entry
78     * @param string $file A source filename if this is related to a source position
79     * @param int $line A line number for the above file
80     * @return bool has a log been written?
81     */
82    public static function debug($message, $details = null, $file = '', $line = 0)
83    {
84        return self::getInstance(self::LOG_DEBUG)->log(
85            $message,
86            $details,
87            $file,
88            $line
89        );
90    }
91
92    /**
93     * Convenience method to directly log to the deprecation log
94     *
95     * @param string $message The log message
96     * @param mixed $details Any details that should be added to the log entry
97     * @param string $file A source filename if this is related to a source position
98     * @param int $line A line number for the above file
99     * @return bool has a log been written?
100     */
101    public static function deprecated($message, $details = null, $file = '', $line = 0)
102    {
103        return self::getInstance(self::LOG_DEPRECATED)->log(
104            $message,
105            $details,
106            $file,
107            $line
108        );
109    }
110
111    /**
112     * Log a message to the facility log
113     *
114     * @param string $message The log message
115     * @param mixed $details Any details that should be added to the log entry
116     * @param string $file A source filename if this is related to a source position
117     * @param int $line A line number for the above file
118     * @triggers LOGGER_DATA_FORMAT can be used to change the logged data or intercept it
119     * @return bool has a log been written?
120     */
121    public function log($message, $details = null, $file = '', $line = 0)
122    {
123        global $EVENT_HANDLER;
124        if (!$this->isLogging) return false;
125
126        $datetime = time();
127        $data = [
128            'facility' => $this->facility,
129            'datetime' => $datetime,
130            'message' => $message,
131            'details' => $details,
132            'file' => $file,
133            'line' => $line,
134            'loglines' => [],
135            'logfile' => $this->getLogfile($datetime),
136        ];
137
138        if ($EVENT_HANDLER !== null) {
139            $event = new Event('LOGGER_DATA_FORMAT', $data);
140            if ($event->advise_before()) {
141                $data['loglines'] = $this->formatLogLines($data);
142            }
143            $event->advise_after();
144        } else {
145            // The event system is not yet available, to ensure the log isn't lost even on
146            // fatal errors, the default action is executed
147            $data['loglines'] = $this->formatLogLines($data);
148        }
149
150        // only log when any data available
151        if (count($data['loglines'])) {
152            return $this->writeLogLines($data['loglines'], $data['logfile']);
153        } else {
154            return false;
155        }
156    }
157
158    /**
159     * Is this logging instace actually logging?
160     *
161     * @return bool
162     */
163    public function isLogging()
164    {
165        return $this->isLogging;
166    }
167
168    /**
169     * Formats the given data as loglines
170     *
171     * @param array $data Event data from LOGGER_DATA_FORMAT
172     * @return string[] the lines to log
173     */
174    protected function formatLogLines($data)
175    {
176        extract($data);
177
178        // details are logged indented
179        if ($details) {
180            if (!is_string($details)) {
181                $details = json_encode($details, JSON_PRETTY_PRINT);
182            }
183            $details = explode("\n", $details);
184            $loglines = array_map(static fn($line) => '  ' . $line, $details);
185        } elseif ($details) {
186            $loglines = [$details];
187        } else {
188            $loglines = [];
189        }
190
191        // datetime, fileline, message
192        $logline = date('Y-m-d H:i:s', $datetime) . "\t";
193        if ($file) {
194            $logline .= $file;
195            if ($line) $logline .= "($line)";
196        }
197        $logline .= "\t" . $message;
198        array_unshift($loglines, $logline);
199
200        return $loglines;
201    }
202
203    /**
204     * Construct the log file for the given day
205     *
206     * @param false|string|int $date Date to access, false for today
207     * @return string
208     */
209    public function getLogfile($date = false)
210    {
211        global $conf;
212
213        if ($date !== null && !is_numeric($date)) {
214            $date = strtotime($date);
215        }
216        if (!$date) $date = time();
217
218        return $conf['logdir'] . '/' . $this->facility . '/' . date('Y-m-d', $date) . '.log';
219    }
220
221    /**
222     * Write the given lines to today's facility log
223     *
224     * @param string[] $lines the raw lines to append to the log
225     * @param string $logfile where to write to
226     * @return bool true if the log was written
227     */
228    protected function writeLogLines($lines, $logfile)
229    {
230        if (defined('DOKU_UNITTEST')) {
231            $stderr = fopen('php://stderr', 'w');
232            fwrite($stderr, "\n[" . $this->facility . '] ' . implode("\n", $lines) . "\n");
233            fclose($stderr);
234        }
235        return io_saveFile($logfile, implode("\n", $lines) . "\n", true);
236    }
237}
238