Completed
Push — 3.0 ( e28bf1...6f19d3 )
by Alexander
113:15 queued 105:35
created

ErrorHandler::renderException()   C

Complexity

Conditions 13
Paths 102

Size

Total Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 18.0493

Importance

Changes 0
Metric Value
dl 0
loc 48
ccs 20
cts 29
cp 0.6897
rs 6.6
c 0
b 0
f 0
cc 13
nc 102
nop 1
crap 18.0493

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * @var string trace line with placeholders to be be substituted.
74
     * The placeholders are {file}, {line} and {text} and the string should be as follows.
75
     *
76
     * `File: {file} - Line: {line} - Text: {text}`
77
     *
78
     * @example <a href="ide://open?file={file}&line={line}">{html}</a>
79
     * @see https://github.com/yiisoft/yii2-debug#open-files-in-ide
80
     * @since 2.0.14
81
     */
82
    public $traceLine = '{html}';
83
84
85
    /**
86
     * Renders the exception.
87
     * @param \Exception|\Error $exception the exception to be rendered.
88
     */
89 1
    protected function renderException($exception)
90
    {
91 1
        if (Yii::$app->has('response')) {
92 1
            $response = Yii::$app->getResponse();
93
            // reset parameters of response to avoid interference with partially created response data
94
            // in case the error occurred while sending the response.
95 1
            $response->isSent = false;
96 1
            $response->bodyRange = null;
97 1
            $response->data = null;
98 1
            $response->setBody(null);
99
        } else {
100
            $response = new Response();
101
        }
102
103 1
        $useCustomErrorAction = $this->errorAction !== null && (!YII_DEBUG || $exception instanceof UserException);
104 1
        $response->setStatusCodeByException($exception);
105
106 1
        if ($useCustomErrorAction) {
107
            $result = Yii::$app->runAction($this->errorAction);
108
            if ($result instanceof Response) {
109
                $response = $result;
110
            } else {
111
                $response->data = $result;
112
            }
113 1
        } elseif ($response->format === Response::FORMAT_HTML) {
114 1
            if ($this->shouldRenderSimpleHtml()) {
115
                // AJAX request
116
                $response->data = '<pre>' . $this->htmlEncode(static::convertExceptionToString($exception)) . '</pre>';
117
            } else {
118
                // if there is an error during error rendering it's useful to
119
                // display PHP error in debug mode instead of a blank screen
120 1
                if (YII_DEBUG) {
121 1
                    ini_set('display_errors', 1);
122
                }
123 1
                $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
124 1
                $file = $useErrorView ? $this->errorView : $this->exceptionView;
125 1
                $response->data = $this->renderFile($file, [
126 1
                    'exception' => $exception,
127
                ]);
128
            }
129
        } elseif ($response->format === Response::FORMAT_RAW) {
130
            $response->data = static::convertExceptionToString($exception);
131
        } else {
132
            $response->data = $this->convertExceptionToArray($exception);
133
        }
134
135 1
        $response->send();
136 1
    }
137
138
    /**
139
     * Converts an exception into an array.
140
     * @param \Exception|\Error $exception the exception being converted
141
     * @return array the array representation of the exception.
142
     */
143
    protected function convertExceptionToArray($exception)
144
    {
145
        if (!YII_DEBUG && !$exception instanceof UserException && !$exception instanceof HttpException) {
146
            $exception = new HttpException(500, Yii::t('yii', 'An internal server error occurred.'));
147
        }
148
149
        $array = [
150
            'name' => ($exception instanceof Exception || $exception instanceof ErrorException) ? $exception->getName() : 'Exception',
151
            '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...
152
            '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...
153
        ];
154
        if ($exception instanceof HttpException) {
155
            $array['status'] = $exception->statusCode;
156
        }
157
        if (YII_DEBUG) {
158
            $array['type'] = get_class($exception);
159
            if (!$exception instanceof UserException) {
160
                $array['file'] = $exception->getFile();
161
                $array['line'] = $exception->getLine();
162
                $array['stack-trace'] = explode("\n", $exception->getTraceAsString());
163
                if ($exception instanceof \yii\db\Exception) {
164
                    $array['error-info'] = $exception->errorInfo;
165
                }
166
            }
167
        }
168
        if (($prev = $exception->getPrevious()) !== null) {
169
            $array['previous'] = $this->convertExceptionToArray($prev);
170
        }
171
172
        return $array;
173
    }
174
175
    /**
176
     * Converts special characters to HTML entities.
177
     * @param string $text to encode.
178
     * @return string encoded original text.
179
     */
180 4
    public function htmlEncode($text)
181
    {
182 4
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
183
    }
184
185
    /**
186
     * Adds informational links to the given PHP type/class.
187
     * @param string $code type/class name to be linkified.
188
     * @return string linkified with HTML type/class name.
189
     */
