Issues (915)

framework/base/ErrorHandler.php (5 issues)

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\base;
10
11
use Yii;
12
use yii\helpers\VarDumper;
13
use yii\web\HttpException;
14
15
/**
16
 * ErrorHandler handles uncaught PHP errors and exceptions.
17
 *
18
 * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
19
 * You can access that instance via `Yii::$app->errorHandler`.
20
 *
21
 * For more details and usage information on ErrorHandler, see the [guide article on handling errors](guide:runtime-handling-errors).
22
 *
23
 * @author Qiang Xue <[email protected]>
24
 * @author Alexander Makarov <[email protected]>
25
 * @author Carsten Brandt <[email protected]>
26
 * @since 2.0
27
 */
28
abstract class ErrorHandler extends Component
29
{
30
    /**
31
     * @event Event an event that is triggered when the handler is called by shutdown function via [[handleFatalError()]].
32
     * @since 2.0.46
33
     */
34
    const EVENT_SHUTDOWN = 'shutdown';
35
36
    /**
37
     * @var bool whether to discard any existing page output before error display. Defaults to true.
38
     */
39
    public $discardExistingOutput = true;
40
    /**
41
     * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
42
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
43
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
44
     * Defaults to 256KB.
45
     */
46
    public $memoryReserveSize = 262144;
47
    /**
48
     * @var \Throwable|null the exception that is being handled currently.
49
     */
50
    public $exception;
51
    /**
52
     * @var bool if true - `handleException()` will finish script with `ExitCode::OK`.
53
     * false - `ExitCode::UNSPECIFIED_ERROR`.
54
     * @since 2.0.36
55
     */
56
    public $silentExitOnException;
57
58
    /**
59
     * @var string Used to reserve memory for fatal error handler.
60
     */
61
    private $_memoryReserve;
62
    /**
63
     * @var \Throwable from HHVM error that stores backtrace
64
     */
65
    private $_hhvmException;
66
    /**
67
     * @var bool whether this instance has been registered using `register()`
68
     */
69
    private $_registered = false;
70
    /**
71
     * @var string the current working directory
72
     */
73
    private $_workingDirectory;
74
75
76 246
    public function init()
77
    {
78 246
        $this->silentExitOnException = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST;
79 246
        parent::init();
80
    }
81
82
    /**
83
     * Register this error handler.
84
     *
85
     * @since 2.0.32 this will not do anything if the error handler was already registered
86
     */
87
    public function register()
88
    {
89
        if (!$this->_registered) {
90
            ini_set('display_errors', false);
0 ignored issues
show
false of type false is incompatible with the type string expected by parameter $value of ini_set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
            ini_set('display_errors', /** @scrutinizer ignore-type */ false);
Loading history...
91
            set_exception_handler([$this, 'handleException']);
92
            if (defined('HHVM_VERSION')) {
93
                set_error_handler([$this, 'handleHhvmError']);
94
            } else {
95
                set_error_handler([$this, 'handleError']);
96
            }
97
            if ($this->memoryReserveSize > 0) {
98
                $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
99
            }
100
            // to restore working directory in shutdown handler
101
            if (PHP_SAPI !== 'cli') {
102
                $this->_workingDirectory = getcwd();
103
            }
104
            register_shutdown_function([$this, 'handleFatalError']);
105
            $this->_registered = true;
106
        }
107
    }
108
109
    /**
110
     * Unregisters this error handler by restoring the PHP error and exception handlers.
111
     * @since 2.0.32 this will not do anything if the error handler was not registered
112
     */
113
    public function unregister()
114
    {
115
        if ($this->_registered) {
116
            $this->_memoryReserve = null;
117
            $this->_workingDirectory = null;
118
            restore_error_handler();
119
            restore_exception_handler();
120
            $this->_registered = false;
121
        }
122
    }
123
124
    /**
125
     * Handles uncaught PHP exceptions.
126
     *
127
     * This method is implemented as a PHP exception handler.
128
     *
129
     * @param \Throwable $exception the exception that is not caught
130
     */
131
    public function handleException($exception)
132
    {
133
        if ($exception instanceof ExitException) {
134
            return;
135
        }
136
137
        $this->exception = $exception;
138
139
        // disable error capturing to avoid recursive errors while handling exceptions
140
        $this->unregister();
141
142
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
143
        // HTTP exceptions will override this value in renderException()
144
        if (PHP_SAPI !== 'cli') {
145
            http_response_code(500);
146
        }
147
148
        try {
149
            $this->logException($exception);
150
            if ($this->discardExistingOutput) {
151
                $this->clearOutput();
152
            }
153
            $this->renderException($exception);
154
            if (!$this->silentExitOnException) {
155
                \Yii::getLogger()->flush(true);
156
                if (defined('HHVM_VERSION')) {
157
                    flush();
158
                }
159
                exit(1);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
160
            }
161
        } catch (\Exception $e) {
162
            // an other exception could be thrown while displaying the exception
163
            $this->handleFallbackExceptionMessage($e, $exception);
164
        } catch (\Throwable $e) {
165
            // additional check for \Throwable introduced in PHP 7
166
            $this->handleFallbackExceptionMessage($e, $exception);
167
        }
168
169
        $this->exception = null;
170
    }
171
172
    /**
173
     * Handles exception thrown during exception processing in [[handleException()]].
174
     * @param \Throwable $exception Exception that was thrown during main exception processing.
175
     * @param \Throwable $previousException Main exception processed in [[handleException()]].
176
     * @since 2.0.11
177
     */
178
    protected function handleFallbackExceptionMessage($exception, $previousException)
179
    {
180
        $msg = "An Error occurred while handling another error:\n";
181
        $msg .= (string) $exception;
182
        $msg .= "\nPrevious exception:\n";
183
        $msg .= (string) $previousException;
184
        if (YII_DEBUG) {
185
            if (PHP_SAPI === 'cli') {
186
                echo $msg . "\n";
187
            } else {
188
                echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
189
            }
190
            $msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);
191
        } else {
192
            echo 'An internal server error occurred.';
193
        }
194
        error_log($msg);
195
        if (defined('HHVM_VERSION')) {
196
            flush();
197
        }
198
        exit(1);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
199
    }
