Completed
Push — master ( d1be6d...8440b6 )
by Andrey
07:58
created

ExceptionHandler   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 467
Duplicated Lines 2.14 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
dl 10
loc 467
rs 2.96
c 0
b 0
f 0
wmc 68
lcom 1
cbo 3

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 6
A register() 0 12 3
A setHandler() 10 10 3
A setFileLinkFormat() 0 7 1
B handle() 0 32 8
A failSafeHandle() 0 17 5
A sendPhpResponse() 0 16 4
A createResponse() 0 10 2
A getHtml() 0 8 2
B getContent() 0 60 10
B getStylesheet() 0 58 1
A decorate() 0 24 1
A formatClass() 0 6 1
A formatPath() 0 13 3
B formatArgs() 0 25 10
A utf8Htmlize() 0 6 2
A escapeHtml() 0 4 2
A catchOutput() 0 6 1
A cleanOutput() 0 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ExceptionHandler 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 ExceptionHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Debug;
13
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\Debug\Exception\FlattenException;
16
use Symfony\Component\Debug\Exception\OutOfMemoryException;
17
18
/**
19
 * ExceptionHandler converts an exception to a Response object.
20
 *
21
 * It is mostly useful in debug mode to replace the default PHP/XDebug
22
 * output with something prettier and more useful.
23
 *
24
 * As this class is mainly used during Kernel boot, where nothing is yet
25
 * available, the Response content is always HTML.
26
 *
27
 * @author Fabien Potencier <[email protected]>
28
 * @author Nicolas Grekas <[email protected]>
29
 */
