Test Failed
Pull Request — master (#25)
by Evgeniy
02:14
created

HtmlRenderer   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 56
eloc 137
dl 0
loc 369
rs 5.5199
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A renderTemplate() 0 25 6
A render() 0 5 1
A htmlEncode() 0 3 1
B renderCallStackItem() 0 26 9
A createServerInformationLink() 0 26 5
B renderCallStack() 0 19 8
A renderRequest() 0 17 4
A isCoreFile() 0 3 2
A renderVerbose() 0 5 1
A renderPreviousExceptions() 0 8 2
C argumentsToString() 0 49 14
A renderCurl() 0 9 2
A __construct() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like HtmlRenderer 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.

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 HtmlRenderer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler\Renderer;
6
7
use Alexkart\CurlBuilder\Command;
8
use Psr\Http\Message\ServerRequestInterface;
9
use RuntimeException;
10
use Throwable;
11
use Yiisoft\ErrorHandler\ThrowableRenderer;
12
13
use function array_values;
14
use function dirname;
15
use function extract;
16
use function file;
17
use function file_exists;
18
use function func_get_arg;
19
use function get_class;
20
use function htmlspecialchars;
21
use function implode;
22
use function is_array;
23
use function is_bool;
24
use function is_object;
25
use function is_resource;
26
use function is_string;
27
use function ksort;
28
use function mb_strlen;
29
use function mb_substr;
30
use function ob_clean;
31
use function ob_get_clean;
32
use function ob_get_level;
33
use function ob_end_clean;
34
use function ob_implicit_flush;
35
use function ob_start;
36
use function realpath;
37
use function rtrim;
38
use function stripos;
39
use function strpos;
40
41
/**
42
 * Formats exception into HTML string.
43
 */
44
final class HtmlRenderer extends ThrowableRenderer
45
{
46
    /**
47
     * @var string The full path to the default template directory.
48
     */
49
    private string $defaultTemplatePath;
50
51
    /**
52
     * @var string The full path of the template file for rendering exceptions without call stack information.
53
     *
54
     * This template should be used in production.
55
     */
56
    private string $template;
57
58
    /**
59
     * @var string The full path of the template file for rendering exceptions with call stack information.
60
     *
61
     * This template should be used in development.
62
     */
63
    private string $verboseTemplate;
64
65
    /**
66
     * @var int The maximum number of source code lines to be displayed. Defaults to 19.
67
     */
68
    private int $maxSourceLines;
69
70
    /**
71
     * @var int The maximum number of trace source code lines to be displayed. Defaults to 13.
72
     */
73
    private int $maxTraceLines;
74
75
    /**
76
     * @var string|null The trace header line with placeholders to be be substituted. Defaults to null.
77
     *
78
     * The placeholders are {file}, {line} and {ide}. A typical use case is the creation of IDE-specific links,
79
     * since when you click on a trace header link, it opens directly in the IDE. You can also insert custom content.
80
     *
81
     * Example IDE link:
82
     *
83
     * ```
84
     * <a href="ide://open?file={file}&line={line}">{ide}</a>
85
     * ```
86
     */
87
    private ?string $traceHeaderLine;
88
89
    /**
90
     * @param array $settings Settings can have the following keys:
91
     * - template: string, full path of the template file for rendering exceptions without call stack information.
92
     * - verboseTemplate: string, full path of the template file for rendering exceptions with call stack information.
93
     * - maxSourceLines: int, maximum number of source code lines to be displayed. Defaults to 19.
94
     * - maxTraceLines: int, maximum number of trace source code lines to be displayed. Defaults to 13.
95
     * - traceHeaderLine: string, trace header line with placeholders to be be substituted. Defaults to null.
96
     */
97
    public function __construct(array $settings = [])
98
    {
99
        $this->defaultTemplatePath = dirname(__DIR__, 2) . '/templates';
100
        $this->template = $settings['template'] ?? $this->defaultTemplatePath . '/production.php';
101
        $this->verboseTemplate = $settings['verboseTemplate'] ?? $this->defaultTemplatePath . '/development.php';
102
        $this->maxSourceLines = $settings['maxSourceLines']  ?? 19;
103
        $this->maxTraceLines = $settings['maxTraceLines']  ?? 13;
104
        $this->traceHeaderLine = $settings['traceHeaderLine'] ?? null;
105
    }
106
107
    public function render(Throwable $t, ServerRequestInterface $request = null): string
108
    {
109
        return $this->renderTemplate($this->template, [
110
            'request' => $request,
111
            'throwable' => $t,
112
        ]);
113
    }
114
115
    public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): string
116
    {
117
        return $this->renderTemplate($this->verboseTemplate, [
118
            'request' => $request,
119
            'throwable' => $t,
120
        ]);
121
    }
122
123
    /**
124
     * Encodes special characters into HTML entities for use as a content.
125
     *
126
     * @param string $content The content to be encoded.
127
     *
128
     * @return string Encoded content.
129
     */
130
    private function htmlEncode(string $content): string
131
    {
132
        return htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
133
    }
134
135
    /**
136
     * Renders a template.
137
     *
138
     * @param string $path The full path of the template file for rendering.
139
     * @param array $parameters The name-value pairs that will be extracted and made available in the template file.
140
     *
141
     * @throws Throwable
142
     *
143
     * @return string The rendering result.
144
     */
145
    private function renderTemplate(string $path, array $parameters): string
146
    {
147
        if (!file_exists($path)) {
148
            throw new RuntimeException("Template not found at $path");
149
        }
150
151
        $renderer = function (): void {
152
            extract(func_get_arg(1), EXTR_OVERWRITE);
153
            require func_get_arg(0);
154
        };
155
156
        $obInitialLevel = ob_get_level();
157
        ob_start();
158
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
159
160
        try {
161
            $renderer->bindTo($this)($path, $parameters);
162
            return ob_get_clean();
163
        } catch (Throwable $e) {
164
            while (ob_get_level() > $obInitialLevel) {
165
                if (!@ob_end_clean()) {
166
                    ob_clean();
167
                }
168
            }
169
            throw $e;
170
        }
171
    }
172
173
    /**
174
     * Renders the previous exception stack for a given Exception.
175
     *
176
     * @param Throwable $t The exception whose precursors should be rendered.
177
     *
178
     * @throws Throwable
179
     *
180
     * @return string HTML content of the rendered previous exceptions. Empty string if there are none.
181
     */
182
    private function renderPreviousExceptions(Throwable $t): string
0 ignored issues
show
Unused Code introduced by
The method renderPreviousExceptions() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
183
    {
184
        if (($previous = $t->getPrevious()) !== null) {
185
            $templatePath = $this->defaultTemplatePath . '/_previous-exception.php';
186
            return $this->renderTemplate($templatePath, ['throwable' => $previous]);
187
        }
188
189
        return '';
190
    }
191
192
    /**
193
     * Renders call stack.
194
     *
195
     * @param Throwable $t The exception to get call stack from.
196
     *
197
     * @throws Throwable
198
     *
199
     * @return string HTML content of the rendered call stack.
200
     */
201
    private function renderCallStack(Throwable $t): string
0 ignored issues
show
Unused Code introduced by
The method renderCallStack() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
202
    {
203
        $out = '<ul>';
204
        $out .= $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1);
205
206
        for ($i = 0, $trace = $t->getTrace(), $length = count($trace); $i < $length; ++$i) {
207
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
208
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
209
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
210
            $function = null;
211
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
212
                $function = $trace[$i]['function'];
213
            }
214
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
215
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
216
        }