200
201
    /**
202
     * Handles HHVM execution errors such as warnings and notices.
203
     *
204
     * This method is used as a HHVM error handler. It will store exception that will
205
     * be used in fatal error handler
206
     *
207
     * @param int $code the level of the error raised.
208
     * @param string $message the error message.
209
     * @param string $file the filename that the error was raised in.
210
     * @param int $line the line number the error was raised at.
211
     * @param mixed $context
212
     * @param mixed $backtrace trace of error
213
     * @return bool whether the normal error handler continues.
214
     *
215
     * @throws ErrorException
216
     * @since 2.0.6
217
     */
218
    public function handleHhvmError($code, $message, $file, $line, $context, $backtrace)
219
    {
220
        if ($this->handleError($code, $message, $file, $line)) {
221
            return true;
222
        }
223
        if (E_ERROR & $code) {
224
            $exception = new ErrorException($message, $code, $code, $file, $line);
225
            $ref = new \ReflectionProperty('\Exception', 'trace');
226
            $ref->setAccessible(true);
227
            $ref->setValue($exception, $backtrace);
228
            $this->_hhvmException = $exception;
229
        }
230
231
        return false;
232
    }
233
234
    /**
235
     * Handles PHP execution errors such as warnings and notices.
236
     *
237
     * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
238
     *
239
     * @param int $code the level of the error raised.
240
     * @param string $message the error message.
241
     * @param string $file the filename that the error was raised in.
242
     * @param int $line the line number the error was raised at.
243
     * @return bool whether the normal error handler continues.
244
     *
245
     * @throws ErrorException
246
     */
247
    public function handleError($code, $message, $file, $line)
248
    {
249
        if (error_reporting() & $code) {
250
            // load ErrorException manually here because autoloading them will not work
251
            // when error occurs while autoloading a class
252
            if (!class_exists('yii\\base\\ErrorException', false)) {
253
                require_once __DIR__ . '/ErrorException.php';
254
            }
255
            $exception = new ErrorException($message, $code, $code, $file, $line);
256
257
            if (PHP_VERSION_ID < 70400) {
258
                // prior to PHP 7.4 we can't throw exceptions inside of __toString() - it will result a fatal error
259
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
260
                array_shift($trace);
261
                foreach ($trace as $frame) {
262
                    if ($frame['function'] === '__toString') {
263
                        $this->handleException($exception);
264
                        if (defined('HHVM_VERSION')) {
265
                            flush();
266
                        }
267
                        exit(1);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
268
                    }
269
                }
270
            }
271
272
            throw $exception;
273
        }
274
275
        return false;
276
    }
277
278
    /**
279
     * Handles fatal PHP errors.
280
     */
281
    public function handleFatalError()
