Completed
Push — 2.1 ( 37580d...257604 )
by
unknown
12:15
created

ErrorHandler   D

Complexity

Total Complexity 90

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 12.37%

Importance

Changes 0
Metric Value
wmc 90
lcom 1
cbo 6
dl 0
loc 461
ccs 23
cts 186
cp 0.1237
rs 4.8717
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
C renderException() 0 48 12
D convertExceptionToArray() 0 31 11
C addTypeLinks() 0 35 8
A getTypeUrl() 0 14 3
D renderCallStackItem() 0 28 9
B renderCallStack() 0 18 8
A renderRequest() 0 11 3
A isCoreFile() 0 4 2
A createHttpStatusLink() 0 4 1
B createServerInformationLink() 0 22 5
A createFrameworkVersionLink() 0 4 1
C argumentsToString() 0 47 14
A renderFile() 0 14 3
A renderPreviousExceptions() 0 8 2
A htmlEncode() 0 4 1
B getExceptionName() 0 9 5
A shouldRenderSimpleHtml() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like ErrorHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ErrorHandler, and based on these observations, apply Extract Interface, too.

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\web;
9
10
use Yii;
11
use yii\base\ErrorException;
12
use yii\base\Exception;
13
use yii\base\UserException;
14
use yii\helpers\VarDumper;
15
16
/**
17
 * ErrorHandler handles uncaught PHP errors and exceptions.
18
 *
19
 * ErrorHandler displays these errors using appropriate views based on the
20
 * nature of the errors and the mode the application runs at.
21
 *
22
 * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
23
 * You can access that instance via `Yii::$app->errorHandler`.
24
 *
25
 * For more details and usage information on ErrorHandler, see the [guide article on handling errors](guide:runtime-handling-errors).
26
 *
27
 * @author Qiang Xue <[email protected]>
28
 * @author Timur Ruziev <[email protected]>
29
 * @since 2.0
30
 */