217
218
        $out .= '</ul>';
219
        return $out;
220
    }
221
222
    /**
223
     * Renders a single call stack element.
224
     *
225
     * @param string|null $file The name where call has happened.
226
     * @param int|null $line The number on which call has happened.
227
     * @param string|null $class The called class name.
228
     * @param string|null $function The called function/method name.
229
     * @param array $args The array of method arguments.
230
     * @param int $index The number of the call stack element.
231
     *
232
     * @throws Throwable
233
     *
234
     * @return string HTML content of the rendered call stack element.
235
     */
236
    private function renderCallStackItem(?string $file, ?int $line, ?string $class, ?string $function, array $args, int $index): string
237
    {
238
        $lines = [];
239
        $begin = $end = 0;
240
241
        if ($file !== null && $line !== null) {
242
            $line--; // adjust line number from one-based to zero-based
243
            $lines = @file($file);
244
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
245
                return '';
246
            }
247
            $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
248
            $begin = $line - $half > 0 ? $line - $half : 0;
249
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
250
        }
251
252
        return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-item.php', [
253
            'file' => $file,
254
            'line' => $line,
255
            'class' => $class,
256
            'function' => $function,
257
            'index' => $index,
258
            'lines' => $lines,
259
            'begin' => $begin,
260
            'end' => $end,
261
            'args' => $args,
262
        ]);
263
    }
