Completed
Push — master ( 55b06d...9f2a87 )
by Alexander
35:57
created

framework/base/ErrorHandler.php (5 issues)

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\base;
9
10
use Yii;
11
use yii\helpers\VarDumper;
12
use yii\web\HttpException;
13
14
/**
15
 * ErrorHandler handles uncaught PHP errors and exceptions.
16
 *
17
 * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
18
 * You can access that instance via `Yii::$app->errorHandler`.
19
 *
20
 * For more details and usage information on ErrorHandler, see the [guide article on handling errors](guide:runtime-handling-errors).
21
 *
22
 * @author Qiang Xue <[email protected]>
23
 * @author Alexander Makarov <[email protected]>
24
 * @author Carsten Brandt <[email protected]>
25
 * @since 2.0
26
 */
27
abstract class ErrorHandler extends Component
28
{
29
    /**
30
     * @var bool whether to discard any existing page output before error display. Defaults to true.
31
     */
32
    public $discardExistingOutput = true;
33
    /**
34
     * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
35
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
36
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
37
     * Defaults to 256KB.
38
     */
39
    public $memoryReserveSize = 262144;
40
    /**
41
     * @var \Exception|null the exception that is being handled currently.
42
     */
43
    public $exception;
44
45
    /**
46
     * @var string Used to reserve memory for fatal error handler.
47
     */
48
    private $_memoryReserve;
49
    /**
50
     * @var \Exception from HHVM error that stores backtrace
51
     */
52
    private $_hhvmException;
53
54
55
    /**
56
     * Register this error handler.
57
     */
58
    public function register()
59
    {
60
        ini_set('display_errors', false);
0 ignored issues
show
false of type false is incompatible with the type string expected by parameter $newvalue 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

60
        ini_set('display_errors', /** @scrutinizer ignore-type */ false);
Loading history...
61
        set_exception_handler([$this, 'handleException']);
62
        if (defined('HHVM_VERSION')) {
63
            set_error_handler([$this, 'handleHhvmError']);
64
        } else {
65
            set_error_handler([$this, 'handleError']);
66
        }
67
        if ($this->memoryReserveSize > 0) {
68
            $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
69
        }
70
        register_shutdown_function([$this, 'handleFatalError']);
71
    }
72
73
    /**
74
     * Unregisters this error handler by restoring the PHP error and exception handlers.
75
     */
76
    public function unregister()
77
    {
78
        restore_error_handler();
79
        restore_exception_handler();
80
    }
81
82
    /**
83
     * Handles uncaught PHP exceptions.
84
     *
85
     * This method is implemented as a PHP exception handler.
86
     *
87
     * @param \Exception $exception the exception that is not caught
88
     */
89
    public function handleException($exception)
90
    {
91
        if ($exception instanceof ExitException) {
92
            return;
93
        }
94
95
        $this->exception = $exception;
96
97
        // disable error capturing to avoid recursive errors while handling exceptions
98
        $this->unregister();
99
100
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
101
        // HTTP exceptions will override this value in renderException()
102
        if (PHP_SAPI !== 'cli') {
103
            http_response_code(500);
104
        }
105
106
        try {
107
            $this->logException($exception);
108
            if ($this->discardExistingOutput) {
109
                $this->clearOutput();
110
            }
111
            $this->renderException($exception);
112
            if (!YII_ENV_TEST) {
113
                \Yii::getLogger()->flush(true);
114
                if (defined('HHVM_VERSION')) {
115
                    flush();
116
                }
117
                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...
118
            }
119
        } catch (\Exception $e) {
120
            // an other exception could be thrown while displaying the exception
121
            $this->handleFallbackExceptionMessage($e, $exception);
122
        } catch (\Throwable $e) {
123
            // additional check for \Throwable introduced in PHP 7
124
            $this->handleFallbackExceptionMessage($e, $exception);
125
        }
126
127
        $this->exception = null;
128
    }
129
130
    /**
131
     * Handles exception thrown during exception processing in [[handleException()]].
132
     * @param \Exception|\Throwable $exception Exception that was thrown during main exception processing.
133
     * @param \Exception $previousException Main exception processed in [[handleException()]].
134
     * @since 2.0.11
135
     */
136
    protected function handleFallbackExceptionMessage($exception, $previousException)
137
    {
138
        $msg = "An Error occurred while handling another error:\n";
139
        $msg .= (string) $exception;
140
        $msg .= "\nPrevious exception:\n";
141
        $msg .= (string) $previousException;
142
        if (YII_DEBUG) {
143
            if (PHP_SAPI === 'cli') {
144
                echo $msg . "\n";
145
            } else {
146
                echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
147
            }
148
        } else {
149
            echo 'An internal server error occurred.';
150
        }
151
        $msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);
152
        error_log($msg);
153
        if (defined('HHVM_VERSION')) {
154
            flush();
155
        }
156
        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...
157
    }
158
159
    /**
160
     * Handles HHVM execution errors such as warnings and notices.
161
     *
162
     * This method is used as a HHVM error handler. It will store exception that will
163
     * be used in fatal error handler
164
     *
165
     * @param int $code the level of the error raised.
166
     * @param string $message the error message.
167
     * @param string $file the filename that the error was raised in.
168
     * @param int $line the line number the error was raised at.
169
     * @param mixed $context
170
     * @param mixed $backtrace trace of error
171
     * @return bool whether the normal error handler continues.
172
     *
173
     * @throws ErrorException
174
     * @since 2.0.6
175
     */
176
    public function handleHhvmError($code, $message, $file, $line, $context, $backtrace)
