Completed
Push — 2.1 ( 4d9204...3e6f8b )
by
unknown
11:56
created

ErrorHandler::renderException()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1
c 0
b 0
f 0
ccs 0
cts 0
cp 0
nc 1
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
51
    /**
52
     * Register this error handler
53
     */
54
    public function register()
55
    {
56
        ini_set('display_errors', false);
57
        set_exception_handler([$this, 'handleException']);
58
        set_error_handler([$this, 'handleError']);
59
60
        if ($this->memoryReserveSize > 0) {
61
            $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
62
        }
63
        register_shutdown_function([$this, 'handleFatalError']);
64
    }
65
66
    /**
67
     * Unregisters this error handler by restoring the PHP error and exception handlers.
68
     */
69
    public function unregister()
70
    {
71
        restore_error_handler();
72
        restore_exception_handler();
73
    }
74
75
    /**
76
     * Handles uncaught PHP exceptions.
77
     *
78
     * This method is implemented as a PHP exception handler.
79
     *
80
     * @param \Exception $exception the exception that is not caught
81
     */
82
    public function handleException($exception)
83
    {
84
        if ($exception instanceof ExitException) {
85
            return;
86
        }
87
88
        $this->exception = $exception;
89
90
        // disable error capturing to avoid recursive errors while handling exceptions
91
        $this->unregister();
92
93
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
94
        // HTTP exceptions will override this value in renderException()
95
        if (PHP_SAPI !== 'cli') {
96
            http_response_code(500);
97
        }
98
99
        try {
100
            $this->logException($exception);
101
            if ($this->discardExistingOutput) {
102
                $this->clearOutput();
103
            }
104
            $this->renderException($exception);
105
            if (!YII_ENV_TEST) {
106
                Yii::getProfiler()->flush();
107
                $this->flushLogger();
108
                exit(1);
109
            }
110
        } catch (\Throwable $e) {
111
            // another exception could be thrown while displaying the exception
112
            $this->handleFallbackExceptionMessage($e, $exception);
113
        }
114
115
        $this->exception = null;
116
    }
117
118
    /**
119
     * Handles exception thrown during exception processing in [[handleException()]].
120
     * @param \Throwable $exception Exception that was thrown during main exception processing.
121
     * @param \Exception $previousException Main exception processed in [[handleException()]].
122
     * @since 2.0.11
123
     */
124
    protected function handleFallbackExceptionMessage($exception, $previousException)
125
    {
126
        $msg = "An Error occurred while handling another error:\n";
127
        $msg .= (string) $exception;
128
        $msg .= "\nPrevious exception:\n";
129
        $msg .= (string) $previousException;
130
        if (YII_DEBUG) {
131
            if (PHP_SAPI === 'cli') {
132
                echo $msg . "\n";
133
            } else {
134
                echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
135
            }
136
        } else {
137
            echo 'An internal server error occurred.';
138
        }
139
        $msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);
140
        error_log($msg);
141
        exit(1);
142
    }
143
144
    /**
145
     * Handles PHP execution errors such as warnings and notices.
146
     *
147
     * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
148
     *
149
     * @param int $code the level of the error raised.
150
     * @param string $message the error message.
151
     * @param string $file the filename that the error was raised in.
152
     * @param int $line the line number the error was raised at.
153
     * @return bool whether the normal error handler continues.
154
     *
155
     * @throws ErrorException
156
     */
157
    public function handleError($code, $message, $file, $line)
158
    {
159
        if (error_reporting() & $code) {
160
            // load ErrorException manually here because autoloading them will not work
161
            // when error occurs while autoloading a class
162
            if (!class_exists(ErrorException::class, false)) {
163
                require_once __DIR__ . '/ErrorException.php';
164
            }
165
            $exception = new ErrorException($message, $code, $code, $file, $line);
166
167
            // in case error appeared in __toString method we can't throw any exception
168
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
169
            array_shift($trace);
170
            foreach ($trace as $frame) {
171
                if ($frame['function'] === '__toString') {
172
                    $this->handleException($exception);
173
                    exit(1);
174
                }
175
            }
176
177
            throw $exception;
178
        }
179
        return false;
180
    }
