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