264
265
    /**
266
     * Converts arguments array to its string representation.
267
     *
268
     * @param array $args arguments array to be converted
269
     *
270
     * @return string The string representation of the arguments array.
271
     */
272
    private function argumentsToString(array $args): string
273
    {
274
        $count = 0;
275
        $isAssoc = $args !== array_values($args);
276
277
        foreach ($args as $key => $value) {
278
            $count++;
279
280
            if ($count >= 5) {
281
                if ($count > 5) {
282
                    unset($args[$key]);
283
                } else {
284
                    $args[$key] = '...';
285
                }
286
                continue;
287
            }
288
289
            if (is_object($value)) {
290
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
291
            } elseif (is_bool($value)) {
292
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
293
            } elseif (is_string($value)) {
294
                $fullValue = $this->htmlEncode($value);
295
                if (mb_strlen($value, 'UTF-8') > 32) {
296
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
297
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
298
                } else {
299
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
300
                }
301
            } elseif (is_array($value)) {
302
                unset($args[$key]);
303
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
304
            } elseif ($value === null) {
305
                $args[$key] = '<span class="keyword">null</span>';
306
            } elseif (is_resource($value)) {
307
                $args[$key] = '<span class="keyword">resource</span>';
308
            } else {
309
                $args[$key] = '<span class="number">' . $value . '</span>';
310
            }
311
312
            if (is_string($key)) {
313
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
314
            } elseif ($isAssoc) {
315
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
316
            }
317
        }
318
319
        ksort($args);
320
        return implode(', ', $args);
321
    }
322
323
    /**
324
     * Renders the information about request.
325
     *
326
     * @param ServerRequestInterface $request
327
     *
328
     * @return string The rendering result.
329
     */
330
    private function renderRequest(ServerRequestInterface $request): string
0 ignored issues
show
Unused Code introduced by
The method renderRequest() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
331
    {
332
        $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
333
334
        foreach ($request->getHeaders() as $name => $values) {
335
            if ($name === 'Host') {
336
                continue;
337
            }
338
339
            foreach ($values as $value) {
340
                $output .= "$name: $value\n";
341
            }
342
        }
343
344
        $output .= "\n" . $request->getBody() . "\n\n";
345
346
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
347
    }
348
349
    /**
350
     * Renders the information about curl request.
351
     *
352
     * @param ServerRequestInterface $request
353
     *
354
     * @return string The rendering result.
355
     */
356
    private function renderCurl(ServerRequestInterface $request): string
0 ignored issues
show
Unused Code introduced by
The method renderCurl() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
357
    {
358
        try {
359
            $output = (new Command())->setRequest($request)->build();
360
        } catch (Throwable $e) {
361
            $output = 'Error generating curl command: ' . $e->getMessage();
362
        }
363
364
        return $this->htmlEncode($output);
365
    }
366
367
    /**
368
     * Creates string containing HTML link which refers to the home page
369
     * of determined web-server software and its full name.
370
     *
371
     * @param ServerRequestInterface $request
372
     *
373
     * @return string The server software information hyperlink.
374
     */
375
    private function createServerInformationLink(ServerRequestInterface $request): string
0 ignored issues
show
Unused Code introduced by
The method createServerInformationLink() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
376
    {
377
        $serverSoftware = $request->getServerParams()['SERVER_SOFTWARE'] ?? null;
378
379
        if ($serverSoftware === null) {
380
            return '';
381
        }
382
383
        $serverUrls = [
384
            'https://httpd.apache.org/' => ['apache'],
385
            'https://nginx.org/' => ['nginx'],
386
            'https://lighttpd.net/' => ['lighttpd'],
387
            'https://iis.net/' => ['iis', 'services'],
388
            'https://www.php.net/manual/en/features.commandline.webserver.php' => ['development'],
389
        ];
390
391
        foreach ($serverUrls as $url => $keywords) {
392
            foreach ($keywords as $keyword) {
393
                if (stripos($serverSoftware, $keyword) !== false) {
394
                    return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">'
395
                        . $this->htmlEncode($serverSoftware) . '</a>';
396
                }
397
            }
398
        }
399
400
        return '';
401
    }
402
403
    /**
404
     * Determines whether given name of the file belongs to the framework.
405
     *
406
     * @param string|null $file The name to be checked.
407
     *
408
     * @return bool Whether given name of the file belongs to the framework.
409
     */
410
    public function isCoreFile(?string $file): bool
411
    {
412
        return $file === null || strpos(realpath($file), dirname(__DIR__, 3)) === 0;
413
    }
414
}
415