31
class ErrorHandler extends \yii\base\ErrorHandler
32
{
33
    /**
34
     * @var int maximum number of source code lines to be displayed. Defaults to 19.
35
     */
36
    public $maxSourceLines = 19;
37
    /**
38
     * @var int maximum number of trace source code lines to be displayed. Defaults to 13.
39
     */
40
    public $maxTraceSourceLines = 13;
41
    /**
42
     * @var string the route (e.g. `site/error`) to the controller action that will be used
43
     * to display external errors. Inside the action, it can retrieve the error information
44
     * using `Yii::$app->errorHandler->exception`. This property defaults to null, meaning ErrorHandler
45
     * will handle the error display.
46
     */
47
    public $errorAction;
48
    /**
49
     * @var string the path of the view file for rendering exceptions without call stack information.
50
     */
51
    public $errorView = '@yii/views/errorHandler/error.php';
52
    /**
53
     * @var string the path of the view file for rendering exceptions.
54
     */
55
    public $exceptionView = '@yii/views/errorHandler/exception.php';
56
    /**
57
     * @var string the path of the view file for rendering exceptions and errors call stack element.
58
     */
59
    public $callStackItemView = '@yii/views/errorHandler/callStackItem.php';
60
    /**
61
     * @var string the path of the view file for rendering previous exceptions.
62
     */
63
    public $previousExceptionView = '@yii/views/errorHandler/previousException.php';
64
    /**
65
     * @var array list of the PHP predefined variables that should be displayed on the error page.
66
     * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be displayed.
67
     * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION']`.
68
     * @see renderRequest()
69
     * @since 2.0.7
70
     */
71
    public $displayVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION'];
72
73
74
    /**
75
     * Renders the exception.
76
     * @param \Exception|\Error $exception the exception to be rendered.
77
     */
78 1
    protected function renderException($exception)
79
    {
80 1
        if (Yii::$app->has('response')) {
81 1
            $response = Yii::$app->getResponse();
82
            // reset parameters of response to avoid interference with partially created response data
83
            // in case the error occurred while sending the response.
84 1
            $response->isSent = false;
85 1
            $response->bodyRange = null;
86 1
            $response->data = null;
87 1
            $response->setBody(null);
88
        } else {
89
            $response = new Response();
90
        }
91
92 1
        $response->setStatusCodeByException($exception);
93
94 1
        $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
95
96 1
        if ($useErrorView && $this->errorAction !== null) {
97
            $result = Yii::$app->runAction($this->errorAction);
98
            if ($result instanceof Response) {
99
                $response = $result;
100
            } else {
101
                $response->data = $result;
102
            }
103 1
        } elseif ($response->format === Response::FORMAT_HTML) {
104 1
            if ($this->shouldRenderSimpleHtml()) {
105
                // AJAX request
106
                $response->data = '<pre>' . $this->htmlEncode(static::convertExceptionToString($exception)) . '</pre>';
107
            } else {
108
                // if there is an error during error rendering it's useful to
109
                // display PHP error in debug mode instead of a blank screen
110 1
                if (YII_DEBUG) {
111 1
                    ini_set('display_errors', 1);
112
                }
113 1
                $file = $useErrorView ? $this->errorView : $this->exceptionView;
114 1
                $response->data = $this->renderFile($file, [
115 1
                    'exception' => $exception,
116
                ]);
117
            }
118
        } elseif ($response->format === Response::FORMAT_RAW) {
119
            $response->data = static::convertExceptionToString($exception);
120
        } else {
121
            $response->data = $this->convertExceptionToArray($exception);
122
        }
123
124 1
        $response->send();
125 1
    }
126
127
    /**
128
     * Converts an exception into an array.
129
     * @param \Exception|\Error $exception the exception being converted
130
     * @return array the array representation of the exception.
131
     */
132
    protected function convertExceptionToArray($exception)
133
    {
134
        if (!YII_DEBUG && !$exception instanceof UserException && !$exception instanceof HttpException) {
135
            $exception = new HttpException(500, Yii::t('yii', 'An internal server error occurred.'));
136
        }
137
138
        $array = [
139
            'name' => ($exception instanceof Exception || $exception instanceof ErrorException) ? $exception->getName() : 'Exception',
140
            'message' => $exception->getMessage(),
0 ignored issues
show
Bug introduced by
The method getMessage does only exist in Error and Exception, but not in yii\base\ErrorException.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
141
            'code' => $exception->getCode(),
0 ignored issues
show
Bug introduced by
The method getCode does only exist in Error and Exception, but not in yii\base\ErrorException.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
142
        ];
143
        if ($exception instanceof HttpException) {
144
            $array['status'] = $exception->statusCode;
145
        }
146
        if (YII_DEBUG) {
147
            $array['type'] = get_class($exception);
148
            if (!$exception instanceof UserException) {
149
                $array['file'] = $exception->getFile();
150
                $array['line'] = $exception->getLine();
151
                $array['stack-trace'] = explode("\n", $exception->getTraceAsString());
152
                if ($exception instanceof \yii\db\Exception) {
153
                    $array['error-info'] = $exception->errorInfo;
154
                }
155
            }
156
        }
157
        if (($prev = $exception->getPrevious()) !== null) {
158
            $array['previous'] = $this->convertExceptionToArray($prev);
159
        }
160
161
        return $array;
162
    }
163
164
    /**
165
     * Converts special characters to HTML entities.
166
     * @param string $text to encode.
167
     * @return string encoded original text.
168
     */
169
    public function htmlEncode($text)
170
    {
171
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
172
    }
173
174
    /**
175
     * Adds informational links to the given PHP type/class.
176
     * @param string $code type/class name to be linkified.
177
     * @return string linkified with HTML type/class name.
178
     */
179
    public function addTypeLinks($code)
180
    {
181
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
182
            $class = $matches[1];
183
            $method = $matches[2];
184
            $text = $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
185
        } else {
186
            $class = $code;
187
            $method = null;
188
            $text = $this->htmlEncode($class);
189
        }
190
191
        $url = null;
192
193
        $shouldGenerateLink = true;
194
        if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
195
            $reflection = new \ReflectionClass($class);
196
            if ($reflection->hasMethod($method)) {
197
                $reflectionMethod = $reflection->getMethod($method);
198
                $shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
199
            } else {
200
                $shouldGenerateLink = false;
201
            }
202
        }
203
204
        if ($shouldGenerateLink) {
205
            $url = $this->getTypeUrl($class, $method);
206
        }
207
208
        if ($url === null) {
209
            return $text;
210
        }
211
212
        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
213
    }
