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