177
    {
178
        if ($this->handleError($code, $message, $file, $line)) {
179
            return true;
180
        }
181
        if (E_ERROR & $code) {
182
            $exception = new ErrorException($message, $code, $code, $file, $line);
183
            $ref = new \ReflectionProperty('\Exception', 'trace');
184
            $ref->setAccessible(true);
185
            $ref->setValue($exception, $backtrace);
186
            $this->_hhvmException = $exception;
187
        }
188
189
        return false;
190
    }
191
192
    /**
193
     * Handles PHP execution errors such as warnings and notices.
194
     *
195
     * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
196
     *
197
     * @param int $code the level of the error raised.
198
     * @param string $message the error message.
199
     * @param string $file the filename that the error was raised in.
200
     * @param int $line the line number the error was raised at.
201
     * @return bool whether the normal error handler continues.
202
     *
203
     * @throws ErrorException
204
     */
205
    public function handleError($code, $message, $file, $line)
206
    {
207
        if (error_reporting() & $code) {
208
            // load ErrorException manually here because autoloading them will not work
209
            // when error occurs while autoloading a class
210
            if (!class_exists('yii\\base\\ErrorException', false)) {
211
                require_once __DIR__ . '/ErrorException.php';
212
            }
213
            $exception = new ErrorException($message, $code, $code, $file, $line);
214
215
            // in case error appeared in __toString method we can't throw any exception
216
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
217
            array_shift($trace);
218
            foreach ($trace as $frame) {
219
                if ($frame['function'] === '__toString') {
220
                    $this->handleException($exception);
221
                    if (defined('HHVM_VERSION')) {
222
                        flush();
223
                    }
224
                    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...
225
                }
226
            }
227
228
            throw $exception;
229
        }
230
231
        return false;
232
    }
233
234
    /**
235
     * Handles fatal PHP errors.
236
     */
237
    public function handleFatalError()
238
    {
239
        unset($this->_memoryReserve);
240
241
        // load ErrorException manually here because autoloading them will not work
242
        // when error occurs while autoloading a class
243
        if (!class_exists('yii\\base\\ErrorException', false)) {
244
            require_once __DIR__ . '/ErrorException.php';
245
        }
246
247
        $error = error_get_last();
248
249
        if (ErrorException::isFatalError($error)) {
250
            if (!empty($this->_hhvmException)) {
251
                $exception = $this->_hhvmException;
252
            } else {
253
                $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
254
            }
255
            $this->exception = $exception;
256
257
            $this->logException($exception);
258
259
            if ($this->discardExistingOutput) {
260
                $this->clearOutput();
261
            }
262
            $this->renderException($exception);
263
264
            // need to explicitly flush logs because exit() next will terminate the app immediately
265
            Yii::getLogger()->flush(true);
266
            if (defined('HHVM_VERSION')) {
267
                flush();
268
            }
269
            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...
270
        }
271
    }
272
273
    /**
274
     * Renders the exception.
275
     * @param \Exception $exception the exception to be rendered.
276
     */
277
    abstract protected function renderException($exception);
278
279
    /**
280
     * Logs the given exception.
281
     * @param \Exception $exception the exception to be logged
282
     * @since 2.0.3 this method is now public.
283
     */
284
    public function logException($exception)
285
    {
286
        $category = get_class($exception);
287
        if ($exception instanceof HttpException) {
288
            $category = 'yii\\web\\HttpException:' . $exception->statusCode;
289
        } elseif ($exception instanceof \ErrorException) {
290
            $category .= ':' . $exception->getSeverity();
291
        }
292
        Yii::error($exception, $category);
293
    }
294
295
    /**
296
     * Removes all output echoed before calling this method.
297
     */
298
    public function clearOutput()
299
    {
300
        // the following manual level counting is to deal with zlib.output_compression set to On
301
        for ($level = ob_get_level(); $level > 0; --$level) {
302
            if (!@ob_end_clean()) {
303
                ob_clean();
304
            }
305
        }
306
    }
307
308
    /**
309
     * Converts an exception into a PHP error.
310
     *
311
     * This method can be used to convert exceptions inside of methods like `__toString()`
312
     * to PHP errors because exceptions cannot be thrown inside of them.
313
     * @param \Exception $exception the exception to convert to a PHP error.
314
     */
315
    public static function convertExceptionToError($exception)
316
    {
317
        trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
318
    }
319
320
    /**
321
     * Converts an exception into a simple string.
322
     * @param \Exception|\Error $exception the exception being converted
323
     * @return string the string representation of the exception.
324
     */
325
    public static function convertExceptionToString($exception)
326
    {
327
        if ($exception instanceof UserException) {
328
            return "{$exception->getName()}: {$exception->getMessage()}";
329
        }
330
331
        if (YII_DEBUG) {
332
            return static::convertExceptionToVerboseString($exception);
333
        }
334
335
        return 'An internal server error occurred.';
336
    }
337
338
    /**
339
     * Converts an exception into a string that has verbose information about the exception and its trace.
340
     * @param \Exception|\Error $exception the exception being converted
341
     * @return string the string representation of the exception.
342
     *
343
     * @since 2.0.14
344
     */
345
    public static function convertExceptionToVerboseString($exception)
346
    {
347
        if ($exception instanceof Exception) {
348
            $message = "Exception ({$exception->getName()})";
349
        } elseif ($exception instanceof ErrorException) {
350
            $message = (string)$exception->getName();
351
        } else {
352
            $message = 'Exception';
353
        }
354
        $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
355
            . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
356
            . "Stack trace:\n" . $exception->getTraceAsString();
357
358
        return $message;
359
    }
360
}
361