282
    {
283
        unset($this->_memoryReserve);
284
285
        if (isset($this->_workingDirectory)) {
286
            // fix working directory for some Web servers e.g. Apache
287
            chdir($this->_workingDirectory);
288
            // flush memory
289
            unset($this->_workingDirectory);
290
        }
291
292
        $error = error_get_last();
293
        if ($error === null) {
294
            return;
295
        }
296
297
        // load ErrorException manually here because autoloading them will not work
298
        // when error occurs while autoloading a class
299
        if (!class_exists('yii\\base\\ErrorException', false)) {
300
            require_once __DIR__ . '/ErrorException.php';
301
        }
302
        if (!ErrorException::isFatalError($error)) {
303
            return;
304
        }
305
306
        if (!empty($this->_hhvmException)) {
307
            $this->exception = $this->_hhvmException;
308
        } else {
309
            $this->exception = new ErrorException(
310
                $error['message'],
311
                $error['type'],
312
                $error['type'],
313
                $error['file'],
314
                $error['line']
315
            );
316
        }
317
        unset($error);
318
319
        $this->logException($this->exception);
320
321
        if ($this->discardExistingOutput) {
322
            $this->clearOutput();
323
        }
324
        $this->renderException($this->exception);
325
326
        // need to explicitly flush logs because exit() next will terminate the app immediately
327
        Yii::getLogger()->flush(true);
328
        if (defined('HHVM_VERSION')) {
329
            flush();
330
        }
331
332
        $this->trigger(static::EVENT_SHUTDOWN);
333
334
        // ensure it is called after user-defined shutdown functions
335
        register_shutdown_function(function () {
336
            exit(1);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
337
        });
338
    }
339
340
    /**
341
     * Renders the exception.
342
     * @param \Throwable $exception the exception to be rendered.
343
     */
344
    abstract protected function renderException($exception);
345
346
    /**
347
     * Logs the given exception.
348
     * @param \Throwable $exception the exception to be logged
349
     * @since 2.0.3 this method is now public.
350
     */
351
    public function logException($exception)
352
    {
353
        $category = get_class($exception);
354
        if ($exception instanceof HttpException) {
355
            $category = 'yii\\web\\HttpException:' . $exception->statusCode;
356
        } elseif ($exception instanceof \ErrorException) {
357
            $category .= ':' . $exception->getSeverity();
358
        }
359
        Yii::error($exception, $category);
360
    }
361
362
    /**
363
     * Removes all output echoed before calling this method.
364
     */
365
    public function clearOutput()
366
    {
367
        // the following manual level counting is to deal with zlib.output_compression set to On
368
        for ($level = ob_get_level(); $level > 0; --$level) {
369
            if (!@ob_end_clean()) {
370
                ob_clean();
371
            }
372
        }
373
    }
374
375
    /**
376
     * Converts an exception into a PHP error.
377
     *
378
     * This method can be used to convert exceptions inside of methods like `__toString()`
379
     * to PHP errors because exceptions cannot be thrown inside of them.
380
     * @param \Throwable $exception the exception to convert to a PHP error.
381
     * @return never
382
     */
383
    public static function convertExceptionToError($exception)
384
    {
385
        trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
386
    }
387
388
    /**
389
     * Converts an exception into a simple string.
390
     * @param \Throwable $exception the exception being converted
391
     * @return string the string representation of the exception.
392
     */
393 1
    public static function convertExceptionToString($exception)
394
    {
395 1
        if ($exception instanceof UserException) {
396
            return "{$exception->getName()}: {$exception->getMessage()}";
397
        }
398
399 1
        if (YII_DEBUG) {
400 1
            return static::convertExceptionToVerboseString($exception);
401
        }
402
403
        return 'An internal server error occurred.';
404
    }
405
406
    /**
407
     * Converts an exception into a string that has verbose information about the exception and its trace.
408
     * @param \Throwable $exception the exception being converted
409
     * @return string the string representation of the exception.
410
     *
411
     * @since 2.0.14
412
     */
413 2
    public static function convertExceptionToVerboseString($exception)
414
    {
415 2
        if ($exception instanceof Exception) {
416
            $message = "Exception ({$exception->getName()})";
417 2
        } elseif ($exception instanceof ErrorException) {
418
            $message = (string)$exception->getName();
419
        } else {
420 2
            $message = 'Exception';
421
        }
422 2
        $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
423 2
            . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
424 2
            . "Stack trace:\n" . $exception->getTraceAsString();
425
426 2
        return $message;
427
    }
428
}
429