190
    public function addTypeLinks($code)
191
    {
192
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
193
            $class = $matches[1];
194
            $method = $matches[2];
195
            $text = $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
196
        } else {
197
            $class = $code;
198
            $method = null;
199
            $text = $this->htmlEncode($class);
200
        }
201
202
        $url = null;
203
204
        $shouldGenerateLink = true;
205
        if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
206
            $reflection = new \ReflectionClass($class);
207
            if ($reflection->hasMethod($method)) {
208
                $reflectionMethod = $reflection->getMethod($method);
209
                $shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
210
            } else {
211
                $shouldGenerateLink = false;
212
            }
213
        }
214
215
        if ($shouldGenerateLink) {
216
            $url = $this->getTypeUrl($class, $method);
217
        }
218
219
        if ($url === null) {
220
            return $text;
221
        }
222
223
        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
224
    }
225
226
    /**
227
     * Returns the informational link URL for a given PHP type/class.
228
     * @param string $class the type or class name.
229
     * @param string|null $method the method name.
230
     * @return string|null the informational link URL.
231
     * @see addTypeLinks()
232
     */
233
    protected function getTypeUrl($class, $method)
234
    {
235
        if (strncmp($class, 'yii\\', 4) !== 0) {
236
            return null;
237
        }
238
239
        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
240
        $url = "http://www.yiiframework.com/doc-2.0/$page.html";
241
        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...
242
            $url .= "#$method()-detail";
243
        }
244
245
        return $url;
246
    }
247
248
    /**
249
     * Renders a view file as a PHP script.
250
     * @param string $_file_ the view file.
251
     * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
252
     * @return string the rendering result
253
     */
254 5
    public function renderFile($_file_, $_params_)
255
    {
256 5
        $_params_['handler'] = $this;
257 5
        if ($this->exception instanceof ErrorException || !Yii::$app->has('view')) {
258
            ob_start();
259
            ob_implicit_flush(false);
260
            extract($_params_, EXTR_OVERWRITE);
261
            require Yii::getAlias($_file_);
262
263
            return ob_get_clean();
264
        }
265
266 5
        return Yii::$app->getView()->renderFile($_file_, $_params_, $this);
267
    }
268
269
    /**
270
     * Renders the previous exception stack for a given Exception.
271
     * @param \Exception $exception the exception whose precursors should be rendered.
272
     * @return string HTML content of the rendered previous exceptions.
273
     * Empty string if there are none.
274
     */
275
    public function renderPreviousExceptions($exception)
276
    {
277
        if (($previous = $exception->getPrevious()) !== null) {
278
            return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
279
        }
280
281
        return '';
282
    }
283
284
    /**
285
     * Renders a single call stack element.
286
     * @param string|null $file name where call has happened.
287
     * @param int|null $line number on which call has happened.
288
     * @param string|null $class called class name.
289
     * @param string|null $method called function/method name.
290
     * @param array $args array of method arguments.
291
     * @param int $index number of the call stack element.
292
     * @return string HTML content of the rendered call stack element.
293
     */
294 1
    public function renderCallStackItem($file, $line, $class, $method, $args, $index)
295
    {
296 1
        $lines = [];
297 1
        $begin = $end = 0;
298 1
        if ($file !== null && $line !== null) {
299 1
            $line--; // adjust line number from one-based to zero-based
300 1
            $lines = @file($file);
301 1
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
302
                return '';
303
            }
304
305 1
            $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
306 1
            $begin = $line - $half > 0 ? $line - $half : 0;
307 1
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
308
        }
309
310 1
        return $this->renderFile($this->callStackItemView, [
311 1
            'file' => $file,
312 1
            'line' => $line,
313 1
            'class' => $class,
314 1
            'method' => $method,
315 1
            'index' => $index,
316 1
            'lines' => $lines,
317 1
            'begin' => $begin,
318 1
            'end' => $end,
319 1
            'args' => $args,
320
        ]);
321
    }
322
323
    /**
324
     * Renders call stack.
325
     * @param \Exception|\ParseError $exception exception to get call stack from
326
     * @return string HTML content of the rendered call stack.
327
     * @since 2.0.12
328
     */
329
    public function renderCallStack($exception)
330
    {
331
        $out = '<ul>';
332
        $out .= $this->renderCallStackItem($exception->getFile(), $exception->getLine(), null, null, [], 1);
333
        for ($i = 0, $trace = $exception->getTrace(), $length = count($trace); $i < $length; ++$i) {
334
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
335
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
336
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
337
            $function = null;
338
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
339
                $function = $trace[$i]['function'];
340
            }
341
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
342
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
343
        }
344
        $out .= '</ul>';
345
        return $out;
346
    }
