1<?php
2
3/*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Monolog;
13
14use Psr\Log\LoggerInterface;
15use Psr\Log\LogLevel;
16use Monolog\Handler\AbstractHandler;
17
18/**
19 * Monolog error handler
20 *
21 * A facility to enable logging of runtime errors, exceptions and fatal errors.
22 *
23 * Quick setup: <code>ErrorHandler::register($logger);</code>
24 *
25 * @author Jordi Boggiano <j.boggiano@seld.be>
26 */
27class ErrorHandler
28{
29    private $logger;
30
31    private $previousExceptionHandler;
32    private $uncaughtExceptionLevel;
33
34    private $previousErrorHandler;
35    private $errorLevelMap;
36    private $handleOnlyReportedErrors;
37
38    private $hasFatalErrorHandler;
39    private $fatalLevel;
40    private $reservedMemory;
41    private $lastFatalTrace;
42    private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR);
43
44    public function __construct(LoggerInterface $logger)
45    {
46        $this->logger = $logger;
47    }
48
49    /**
50     * Registers a new ErrorHandler for a given Logger
51     *
52     * By default it will handle errors, exceptions and fatal errors
53     *
54     * @param  LoggerInterface $logger
55     * @param  array|false     $errorLevelMap  an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
56     * @param  int|false       $exceptionLevel a LogLevel::* constant, or false to disable exception handling
57     * @param  int|false       $fatalLevel     a LogLevel::* constant, or false to disable fatal error handling
58     * @return ErrorHandler
59     */
60    public static function register(LoggerInterface $logger, $errorLevelMap = array(), $exceptionLevel = null, $fatalLevel = null)
61    {
62        //Forces the autoloader to run for LogLevel. Fixes an autoload issue at compile-time on PHP5.3. See https://github.com/Seldaek/monolog/pull/929
63        class_exists('\\Psr\\Log\\LogLevel', true);
64
65        /** @phpstan-ignore-next-line */
66        $handler = new static($logger);
67        if ($errorLevelMap !== false) {
68            $handler->registerErrorHandler($errorLevelMap);
69        }
70        if ($exceptionLevel !== false) {
71            $handler->registerExceptionHandler($exceptionLevel);
72        }
73        if ($fatalLevel !== false) {
74            $handler->registerFatalHandler($fatalLevel);
75        }
76
77        return $handler;
78    }
79
80    public function registerExceptionHandler($level = null, $callPrevious = true)
81    {
82        $prev = set_exception_handler(array($this, 'handleException'));
83        $this->uncaughtExceptionLevel = $level;
84        if ($callPrevious && $prev) {
85            $this->previousExceptionHandler = $prev;
86        }
87    }
88
89    public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1, $handleOnlyReportedErrors = true)
90    {
91        $prev = set_error_handler(array($this, 'handleError'), $errorTypes);
92        $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
93        if ($callPrevious) {
94            $this->previousErrorHandler = $prev ?: true;
95        }
96
97        $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
98    }
99
100    public function registerFatalHandler($level = null, $reservedMemorySize = 20)
101    {
102        register_shutdown_function(array($this, 'handleFatalError'));
103
104        $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
105        $this->fatalLevel = $level;
106        $this->hasFatalErrorHandler = true;
107    }
108
109    protected function defaultErrorLevelMap()
110    {
111        return array(
112            E_ERROR             => LogLevel::CRITICAL,
113            E_WARNING           => LogLevel::WARNING,
114            E_PARSE             => LogLevel::ALERT,
115            E_NOTICE            => LogLevel::NOTICE,
116            E_CORE_ERROR        => LogLevel::CRITICAL,
117            E_CORE_WARNING      => LogLevel::WARNING,
118            E_COMPILE_ERROR     => LogLevel::ALERT,
119            E_COMPILE_WARNING   => LogLevel::WARNING,
120            E_USER_ERROR        => LogLevel::ERROR,
121            E_USER_WARNING      => LogLevel::WARNING,
122            E_USER_NOTICE       => LogLevel::NOTICE,
123            E_STRICT            => LogLevel::NOTICE,
124            E_RECOVERABLE_ERROR => LogLevel::ERROR,
125            E_DEPRECATED        => LogLevel::NOTICE,
126            E_USER_DEPRECATED   => LogLevel::NOTICE,
127        );
128    }
129
130    /**
131     * @private
132     */
133    public function handleException($e)
134    {
135        $this->logger->log(
136            $this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel,
137            sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
138            array('exception' => $e)
139        );
140
141        if ($this->previousExceptionHandler) {
142            call_user_func($this->previousExceptionHandler, $e);
143        }
144
145        exit(255);
146    }
147
148    /**
149     * @private
150     */
151    public function handleError($code, $message, $file = '', $line = 0, $context = array())
152    {
153        if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
154            return;
155        }
156
157        // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
158        if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
159            $level = isset($this->errorLevelMap[$code]) ? $this->errorLevelMap[$code] : LogLevel::CRITICAL;
160            $this->logger->log($level, self::codeToString($code).': '.$message, array('code' => $code, 'message' => $message, 'file' => $file, 'line' => $line));
161        } else {
162            // http://php.net/manual/en/function.debug-backtrace.php
163            // As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added.
164            // Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'.
165            $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS);
166            array_shift($trace); // Exclude handleError from trace
167            $this->lastFatalTrace = $trace;
168        }
169
170        if ($this->previousErrorHandler === true) {
171            return false;
172        } elseif ($this->previousErrorHandler) {
173            return call_user_func($this->previousErrorHandler, $code, $message, $file, $line, $context);
174        }
175    }
176
177    /**
178     * @private
179     */
180    public function handleFatalError()
181    {
182        $this->reservedMemory = null;
183
184        $lastError = error_get_last();
185        if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
186            $this->logger->log(
187                $this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel,
188                'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
189                array('code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace)
190            );
191
192            if ($this->logger instanceof Logger) {
193                foreach ($this->logger->getHandlers() as $handler) {
194                    if ($handler instanceof AbstractHandler) {
195                        $handler->close();
196                    }
197                }
198            }
199        }
200    }
201
202    private static function codeToString($code)
203    {
204        switch ($code) {
205            case E_ERROR:
206                return 'E_ERROR';
207            case E_WARNING:
208                return 'E_WARNING';
209            case E_PARSE:
210                return 'E_PARSE';
211            case E_NOTICE:
212                return 'E_NOTICE';
213            case E_CORE_ERROR:
214                return 'E_CORE_ERROR';
215            case E_CORE_WARNING:
216                return 'E_CORE_WARNING';
217            case E_COMPILE_ERROR:
218                return 'E_COMPILE_ERROR';
219            case E_COMPILE_WARNING:
220                return 'E_COMPILE_WARNING';
221            case E_USER_ERROR:
222                return 'E_USER_ERROR';
223            case E_USER_WARNING:
224                return 'E_USER_WARNING';
225            case E_USER_NOTICE:
226                return 'E_USER_NOTICE';
227            case E_STRICT:
228                return 'E_STRICT';
229            case E_RECOVERABLE_ERROR:
230                return 'E_RECOVERABLE_ERROR';
231            case E_DEPRECATED:
232                return 'E_DEPRECATED';
233            case E_USER_DEPRECATED:
234                return 'E_USER_DEPRECATED';
235        }
236
237        return 'Unknown PHP error';
238    }
239}
240