214
215
    /**
216
     * Returns the informational link URL for a given PHP type/class.
217
     * @param string $class the type or class name.
218
     * @param string|null $method the method name.
219
     * @return string|null the informational link URL.
220
     * @see addTypeLinks()
221
     */
222
    protected function getTypeUrl($class, $method)
223
    {
224
        if (strpos($class, 'yii\\') !== 0) {
225
            return null;
226
        }
227
228
        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
229
        $url = "http://www.yiiframework.com/doc-2.0/$page.html";
230
        if ($method) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $method of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
231
            $url .= "#$method()-detail";
232
        }
233
234
        return $url;
235
    }
236
237
    /**
238
     * Renders a view file as a PHP script.
239
     * @param string $_file_ the view file.
240
     * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
241
     * @return string the rendering result
242
     */
243 1
    public function renderFile($_file_, $_params_)
244
    {
245 1
        $_params_['handler'] = $this;
246 1
        if ($this->exception instanceof ErrorException || !Yii::$app->has('view')) {
247
            ob_start();
248
            ob_implicit_flush(false);
249
            extract($_params_, EXTR_OVERWRITE);
250
            require Yii::getAlias($_file_);
251
252
            return ob_get_clean();
253
        }
254
255 1
        return Yii::$app->getView()->renderFile($_file_, $_params_, $this);
256
    }
257
258
    /**
259
     * Renders the previous exception stack for a given Exception.
260
     * @param \Exception $exception the exception whose precursors should be rendered.
261
     * @return string HTML content of the rendered previous exceptions.
262
     * Empty string if there are none.
263
     */
264
    public function renderPreviousExceptions($exception)
265
    {
266
        if (($previous = $exception->getPrevious()) !== null) {
267
            return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
268
        }
269
270
        return '';
271
    }
272
273
    /**
274
     * Renders a single call stack element.
275
     * @param string|null $file name where call has happened.
276
     * @param int|null $line number on which call has happened.
277
     * @param string|null $class called class name.
278
     * @param string|null $method called function/method name.
279
     * @param array $args array of method arguments.
280
     * @param int $index number of the call stack element.
281
     * @return string HTML content of the rendered call stack element.
282
     */
283
    public function renderCallStackItem($file, $line, $class, $method, $args, $index)
284
    {
285
        $lines = [];
286
        $begin = $end = 0;
287
        if ($file !== null && $line !== null) {
288
            $line--; // adjust line number from one-based to zero-based
289
            $lines = @file($file);
290
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
291
                return '';
292
            }
293
294
            $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
295
            $begin = $line - $half > 0 ? $line - $half : 0;
296
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
297
        }
298
299
        return $this->renderFile($this->callStackItemView, [
300
            'file' => $file,
301
            'line' => $line,
302
            'class' => $class,
303
            'method' => $method,
304
            'index' => $index,
305
            'lines' => $lines,
306
            'begin' => $begin,
307
            'end' => $end,
308
            'args' => $args,
309
        ]);
310
    }
311
312
    /**
313
     * Renders call stack.
314
     * @param \Exception|\ParseError $exception exception to get call stack from
315
     * @return string HTML content of the rendered call stack.
316
     * @since 2.0.12
317
     */
318
    public function renderCallStack($exception)
319
    {
320
        $out = '<ul>';
321
        $out .= $this->renderCallStackItem($exception->getFile(), $exception->getLine(), null, null, [], 1);
322
        for ($i = 0, $trace = $exception->getTrace(), $length = count($trace); $i < $length; ++$i) {
323
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
324
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
325
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
326
            $function = null;
327
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
328
                $function = $trace[$i]['function'];
329
            }
330
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
331
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
332
        }
333
        $out .= '</ul>';
334
        return $out;
335
    }
336
337
    /**
338
     * Renders the global variables of the request.
339
     * List of global variables is defined in [[displayVars]].
340
     * @return string the rendering result
341
     * @see displayVars
342
     */
343
    public function renderRequest()
344
    {
345
        $request = '';
346
        foreach ($this->displayVars as $name) {
347
            if (!empty($GLOBALS[$name])) {
348
                $request .= '$' . $name . ' = ' . VarDumper::export($GLOBALS[$name]) . ";\n\n";
349
            }
350
        }
351
352
        return '<pre>' . $this->htmlEncode(rtrim($request, "\n")) . '</pre>';
353
    }