347
348
    /**
349
     * Renders the global variables of the request.
350
     * List of global variables is defined in [[displayVars]].
351
     * @return string the rendering result
352
     * @see displayVars
353
     */
354
    public function renderRequest()
355
    {
356
        $request = '';
357
        foreach ($this->displayVars as $name) {
358
            if (!empty($GLOBALS[$name])) {
359
                $request .= '$' . $name . ' = ' . VarDumper::export($GLOBALS[$name]) . ";\n\n";
360
            }
361
        }
362
363
        return '<pre>' . $this->htmlEncode(rtrim($request, "\n")) . '</pre>';
364
    }
365
366
    /**
367
     * Determines whether given name of the file belongs to the framework.
368
     * @param string $file name to be checked.
369
     * @return bool whether given name of the file belongs to the framework.
370
     */
371 1
    public function isCoreFile($file)
372
    {
373 1
        return $file === null || strpos(realpath($file), YII2_PATH . DIRECTORY_SEPARATOR) === 0;
374
    }
375
376
    /**
377
     * Creates HTML containing link to the page with the information on given HTTP status code.
378
     * @param int $statusCode to be used to generate information link.
379
     * @param string $statusDescription Description to display after the the status code.
380
     * @return string generated HTML with HTTP status code information.
381
     */
382
    public function createHttpStatusLink($statusCode, $statusDescription)
383
    {
384
        return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
385
    }
386
387
    /**
388
     * Creates string containing HTML link which refers to the home page of determined web-server software
389
     * and its full name.
390
     * @return string server software information hyperlink.
391
     */
392
    public function createServerInformationLink()
393
    {
394
        $serverUrls = [
395
            'http://httpd.apache.org/' => ['apache'],
396
            'http://nginx.org/' => ['nginx'],
397
            'http://lighttpd.net/' => ['lighttpd'],
398
            'http://gwan.com/' => ['g-wan', 'gwan'],
399
            'http://iis.net/' => ['iis', 'services'],
400
            'http://php.net/manual/en/features.commandline.webserver.php' => ['development'],
401
        ];
402
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
403
            foreach ($serverUrls as $url => $keywords) {
404
                foreach ($keywords as $keyword) {
405
                    if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
406
                        return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
407
                    }
408
                }
409
            }
410
        }
411
412
        return '';
413
    }
414
415
    /**
416
     * Creates string containing HTML link which refers to the page with the current version
417
     * of the framework and version number text.
418
     * @return string framework version information hyperlink.
419
     */
420
    public function createFrameworkVersionLink()
421
    {
422
        return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
423
    }
424
425
    /**
426
     * Converts arguments array to its string representation.
427
     *
428
     * @param array $args arguments array to be converted
429
     * @return string string representation of the arguments array
430
     */
431
    public function argumentsToString($args)
432
    {
433
        $count = 0;
434
        $isAssoc = $args !== array_values($args);
435
436
        foreach ($args as $key => $value) {
437
            $count++;
438
            if ($count >= 5) {
439
                if ($count > 5) {
440
                    unset($args[$key]);
441
                } else {
442
                    $args[$key] = '...';
443
                }
444
                continue;
445
            }
446
447
            if (is_object($value)) {
448
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
449
            } elseif (is_bool($value)) {
450
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
451
            } elseif (is_string($value)) {
452
                $fullValue = $this->htmlEncode($value);
453
                if (mb_strlen($value, 'UTF-8') > 32) {
454
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
455
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
456
                } else {
457
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
458
                }
459
            } elseif (is_array($value)) {
460
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
461
            } elseif ($value === null) {
462
                $args[$key] = '<span class="keyword">null</span>';
463
            } elseif (is_resource($value)) {
464
                $args[$key] = '<span class="keyword">resource</span>';
465
            } else {
466
                $args[$key] = '<span class="number">' . $value . '</span>';
467
            }
468
469
            if (is_string($key)) {
470
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
471
            } elseif ($isAssoc) {
472
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
473
            }
474
        }
475
476
        return implode(', ', $args);
477
    }
478
479
    /**
480
     * Returns human-readable exception name.
481
     * @param \Exception $exception
482
     * @return string human-readable exception name or null if it cannot be determined
483
     */
484 3
    public function getExceptionName($exception)
485
    {
486 3
        if ($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\InvalidCallException ||
487
            $exception instanceof \yii\base\InvalidArgumentException ||
488 3
            $exception instanceof \yii\base\UnknownMethodException) {
489 3
            return $exception->getName();
490
        }
491
492
        return null;
493
    }
494
495
    /**
496
     * @return bool if simple HTML should be rendered
497
     * @since 2.0.12
498
     */
499
    protected function shouldRenderSimpleHtml()
500
    {
501
        return YII_ENV_TEST || Yii::$app->getRequest()->getHeaderLine('x-requested-with') === 'XMLHttpRequest';
502
    }
503
}
504