30
class ExceptionHandler
31
{
32
    private $debug;
33
    private $charset;
34
    private $handler;
35
    private $caughtBuffer;
36
    private $caughtLength;
37
    private $fileLinkFormat;
38
39
    public function __construct($debug = true, $charset = null, $fileLinkFormat = null)
40
    {
41
        if (false !== strpos($charset, '%')) {
42
            @trigger_error('Providing $fileLinkFormat as second argument to '.__METHOD__.' is deprecated since version 2.8 and will be unsupported in 3.0. Please provide it as third argument, after $charset.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
43
44
            // Swap $charset and $fileLinkFormat for BC reasons
45
            $pivot = $fileLinkFormat;
46
            $fileLinkFormat = $charset;
47
            $charset = $pivot;
48
        }
49
        $this->debug = $debug;
50
        $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8';
51
        $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
52
    }
53
54
    /**
55
     * Registers the exception handler.
56
     *
57
     * @param bool        $debug          Enable/disable debug mode, where the stack trace is displayed
58
     * @param string|null $charset        The charset used by exception messages
59
     * @param string|null $fileLinkFormat The IDE link template
60
     *
61
     * @return static
62
     */
63
    public static function register($debug = true, $charset = null, $fileLinkFormat = null)
64
    {
65
        $handler = new static($debug, $charset, $fileLinkFormat);
66
67
        $prev = set_exception_handler(array($handler, 'handle'));
68
        if (is_array($prev) && $prev[0] instanceof ErrorHandler) {
69
            restore_exception_handler();
70
            $prev[0]->setExceptionHandler(array($handler, 'handle'));
71
        }
72
73
        return $handler;
74
    }
75
76
    /**
77
     * Sets a user exception handler.
78
     *
79
     * @param callable $handler An handler that will be called on Exception
80
     *
81
     * @return callable|null The previous exception handler if any
82
     */
83 View Code Duplication
    public function setHandler($handler)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
84
    {
85
        if (null !== $handler && !is_callable($handler)) {
86
            throw new \LogicException('The exception handler must be a valid PHP callable.');
87
        }
88
        $old = $this->handler;
89
        $this->handler = $handler;
90
91
        return $old;
92
    }
93
94
    /**
95
     * Sets the format for links to source files.
96
     *
97
     * @param string $format The format for links to source files
98
     *
99
     * @return string The previous file link format
100
     */
101
    public function setFileLinkFormat($format)
102
    {
103
        $old = $this->fileLinkFormat;
104
        $this->fileLinkFormat = $format;
105
106
        return $old;
107
    }
108
109
    /**
110
     * Sends a response for the given Exception.
111
     *
112
     * To be as fail-safe as possible, the exception is first handled
113
     * by our simple exception handler, then by the user exception handler.
114
     * The latter takes precedence and any output from the former is cancelled,
115
     * if and only if nothing bad happens in this handling path.
116
     */
117
    public function handle(\Exception $exception)
118
    {
119
        if (null === $this->handler || $exception instanceof OutOfMemoryException) {
120
            $this->failSafeHandle($exception);
121
122
            return;
123
        }
124
125
        $caughtLength = $this->caughtLength = 0;
126
127
        ob_start(array($this, 'catchOutput'));
128
        $this->failSafeHandle($exception);
129
        while (null === $this->caughtBuffer && ob_end_flush()) {
130
            // Empty loop, everything is in the condition
131
        }
132
        if (isset($this->caughtBuffer[0])) {
133
            ob_start(array($this, 'cleanOutput'));
134
            echo $this->caughtBuffer;
135
            $caughtLength = ob_get_length();
136
        }
137
        $this->caughtBuffer = null;
138
139
        try {
140
            call_user_func($this->handler, $exception);
141
            $this->caughtLength = $caughtLength;
142
        } catch (\Exception $e) {
143
            if (!$caughtLength) {
144
                // All handlers failed. Let PHP handle that now.
145
                throw $exception;
146
            }
147
        }
148
    }
149
150
    /**
151
     * Sends a response for the given Exception.
152
     *
153
     * If you have the Symfony HttpFoundation component installed,
154
     * this method will use it to create and send the response. If not,
155
     * it will fallback to plain PHP functions.
156
     *
157
     * @param \Exception $exception An \Exception instance
158
     */
159
    private function failSafeHandle(\Exception $exception)
160
    {
161
        if (class_exists('Symfony\Component\HttpFoundation\Response', false)
162
            && __CLASS__ !== get_class($this)
163
            && ($reflector = new \ReflectionMethod($this, 'createResponse'))
164
            && __CLASS__ !== $reflector->class
165
        ) {
166
            $response = $this->createResponse($exception);
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\Debug\...ndler::createResponse() has been deprecated with message: since 2.8, to be removed in 3.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
167
            $response->sendHeaders();
168
            $response->sendContent();
169
            @trigger_error(sprintf("The %s::createResponse method is deprecated since 2.8 and won't be called anymore when handling an exception in 3.0.", $reflector->class), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
170
171
            return;
172
        }
173
174
        $this->sendPhpResponse($exception);
175
    }
176
177
    /**
178
     * Sends the error associated with the given Exception as a plain PHP response.
179
     *
180
     * This method uses plain PHP functions like header() and echo to output
181
     * the response.
182
     *
183
     * @param \Exception|FlattenException $exception An \Exception or FlattenException instance
184
     */
185
    public function sendPhpResponse($exception)
186
    {
187
        if (!$exception instanceof FlattenException) {
188
            $exception = FlattenException::create($exception);
189
        }
190
191
        if (!headers_sent()) {
192
            header(sprintf('HTTP/1.0 %s', $exception->getStatusCode()));
193
            foreach ($exception->getHeaders() as $name => $value) {
194
                header($name.': '.$value, false);
195
            }
196
            header('Content-Type: text/html; charset='.$this->charset);
197
        }
198
199
        echo $this->decorate($this->getContent($exception), $this->getStylesheet($exception));
200
    }
201
202
    /**
203
     * Creates the error Response associated with the given Exception.
204
     *
205
     * @param \Exception|FlattenException $exception An \Exception or FlattenException instance
206
     *
207
     * @return Response A Response instance
208
     *
209
     * @deprecated since 2.8, to be removed in 3.0.
210
     */
211
    public function createResponse($exception)
212
    {
213
        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
214
215
        if (!$exception instanceof FlattenException) {
216
            $exception = FlattenException::create($exception);
217
        }
218
219
        return Response::create($this->getHtml($exception), $exception->getStatusCode(), $exception->getHeaders())->setCharset($this->charset);
220
    }
221
222
    /**
223
     * Gets the full HTML content associated with the given exception.
224
     *
225
     * @param \Exception|FlattenException $exception An \Exception or FlattenException instance
226
     *
227
     * @return string The HTML content as a string
228
     */
229
    public function getHtml($exception)
230
    {
231
        if (!$exception instanceof FlattenException) {
232
            $exception = FlattenException::create($exception);
233
        }
234
235
        return $this->decorate($this->getContent($exception), $this->getStylesheet($exception));
236
    }
237
238
    /**
239
     * Gets the HTML content associated with the given exception.
240
     *
241
     * @param FlattenException $exception A FlattenException instance
242
     *
243
     * @return string The content as a string
244
     */
245
    public function getContent(FlattenException $exception)
246
    {
247
        switch ($exception->getStatusCode()) {
248
            case 404:
249
                $title = 'Sorry, the page you are looking for could not be found.';
250
                break;
251
            default:
252
                $title = 'Whoops, looks like something went wrong.';
253
        }
254
255
        $content = '';
256
        if ($this->debug) {
257
            try {
258
                $count = count($exception->getAllPrevious());
259
                $total = $count + 1;
260
                foreach ($exception->toArray() as $position => $e) {
261
                    $ind = $count - $position + 1;
262
                    $class = $this->formatClass($e['class']);
263
                    $message = nl2br($this->escapeHtml($e['message']));
264
                    $content .= sprintf(<<<'EOF'
265
                        <h2 class="block_exception clear_fix">
266
                            <span class="exception_counter">%d/%d</span>
267
                            <span class="exception_title">%s%s:</span>
268
                            <span class="exception_message">%s</span>
269
                        </h2>
270
                        <div class="block">
271
                            <ol class="traces list_exception">
272
273
EOF
274
                        , $ind, $total, $class, $this->formatPath($e['trace'][0]['file'], $e['trace'][0]['line']), $message);
275
                    foreach ($e['trace'] as $trace) {
276
                        $content .= '       <li>';
277
                        if ($trace['function']) {
278
                            $content .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args']));
279
                        }
280
                        if (isset($trace['file']) && isset($trace['line'])) {
281
                            $content .= $this->formatPath($trace['file'], $trace['line']);
282
                        }
283
                        $content .= "</li>\n";
284
                    }
285
286
                    $content .= "    </ol>\n</div>\n";
287
                }
288
            } catch (\Exception $e) {
289
                // something nasty happened and we cannot throw an exception anymore
290
                if ($this->debug) {
291
                    $title = sprintf('Exception thrown when handling an exception (%s: %s)', get_class($e), $this->escapeHtml($e->getMessage()));
292
                } else {
293
                    $title = 'Whoops, looks like something went wrong.';
294
                }
295
            }
296
        }
297
298
        return <<<EOF
299
            <div id="sf-resetcontent" class="sf-reset">
300
                <h1>$title</h1>
301
                $content
302
            </div>
303
EOF;
304
    }
305
306
    /**
307
     * Gets the stylesheet associated with the given exception.
308
     *
309
     * @param FlattenException $exception A FlattenException instance
310
     *
311
     * @return string The stylesheet as a string
312
     */
313
    public function getStylesheet(FlattenException $exception)
0 ignored issues
show
Unused Code introduced by
The parameter $exception is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
314
    {
315
        return <<<'EOF'
316
            .sf-reset { font: 11px Verdana, Arial, sans-serif; color: #333 }
317
            .sf-reset .clear { clear:both; height:0; font-size:0; line-height:0; }
318
            .sf-reset .clear_fix:after { display:block; height:0; clear:both; visibility:hidden; }
319
            .sf-reset .clear_fix { display:inline-block; }
320
            .sf-reset * html .clear_fix { height:1%; }
321
            .sf-reset .clear_fix { display:block; }
322
            .sf-reset, .sf-reset .block { margin: auto }
323
            .sf-reset abbr { border-bottom: 1px dotted #000; cursor: help; }
324
            .sf-reset p { font-size:14px; line-height:20px; color:#868686; padding-bottom:20px }
325
            .sf-reset strong { font-weight:bold; }
326
            .sf-reset a { color:#6c6159; cursor: default; }
327
            .sf-reset a img { border:none; }
328
            .sf-reset a:hover { text-decoration:underline; }
329
            .sf-reset em { font-style:italic; }
330
            .sf-reset h1, .sf-reset h2 { font: 20px Georgia, "Times New Roman", Times, serif }
331
            .sf-reset .exception_counter { background-color: #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; }
332
            .sf-reset .exception_title { margin-left: 3em; margin-bottom: 0.7em; display: block; }
333
            .sf-reset .exception_message { margin-left: 3em; display: block; }
334
            .sf-reset .traces li { font-size:12px; padding: 2px 4px; list-style-type:decimal; margin-left:20px; }
335
            .sf-reset .block { background-color:#FFFFFF; padding:10px 28px; margin-bottom:20px;
336
                -webkit-border-bottom-right-radius: 16px;
337
                -webkit-border-bottom-left-radius: 16px;
338
                -moz-border-radius-bottomright: 16px;
339
                -moz-border-radius-bottomleft: 16px;
340
                border-bottom-right-radius: 16px;
341
                border-bottom-left-radius: 16px;
342
                border-bottom:1px solid #ccc;
343
                border-right:1px solid #ccc;
344
                border-left:1px solid #ccc;
345
                word-wrap: break-word;
346
            }
347
            .sf-reset .block_exception { background-color:#ddd; color: #333; padding:20px;
348
                -webkit-border-top-left-radius: 16px;
349
                -webkit-border-top-right-radius: 16px;
350
                -moz-border-radius-topleft: 16px;
351
                -moz-border-radius-topright: 16px;
352
                border-top-left-radius: 16px;
353
                border-top-right-radius: 16px;
354
                border-top:1px solid #ccc;
355
                border-right:1px solid #ccc;
356
                border-left:1px solid #ccc;
357
                overflow: hidden;
358
                word-wrap: break-word;
359
            }
360
            .sf-reset a { background:none; color:#868686; text-decoration:none; }
361
            .sf-reset a:hover { background:none; color:#313131; text-decoration:underline; }
362
            .sf-reset ol { padding: 10px 0; }
363
            .sf-reset h1 { background-color:#FFFFFF; padding: 15px 28px; margin-bottom: 20px;
364
                -webkit-border-radius: 10px;
365
                -moz-border-radius: 10px;
366
                border-radius: 10px;
367
                border: 1px solid #ccc;
368
            }
369
EOF;
370
    }
371
372
    private function decorate($content, $css)
373
    {
374
        return <<<EOF
375
<!DOCTYPE html>
376
<html>
377
    <head>
378
        <meta charset="{$this->charset}" />
379
        <meta name="robots" content="noindex,nofollow" />
380
        <style>
381
            /* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html */
382
            html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}
383
384
            html { background: #eee; padding: 10px }
385
            img { border: 0; }
386
            #sf-resetcontent { width:970px; margin:0 auto; }
387
            $css
388
        </style>
389
    </head>
390
    <body>
391
        $content
392
    </body>
393
</html>
394
EOF;
395
    }
396
397
    private function formatClass($class)
398
    {
399
        $parts = explode('\\', $class);
400
401
        return sprintf('<abbr title="%s">%s</abbr>', $class, array_pop($parts));
402
    }
403
404
    private function formatPath($path, $line)
405
    {
406
        $path = $this->escapeHtml($path);
407
        $file = preg_match('#[^/\\\\]*$#', $path, $file) ? $file[0] : $path;
408
409
        if ($linkFormat = $this->fileLinkFormat) {
410
            $link = strtr($this->escapeHtml($linkFormat), array('%f' => $path, '%l' => (int) $line));
411
412
            return sprintf(' in <a href="%s" title="Go to source">%s line %d</a>', $link, $file, $line);
413
        }
414
415
        return sprintf(' in <a title="%s line %3$d" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">%s line %d</a>', $path, $file, $line);
416
    }
417
418
    /**
419
     * Formats an array as a string.
420
     *
421
     * @param array $args The argument array
422
     *
423
     * @return string
424
     */
425
    private function formatArgs(array $args)
426
    {
427
        $result = array();
428
        foreach ($args as $key => $item) {
429
            if ('object' === $item[0]) {
430
                $formattedValue = sprintf('<em>object</em>(%s)', $this->formatClass($item[1]));
431
            } elseif ('array' === $item[0]) {
432
                $formattedValue = sprintf('<em>array</em>(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
433
            } elseif ('string' === $item[0]) {
434
                $formattedValue = sprintf("'%s'", $this->escapeHtml($item[1]));
435
            } elseif ('null' === $item[0]) {
436
                $formattedValue = '<em>null</em>';
437
            } elseif ('boolean' === $item[0]) {
438
                $formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>';
439
            } elseif ('resource' === $item[0]) {
440
                $formattedValue = '<em>resource</em>';
441
            } else {
442
                $formattedValue = str_replace("\n", '', var_export($this->escapeHtml((string) $item[1]), true));
443
            }
444
445
            $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
446
        }
447
448
        return implode(', ', $result);
449
    }
450
451
    /**
452
     * Returns an UTF-8 and HTML encoded string.
453
     *
454
     * @deprecated since version 2.7, to be removed in 3.0.
455
     */
456
    protected static function utf8Htmlize($str)
457
    {
458
        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
459
460
        return htmlspecialchars($str, ENT_QUOTES | (\PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), 'UTF-8');
461
    }
462
463
    /**
464
     * HTML-encodes a string.
465
     */
466
    private function escapeHtml($str)
467
    {
468
        return htmlspecialchars($str, ENT_QUOTES | (\PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), $this->charset);
469
    }
470
471
    /**
472
     * @internal
473
     */
474
    public function catchOutput($buffer)
475
    {
476
        $this->caughtBuffer = $buffer;
477
478
        return '';
479
    }
480
481
    /**
482
     * @internal
483
     */
484
    public function cleanOutput($buffer)
485
    {
486
        if ($this->caughtLength) {
487
            // use substr_replace() instead of substr() for mbstring overloading resistance
488
            $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength);
489
            if (isset($cleanBuffer[0])) {
490
                $buffer = $cleanBuffer;
491
            }
492
        }
493
494
        return $buffer;
495
    }
496
}
497