354
355
    /**
356
     * Determines whether given name of the file belongs to the framework.
357
     * @param string $file name to be checked.
358
     * @return bool whether given name of the file belongs to the framework.
359
     */
360
    public function isCoreFile($file)
361
    {
362
        return $file === null || strpos(realpath($file), YII2_PATH . DIRECTORY_SEPARATOR) === 0;
363
    }
364
365
    /**
366
     * Creates HTML containing link to the page with the information on given HTTP status code.
367
     * @param int $statusCode to be used to generate information link.
368
     * @param string $statusDescription Description to display after the the status code.
369
     * @return string generated HTML with HTTP status code information.
370
     */
371
    public function createHttpStatusLink($statusCode, $statusDescription)
372
    {
373
        return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
374
    }
375
376
    /**
377
     * Creates string containing HTML link which refers to the home page of determined web-server software
378
     * and its full name.
379
     * @return string server software information hyperlink.
380
     */
381
    public function createServerInformationLink()
382
    {
383
        $serverUrls = [
384
            'http://httpd.apache.org/' => ['apache'],
385
            'http://nginx.org/' => ['nginx'],
386
            'http://lighttpd.net/' => ['lighttpd'],
387
            'http://gwan.com/' => ['g-wan', 'gwan'],
388
            'http://iis.net/' => ['iis', 'services'],
389
            'http://php.net/manual/en/features.commandline.webserver.php' => ['development'],
390
        ];
391
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
392
            foreach ($serverUrls as $url => $keywords) {
393
                foreach ($keywords as $keyword) {
394
                    if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
395
                        return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
396
                    }
397
                }
398
            }
399
        }
400
401
        return '';
402
    }
403
404
    /**
405
     * Creates string containing HTML link which refers to the page with the current version
406
     * of the framework and version number text.
407
     * @return string framework version information hyperlink.
408
     */
409
    public function createFrameworkVersionLink()
410
    {
411
        return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
412
    }
413
414
    /**
415
     * Converts arguments array to its string representation
416
     *
417
     * @param array $args arguments array to be converted
418
     * @return string string representation of the arguments array
419
     */
420
    public function argumentsToString($args)
421
    {
422
        $count = 0;
423
        $isAssoc = $args !== array_values($args);
424
425
        foreach ($args as $key => $value) {
426
            $count++;
427
            if ($count >= 5) {
428
                if ($count > 5) {
429
                    unset($args[$key]);
430
                } else {
431
                    $args[$key] = '...';
432
                }
433
                continue;
434
            }
435
436
            if (is_object($value)) {
437
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
438
            } elseif (is_bool($value)) {
439
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
440
            } elseif (is_string($value)) {
441
                $fullValue = $this->htmlEncode($value);
442
                if (mb_strlen($value, 'UTF-8') > 32) {
443
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
444
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
445
                } else {
446
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
447
                }
448
            } elseif (is_array($value)) {
449
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
450
            } elseif ($value === null) {
451
                $args[$key] = '<span class="keyword">null</span>';
452
            } elseif (is_resource($value)) {
453
                $args[$key] = '<span class="keyword">resource</span>';
454
            } else {
455
                $args[$key] = '<span class="number">' . $value . '</span>';
456
            }
457
458
            if (is_string($key)) {
459
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
460
            } elseif ($isAssoc) {
461
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
462
            }
463
        }
464
465
        return implode(', ', $args);
466
    }
467
468
    /**
469
     * Returns human-readable exception name
470
     * @param \Exception $exception
471
     * @return string human-readable exception name or null if it cannot be determined
472
     */
473
    public function getExceptionName($exception)
474
    {
475
        if ($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\InvalidCallException ||
476
            $exception instanceof \yii\base\InvalidArgumentException ||
477
            $exception instanceof \yii\base\UnknownMethodException) {
478
            return $exception->getName();
479
        }
480
        return null;
481
    }
482
483
    /**
484
     * @return bool if simple HTML should be rendered
485
     * @since 2.0.12
486
     */
487
    protected function shouldRenderSimpleHtml()
488
    {
489
        return YII_ENV_TEST || Yii::$app->getRequest()->getHeaderLine('x-requested-with') === 'XMLHttpRequest';
490
    }
491
}
492