]> git.mxchange.org Git - friendica.git/blob - src/Core/Logger/Handler/ErrorHandler.php
Issue 13221: Diaspora posts are now stored correctly
[friendica.git] / src / Core / Logger / Handler / ErrorHandler.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 declare(strict_types=1);
23
24 namespace Friendica\Core\Logger\Handler;
25
26 use Psr\Log\LoggerInterface;
27 use Psr\Log\LogLevel;
28 use Throwable;
29
30 /**
31  * A facility to enable logging of runtime errors, exceptions and fatal errors.
32  *
33  * Quick setup: <code>ErrorHandler::register($logger);</code>
34  */
35 class ErrorHandler
36 {
37         /** @var LoggerInterface */
38         private $logger;
39
40         /** @var ?callable */
41         private $previousExceptionHandler = null;
42         /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
43         private $uncaughtExceptionLevelMap = [];
44
45         /** @var callable|true|null */
46         private $previousErrorHandler = null;
47         /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */
48         private $errorLevelMap = [];
49         /** @var bool */
50         private $handleOnlyReportedErrors = true;
51
52         /** @var bool */
53         private $hasFatalErrorHandler = false;
54         /** @var LogLevel::* */
55         private $fatalLevel = LogLevel::ALERT;
56         /** @var ?string */
57         private $reservedMemory = null;
58         /** @var ?mixed */
59         private $lastFatalTrace;
60         /** @var int[] */
61         private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
62
63         public function __construct(LoggerInterface $logger)
64         {
65                 $this->logger = $logger;
66         }
67
68         /**
69          * Registers a new ErrorHandler for a given Logger
70          *
71          * By default it will handle errors, exceptions and fatal errors
72          *
73          * @param  LoggerInterface                        $logger
74          * @param  array<int, LogLevel::*>|false          $errorLevelMap     an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
75          * @param  array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling
76          * @param  LogLevel::*|null|false                 $fatalLevel        a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling
77          *
78          * @return ErrorHandler
79          */
80         public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self
81         {
82                 /** @phpstan-ignore-next-line */
83                 $handler = new static($logger);
84                 if ($errorLevelMap !== false) {
85                         $handler->registerErrorHandler($errorLevelMap);
86                 }
87                 if ($exceptionLevelMap !== false) {
88                         $handler->registerExceptionHandler($exceptionLevelMap);
89                 }
90                 if ($fatalLevel !== false) {
91                         $handler->registerFatalHandler($fatalLevel);
92                 }
93
94                 return $handler;
95         }
96
97         /**
98          * Stringify the class of the given object for logging purpose
99          *
100          * @param object $object An object to retrieve the class
101          *
102          * @return string the classname of the object
103          */
104         public static function getClass(object $object): string
105         {
106                 $class = \get_class($object);
107
108                 if (false === ($pos = \strpos($class, "@anonymous\0"))) {
109                         return $class;
110                 }
111
112                 if (false === ($parent = \get_parent_class($class))) {
113                         return \substr($class, 0, $pos + 10);
114                 }
115
116                 return $parent . '@anonymous';
117         }
118
119         /**
120          * @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping
121          * @param bool                             $callPrevious Set to true, if a previously defined exception handler should be called after handling this exception
122          *
123          * @return $this
124          */
125         public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self
126         {
127                 $prev = set_exception_handler(function (Throwable $e): void {
128                         $this->handleException($e);
129                 });
130                 $this->uncaughtExceptionLevelMap = $levelMap;
131                 foreach ($this->defaultExceptionLevelMap() as $class => $level) {
132                         if (!isset($this->uncaughtExceptionLevelMap[$class])) {
133                                 $this->uncaughtExceptionLevelMap[$class] = $level;
134                         }
135                 }
136                 if ($callPrevious && $prev) {
137                         $this->previousExceptionHandler = $prev;
138                 }
139
140                 return $this;
141         }
142
143         /**
144          * @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping
145          * @param bool                    $callPrevious Set to true, if a previously defined exception handler should be called after handling this exception
146          * @param int                     $errorTypes a Mask for masking the errortypes, which should be handled by this error handler
147          * @param bool                    $handleOnlyReportedErrors Set to true, only errors set per error_reporting() will be logged
148          *
149          * @return $this
150          */
151         public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self
152         {
153                 $prev                = set_error_handler([$this, 'handleError'], $errorTypes);
154                 $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
155                 if ($callPrevious) {
156                         $this->previousErrorHandler = $prev ?: true;
157                 } else {
158                         $this->previousErrorHandler = null;
159                 }
160
161                 $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
162
163                 return $this;
164         }
165
166         /**
167          * @param LogLevel::*|null $level              a LogLevel::* constant, null to use the default LogLevel::ALERT
168          * @param int              $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done
169          *
170          * @return $this
171          */
172         public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self
173         {
174                 register_shutdown_function([$this, 'handleFatalError']);
175
176                 $this->reservedMemory       = str_repeat(' ', 1024 * $reservedMemorySize);
177                 $this->fatalLevel           = null === $level ? LogLevel::ALERT : $level;
178                 $this->hasFatalErrorHandler = true;
179
180                 return $this;
181         }
182
183         /**
184          * @return array<class-string, LogLevel::*>
185          */
186         protected function defaultExceptionLevelMap(): array
187         {
188                 return [
189                         'ParseError' => LogLevel::CRITICAL,
190                         'Throwable'  => LogLevel::ERROR,
191                 ];
192         }
193
194         /**
195          * @return array<int, LogLevel::*>
196          */
197         protected function defaultErrorLevelMap(): array
198         {
199                 return [
200                         E_ERROR             => LogLevel::CRITICAL,
201                         E_WARNING           => LogLevel::WARNING,
202                         E_PARSE             => LogLevel::ALERT,
203                         E_NOTICE            => LogLevel::NOTICE,
204                         E_CORE_ERROR        => LogLevel::CRITICAL,
205                         E_CORE_WARNING      => LogLevel::WARNING,
206                         E_COMPILE_ERROR     => LogLevel::ALERT,
207                         E_COMPILE_WARNING   => LogLevel::WARNING,
208                         E_USER_ERROR        => LogLevel::ERROR,
209                         E_USER_WARNING      => LogLevel::WARNING,
210                         E_USER_NOTICE       => LogLevel::NOTICE,
211                         E_STRICT            => LogLevel::NOTICE,
212                         E_RECOVERABLE_ERROR => LogLevel::ERROR,
213                         E_DEPRECATED        => LogLevel::NOTICE,
214                         E_USER_DEPRECATED   => LogLevel::NOTICE,
215                 ];
216         }
217
218         /**
219          * The Exception handler
220          *
221          * @param Throwable $e The Exception to handle
222          */
223         private function handleException(Throwable $e): void
224         {
225                 $level = LogLevel::ERROR;
226                 foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) {
227                         if ($e instanceof $class) {
228                                 $level = $candidate;
229                                 break;
230                         }
231                 }
232
233                 $this->logger->log(
234                         $level,
235                         sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
236                         ['exception' => $e]
237                 );
238
239                 if ($this->previousExceptionHandler) {
240                         ($this->previousExceptionHandler)($e);
241                 }
242
243                 if (!headers_sent() && !ini_get('display_errors')) {
244                         http_response_code(500);
245                 }
246
247                 exit(255);
248         }
249
250         /**
251          * The Error handler
252          *
253          * @private
254          *
255          * @param int        $code    The PHP error code
256          * @param string     $message The error message
257          * @param string     $file    If possible, set the file at which the failure occurred
258          * @param int        $line
259          * @param array|null $context If possible, add a context to the error for better analysis
260          *
261          * @return bool
262          */
263         public function handleError(int $code, string $message, string $file = '', int $line = 0, ?array $context = []): bool
264         {
265                 if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
266                         return false;
267                 }
268
269                 // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
270                 if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
271                         $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
272                         $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]);
273                 } else {
274                         $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
275                         array_shift($trace); // Exclude handleError from trace
276                         $this->lastFatalTrace = $trace;
277                 }
278
279                 if ($this->previousErrorHandler === true) {
280                         return false;
281                 } elseif ($this->previousErrorHandler) {
282                         return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context);
283                 }
284
285                 return true;
286         }
287
288         /**
289          * @private
290          */
291         public function handleFatalError(): void
292         {
293                 $this->reservedMemory = '';
294
295                 $lastError = error_get_last();
296                 if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
297                         $this->logger->log(
298                                 $this->fatalLevel,
299                                 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
300                                 ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace]
301                         );
302                 }
303         }
304
305         /**
306          * @param mixed $code
307          *
308          * @return string
309          */
310         private static function codeToString($code): string
311         {
312                 switch ($code) {
313                         case E_ERROR:
314                                 return 'E_ERROR';
315                         case E_WARNING:
316                                 return 'E_WARNING';
317                         case E_PARSE:
318                                 return 'E_PARSE';
319                         case E_NOTICE:
320                                 return 'E_NOTICE';
321                         case E_CORE_ERROR:
322                                 return 'E_CORE_ERROR';
323                         case E_CORE_WARNING:
324                                 return 'E_CORE_WARNING';
325                         case E_COMPILE_ERROR:
326                                 return 'E_COMPILE_ERROR';
327                         case E_COMPILE_WARNING:
328                                 return 'E_COMPILE_WARNING';
329                         case E_USER_ERROR:
330                                 return 'E_USER_ERROR';
331                         case E_USER_WARNING:
332                                 return 'E_USER_WARNING';
333                         case E_USER_NOTICE:
334                                 return 'E_USER_NOTICE';
335                         case E_STRICT:
336                                 return 'E_STRICT';
337                         case E_RECOVERABLE_ERROR:
338                                 return 'E_RECOVERABLE_ERROR';
339                         case E_DEPRECATED:
340                                 return 'E_DEPRECATED';
341                         case E_USER_DEPRECATED:
342                                 return 'E_USER_DEPRECATED';
343                 }
344
345                 return 'Unknown PHP error';
346         }
347 }