181
182
    /**
183
     * Handles fatal PHP errors
184
     */
185
    public function handleFatalError()
186
    {
187
        unset($this->_memoryReserve);
188
189
        // load ErrorException manually here because autoloading them will not work
190
        // when error occurs while autoloading a class
191
        if (!class_exists(ErrorException::class, false)) {
192
            require_once __DIR__ . '/ErrorException.php';
193
        }
194
195
        $error = error_get_last();
196
197
        if (ErrorException::isFatalError($error)) {
198
            $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
199
            $this->exception = $exception;
200
201
            $this->logException($exception);
202
203
            if ($this->discardExistingOutput) {
204
                $this->clearOutput();
205
            }
206
            $this->renderException($exception);
207
208
            // need to explicitly flush logs because exit() next will terminate the app immediately
209
            Yii::getProfiler()->flush();
210
            $this->flushLogger();
211
            exit(1);
212
        }
213
    }
214
215
    /**
216
     * Renders the exception.
217
     * @param \Exception $exception the exception to be rendered.
218
     */
219
    abstract protected function renderException($exception);
220
221
    /**
222
     * Logs the given exception
223
     * @param \Exception $exception the exception to be logged
224
     * @since 2.0.3 this method is now public.
225
     */
226
    public function logException($exception)
227
    {
228
        $category = get_class($exception);
229
        if ($exception instanceof HttpException) {
230
            $category = HttpException::class . ': ' . $exception->statusCode;
231
        } elseif ($exception instanceof \ErrorException) {
0 ignored issues
show
Bug introduced by
The class ErrorException does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
232
            $category .= ':' . $exception->getSeverity();
233
        }
234
        Yii::error($exception, $category);
235
    }
236
237
    /**
238
     * Removes all output echoed before calling this method.
239
     */
240
    public function clearOutput()
241
    {
242
        // the following manual level counting is to deal with zlib.output_compression set to On
243
        for ($level = ob_get_level(); $level > 0; --$level) {
244
            if (!@ob_end_clean()) {
245
                ob_clean();
246
            }
247
        }
248
    }
249
250
    /**
251
     * Converts an exception into a PHP error.
252
     *
253
     * This method can be used to convert exceptions inside of methods like `__toString()`
254
     * to PHP errors because exceptions cannot be thrown inside of them.
255
     * @param \Exception $exception the exception to convert to a PHP error.
256
     */
257
    public static function convertExceptionToError($exception)
258
    {
259
        trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
260
    }
261
262
    /**
263
     * Converts an exception into a simple string.
264
     * @param \Exception|\Error $exception the exception being converted
265
     * @return string the string representation of the exception.
266
     */
267
    public static function convertExceptionToString($exception)
268
    {
269
        if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
270
            $message = "{$exception->getName()}: {$exception->getMessage()}";
271
        } elseif (YII_DEBUG) {
272
            if ($exception instanceof Exception) {
273
                $message = "Exception ({$exception->getName()})";
274
            } elseif ($exception instanceof ErrorException) {
275
                $message = "{$exception->getName()}";
0 ignored issues
show
Bug introduced by
Consider using $exception->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
276
            } else {
277
                $message = 'Exception';
278
            }
279
            $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
280
                . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
281
                . "Stack trace:\n" . $exception->getTraceAsString();
282
        } else {
283
            $message = 'Error: ' . $exception->getMessage();
284
        }
285
        return $message;
286
    }
287
288
    /**
289
     * Attempts to flush logger messages.
290
     * @since 2.1
291
     */
292
    protected function flushLogger()
293
    {
294
        $logger = Yii::getLogger();
295
        if ($logger instanceof \yii\log\Logger) {
296
            $logger->flush(true);
297
        }
298
        // attempt to invoke logger destructor:
299
        unset($logger);
300
        Yii::setLogger(null);
301
    }
302
}
303