]> git.mxchange.org Git - friendica.git/blob - src/Core/Logger/Handler/ErrorHandler.php
Add extended ErrorHandling
[friendica.git] / src / Core / Logger / Handler / ErrorHandler.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, 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
29 /**
30  * A facility to enable logging of runtime errors, exceptions and fatal errors.
31  *
32  * Quick setup: <code>ErrorHandler::register($logger);</code>
33  */
34 class ErrorHandler
35 {
36         /** @var LoggerInterface */
37         private $logger;
38
39         /** @var ?callable */
40         private $previousExceptionHandler = null;
41         /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
42         private $uncaughtExceptionLevelMap = [];
43
44         /** @var callable|true|null */
45         private $previousErrorHandler = null;
46         /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */
47         private $errorLevelMap = [];
48         /** @var bool */
49         private $handleOnlyReportedErrors = true;
50
51         /** @var bool */
52         private $hasFatalErrorHandler = false;
53         /** @var LogLevel::* */
54         private $fatalLevel = LogLevel::ALERT;
55         /** @var ?string */
56         private $reservedMemory = null;
57         /** @var ?mixed */
58         private $lastFatalTrace;
59         /** @var int[] */
60         private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
61
62         public function __construct(LoggerInterface $logger)
63         {
64                 $this->logger = $logger;
65         }
66
67         /**
68          * Registers a new ErrorHandler for a given Logger
69          *
70          * By default it will handle errors, exceptions and fatal errors
71          *
72          * @param  LoggerInterface                        $logger
73          * @param  array<int, LogLevel::*>|false          $errorLevelMap     an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
74          * @param  array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling
75          * @param  LogLevel::*|null|false                 $fatalLevel        a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling
76          * @return ErrorHandler
77          */
78         public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self
79         {
80                 /** @phpstan-ignore-next-line */
81                 $handler = new static($logger);
82                 if ($errorLevelMap !== false) {
83                         $handler->registerErrorHandler($errorLevelMap);
84                 }
85                 if ($exceptionLevelMap !== false) {
86                         $handler->registerExceptionHandler($exceptionLevelMap);
87                 }
88                 if ($fatalLevel !== false) {
89                         $handler->registerFatalHandler($fatalLevel);
90                 }
91
92                 return $handler;
93         }
94
95         public static function getClass(object $object): string
96         {
97                 $class = \get_class($object);
98
99                 if (false === ($pos = \strpos($class, "@anonymous\0"))) {
100                         return $class;
101                 }
102
103                 if (false === ($parent = \get_parent_class($class))) {
104                         return \substr($class, 0, $pos + 10);
105                 }
106
107                 return $parent . '@anonymous';
108         }
109
110         /**
111          * @param  array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping
112          * @return $this
113          */
114         public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self
115         {
116                 $prev = set_exception_handler(function (\Throwable $e): void {
117                         $this->handleException($e);
118                 });
119                 $this->uncaughtExceptionLevelMap = $levelMap;
120                 foreach ($this->defaultExceptionLevelMap() as $class => $level) {
121                         if (!isset($this->uncaughtExceptionLevelMap[$class])) {
122                                 $this->uncaughtExceptionLevelMap[$class] = $level;
123                         }
124                 }
125                 if ($callPrevious && $prev) {
126                         $this->previousExceptionHandler = $prev;
127                 }
128
129                 return $this;
130         }
131
132         /**
133          * @param  array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping
134          * @return $this
135          */
136         public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self
137         {
138                 $prev                = set_error_handler([$this, 'handleError'], $errorTypes);
139                 $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
140                 if ($callPrevious) {
141                         $this->previousErrorHandler = $prev ?: true;
142                 } else {
143                         $this->previousErrorHandler = null;
144                 }
145
146                 $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
147
148                 return $this;
149         }
150
151         /**
152          * @param LogLevel::*|null $level              a LogLevel::* constant, null to use the default LogLevel::ALERT
153          * @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
154          */
155         public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self
156         {
157                 register_shutdown_function([$this, 'handleFatalError']);
158
159                 $this->reservedMemory       = str_repeat(' ', 1024 * $reservedMemorySize);
160                 $this->fatalLevel           = null === $level ? LogLevel::ALERT : $level;
161                 $this->hasFatalErrorHandler = true;
162
163                 return $this;
164         }
165
166         /**
167          * @return array<class-string, LogLevel::*>
168          */
169         protected function defaultExceptionLevelMap(): array
170         {
171                 return [
172                         'ParseError' => LogLevel::CRITICAL,
173                         'Throwable'  => LogLevel::ERROR,
174                 ];
175         }
176
177         /**
178          * @return array<int, LogLevel::*>
179          */
180         protected function defaultErrorLevelMap(): array
181         {
182                 return [
183                         E_ERROR             => LogLevel::CRITICAL,
184                         E_WARNING           => LogLevel::WARNING,
185                         E_PARSE             => LogLevel::ALERT,
186                         E_NOTICE            => LogLevel::NOTICE,
187                         E_CORE_ERROR        => LogLevel::CRITICAL,
188                         E_CORE_WARNING      => LogLevel::WARNING,
189                         E_COMPILE_ERROR     => LogLevel::ALERT,
190                         E_COMPILE_WARNING   => LogLevel::WARNING,
191                         E_USER_ERROR        => LogLevel::ERROR,
192                         E_USER_WARNING      => LogLevel::WARNING,
193                         E_USER_NOTICE       => LogLevel::NOTICE,
194                         E_STRICT            => LogLevel::NOTICE,
195                         E_RECOVERABLE_ERROR => LogLevel::ERROR,
196                         E_DEPRECATED        => LogLevel::NOTICE,
197                         E_USER_DEPRECATED   => LogLevel::NOTICE,
198                 ];
199         }
200
201         private function handleException(\Throwable $e): void
202         {
203                 $level = LogLevel::ERROR;
204                 foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) {
205                         if ($e instanceof $class) {
206                                 $level = $candidate;
207                                 break;
208                         }
209                 }
210
211                 $this->logger->log(
212                         $level,
213                         sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
214                         ['exception' => $e]
215                 );
216
217                 if ($this->previousExceptionHandler) {
218                         ($this->previousExceptionHandler)($e);
219                 }
220
221                 if (!headers_sent() && !ini_get('display_errors')) {
222                         http_response_code(500);
223                 }
224
225                 exit(255);
226         }
227
228         /**
229          * @private
230          *
231          * @param mixed[] $context
232          */
233         public function handleError(int $code, string $message, string $file = '', int $line = 0, array $context = []): bool
234         {
235                 if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
236                         return false;
237                 }
238
239                 // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
240                 if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
241                         $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
242                         $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]);
243                 } else {
244                         $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
245                         array_shift($trace); // Exclude handleError from trace
246                         $this->lastFatalTrace = $trace;
247                 }
248
249                 if ($this->previousErrorHandler === true) {
250                         return false;
251                 } elseif ($this->previousErrorHandler) {
252                         return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context);
253                 }
254
255                 return true;
256         }
257
258         /**
259          * @private
260          */
261         public function handleFatalError(): void
262         {
263                 $this->reservedMemory = '';
264
265                 $lastError = error_get_last();
266                 if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
267                         $this->logger->log(
268                                 $this->fatalLevel,
269                                 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
270                                 ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace]
271                         );
272                 }
273         }
274
275         /**
276          * @param int $code
277          */
278         private static function codeToString($code): string
279         {
280                 switch ($code) {
281                         case E_ERROR:
282                                 return 'E_ERROR';
283                         case E_WARNING:
284                                 return 'E_WARNING';
285                         case E_PARSE:
286                                 return 'E_PARSE';
287                         case E_NOTICE:
288                                 return 'E_NOTICE';
289                         case E_CORE_ERROR:
290                                 return 'E_CORE_ERROR';
291                         case E_CORE_WARNING:
292                                 return 'E_CORE_WARNING';
293                         case E_COMPILE_ERROR:
294                                 return 'E_COMPILE_ERROR';
295                         case E_COMPILE_WARNING:
296                                 return 'E_COMPILE_WARNING';
297                         case E_USER_ERROR:
298                                 return 'E_USER_ERROR';
299                         case E_USER_WARNING:
300                                 return 'E_USER_WARNING';
301                         case E_USER_NOTICE:
302                                 return 'E_USER_NOTICE';
303                         case E_STRICT:
304                                 return 'E_STRICT';
305                         case E_RECOVERABLE_ERROR:
306                                 return 'E_RECOVERABLE_ERROR';
307                         case E_DEPRECATED:
308                                 return 'E_DEPRECATED';
309                         case E_USER_DEPRECATED:
310                                 return 'E_USER_DEPRECATED';
311                 }
312
313                 return 'Unknown PHP error';
314         }
315 }