Completed
Push — 2.1-master-merge ( 240673 )
by Alexander
13:45
created

ErrorHandler   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 278
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 8

Test Coverage

Coverage 9.09%

Importance

Changes 0
Metric Value
wmc 37
lcom 2
cbo 8
dl 0
loc 278
rs 8.6
c 0
b 0
f 0
ccs 10
cts 110
cp 0.0909

12 Methods

Rating   Name   Duplication   Size   Complexity  
A register() 0 11 2
A unregister() 0 5 1
B handleException() 0 35 6
A handleFallbackExceptionMessage() 0 19 3
B handleError() 0 25 5
B handleFatalError() 0 29 4
renderException() 0 1 ?
A logException() 0 10 3
A clearOutput() 0 9 3
A convertExceptionToError() 0 4 1
B convertExceptionToString() 0 21 7
A flushLogger() 0 10 2
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
180
        return false;
181
    }
182
183
    /**
184
     * Handles fatal PHP errors.
185
     */
186
    public function handleFatalError()
187
    {
188
        unset($this->_memoryReserve);
189
190
        // load ErrorException manually here because autoloading them will not work
191
        // when error occurs while autoloading a class
192
        if (!class_exists(ErrorException::class, false)) {
193
            require_once __DIR__ . '/ErrorException.php';
194
        }
195
196
        $error = error_get_last();
197
198
        if (ErrorException::isFatalError($error)) {
199
            $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
200
            $this->exception = $exception;
201
202
            $this->logException($exception);
203
204
            if ($this->discardExistingOutput) {
205
                $this->clearOutput();
206
            }
207
            $this->renderException($exception);
208
209
            // need to explicitly flush logs because exit() next will terminate the app immediately
210
            Yii::getProfiler()->flush();
211
            $this->flushLogger();
212
            exit(1);
213
        }
214
    }
215
216
    /**
217
     * Renders the exception.
218
     * @param \Exception $exception the exception to be rendered.
219
     */
220
    abstract protected function renderException($exception);
221
222
    /**
223
     * Logs the given exception.
224
     * @param \Exception $exception the exception to be logged
225
     * @since 2.0.3 this method is now public.
226
     */
227
    public function logException($exception)
228
    {
229
        $category = get_class($exception);
230
        if ($exception instanceof HttpException) {
231
            $category = HttpException::class . ': ' . $exception->statusCode;
232
        } 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...
233
            $category .= ':' . $exception->getSeverity();
234
        }
235
        Yii::error($exception, $category);
236
    }
237
238
    /**
239
     * Removes all output echoed before calling this method.
240
     */
241
    public function clearOutput()
242
    {
243
        // the following manual level counting is to deal with zlib.output_compression set to On
244
        for ($level = ob_get_level(); $level > 0; --$level) {
245
            if (!@ob_end_clean()) {
246
                ob_clean();
247
            }
248
        }
249
    }
250
251
    /**
252
     * Converts an exception into a PHP error.
253
     *
254
     * This method can be used to convert exceptions inside of methods like `__toString()`
255
     * to PHP errors because exceptions cannot be thrown inside of them.
256
     * @param \Exception $exception the exception to convert to a PHP error.
257
     */
258
    public static function convertExceptionToError($exception)
259
    {
260
        trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
261
    }
262
263
    /**
264
     * Converts an exception into a simple string.
265
     * @param \Exception|\Error $exception the exception being converted
266
     * @return string the string representation of the exception.
267
     */
268 5
    public static function convertExceptionToString($exception)
269
    {
270 5
        if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
271
            $message = "{$exception->getName()}: {$exception->getMessage()}";
272 5
        } elseif (YII_DEBUG) {
273 5
            if ($exception instanceof Exception) {
274
                $message = "Exception ({$exception->getName()})";
275 5
            } elseif ($exception instanceof ErrorException) {
276
                $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...
277
            } else {
278 5
                $message = 'Exception';
279
            }
280 5
            $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
281 5
                . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
282 5
                . "Stack trace:\n" . $exception->getTraceAsString();
283
        } else {
284
            $message = 'Error: ' . $exception->getMessage();
285
        }
286
287 5
        return $message;
288
    }
289
290
    /**
291
     * Attempts to flush logger messages.
292
     * @since 2.1
293
     */
294
    protected function flushLogger()
295
    {
296
        $logger = Yii::getLogger();
297
        if ($logger instanceof \yii\log\Logger) {
298
            $logger->flush(true);
299
        }
300
        // attempt to invoke logger destructor:
301
        unset($logger);
302
        Yii::setLogger(null);
303
    }
304
}
305