Passed
Pull Request — master (#217)
by
unknown
01:43
created

HtmlRenderer::renderTemplate()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 20
c 2
b 0
f 0
nc 16
nop 2
dl 0
loc 30
rs 8.9777
ccs 0
cts 19
cp 0
crap 42
1
<?php
2
3
namespace Yiisoft\Yii\Web\ErrorHandler;
4
5
use Alexkart\CurlBuilder\Command;
6
use Yiisoft\Yii\Web\Info;
7
8
final class HtmlRenderer extends ThrowableRenderer
9
{
10
    private const DEFAULT_TEMPLATES = [
11
        'callStackItem',
12
        'error',
13
        'exception',
14
        'previousException'
15
    ];
16
17
    private int $maxSourceLines = 19;
18
    private int $maxTraceLines = 13;
19
20
    private string $traceLine = '{html}';
21
22
    private string $errorTemplate;
23
    private string $exceptionTemplate;
24
25
    public function __construct(array $templates = [])
26
    {
27
        $this->errorTemplate = isset($templates['error']) ? $templates['error'] : 'error';
28
        $this->exceptionTemplate = isset($templates['exception']) ? $templates['exception'] : 'exception';
29
    }
30
31
    public function withMaxSourceLines(int $maxSourceLines): self
32
    {
33
        $new = clone $this;
34
        $new->maxSourceLines = $maxSourceLines;
35
        return $new;
36
    }
37
38
    public function withMaxTraceLines(int $maxTraceLines): self
39
    {
40
        $new = clone $this;
41
        $new->maxTraceLines = $maxTraceLines;
42
        return $new;
43
    }
44
45
    public function withTraceLine(string $traceLine): self
46
    {
47
        $new = clone $this;
48
        $new->traceLine = $traceLine;
49
        return $new;
50
    }
51
52
    public function render(\Throwable $t): string
53
    {
54
        return $this->renderTemplate($this->errorTemplate, [
55
            'throwable' => $t,
56
        ]);
57
    }
58
59
    public function renderVerbose(\Throwable $t): string
60
    {
61
        return $this->renderTemplate($this->exceptionTemplate, [
62
            'throwable' => $t,
63
        ]);
64
    }
65
66
    private function htmlEncode(string $text): string
67
    {
68
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
69
    }
70
71
    private function renderTemplate(string $template, array $params): string
72
    {
73
        if (in_array($template, self::DEFAULT_TEMPLATES)) {
74
            $path = __DIR__ . '/templates/' . $template . '.php';
75
        } else {
76
            $path = $template;
77
        }
78
79
        if (!file_exists($path)) {
80
            throw new \RuntimeException("Template not found at $path");
81
        }
82
83
        $renderer = function (): void {
84
            extract(func_get_arg(1), EXTR_OVERWRITE);
85
            require func_get_arg(0);
86
        };
87
88
        $obInitialLevel = ob_get_level();
89
        ob_start();
90
        ob_implicit_flush(0);
91
        try {
92
            $renderer->bindTo($this)($path, $params);
93
            return ob_get_clean();
94
        } catch (\Throwable $e) {
95
            while (ob_get_level() > $obInitialLevel) {
96
                if (!@ob_end_clean()) {
97
                    ob_clean();
98
                }
99
            }
100
            throw $e;
101
        }
102
    }
103
104
    /**
105
     * Renders the previous exception stack for a given Exception.
106
     * @param \Throwable $t the exception whose precursors should be rendered.
107
     * @return string HTML content of the rendered previous exceptions.
108
     * Empty string if there are none.
109
     * @throws \Throwable
110
     */
111
    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...
112
    {
113
        if (($previous = $t->getPrevious()) !== null) {
114
            return $this->renderTemplate('previousException', ['throwable' => $previous]);
115
        }
116
        return '';
117
    }
118
119
    /**
120
     * Renders a single call stack element.
121
     * @param string|null $file name where call has happened.
122
     * @param int|null $line number on which call has happened.
123
     * @param string|null $class called class name.
124
     * @param string|null $method called function/method name.
125
     * @param array $args array of method arguments.
126
     * @param int $index number of the call stack element.
127
     * @return string HTML content of the rendered call stack element.
128
     * @throws \Throwable
129
     */
130
    private function renderCallStackItem(?string $file, ?int $line, ?string $class, ?string $method, array $args, int $index): string
131
    {
132
        $lines = [];
133
        $begin = $end = 0;
134
        if ($file !== null && $line !== null) {
135
            $line--; // adjust line number from one-based to zero-based
136
            $lines = @file($file);
137
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
138
                return '';
139
            }
140
            $half = (int)(($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
141
            $begin = $line - $half > 0 ? $line - $half : 0;
142
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
143
        }
144
        return $this->renderTemplate('callStackItem', [
145
            'file' => $file,
146
            'line' => $line,
147
            'class' => $class,
148
            'method' => $method,
149
            'index' => $index,
150
            'lines' => $lines,
151
            'begin' => $begin,
152
            'end' => $end,
153
            'args' => $args,
154
        ]);
155
    }
156
157
    /**
158
     * Renders call stack.
159
     * @param \Throwable $t exception to get call stack from
160
     * @return string HTML content of the rendered call stack.
161
     * @throws \Throwable
162
     */
163
    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...
164
    {
165
        $out = '<ul>';
166
        $out .= $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1);
167
        for ($i = 0, $trace = $t->getTrace(), $length = count($trace); $i < $length; ++$i) {
168
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
169
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
170
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
171
            $function = null;
172
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
173
                $function = $trace[$i]['function'];
174
            }
175
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
176
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
177
        }
178
        $out .= '</ul>';
179
        return $out;
180
    }
181
182
    /**
183
     * Determines whether given name of the file belongs to the framework.
184
     * @param string $file name to be checked.
185
     * @return bool whether given name of the file belongs to the framework.
186
     */
187
    private function isCoreFile(?string $file): bool
0 ignored issues
show
Unused Code introduced by
The method isCoreFile() 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...
188
    {
189
        return $file === null || strpos(realpath($file), Info::frameworkPath() . DIRECTORY_SEPARATOR) === 0;
190
    }
191
192
    /**
193
     * Adds informational links to the given PHP type/class.
194
     * @param string $code type/class name to be linkified.
195
     * @param string $title custom title to use
196
     * @return string linkified with HTML type/class name.
197
     * @throws \ReflectionException
198
     */
199
    private function addTypeLinks(string $code, string $title = null): string
0 ignored issues
show
Unused Code introduced by
The method addTypeLinks() 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...
200
    {
201
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
202
            [, $class, $method] = $matches;
203
            $text = $title ? $this->htmlEncode($title) : $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
204
        } else {
205
            $class = $code;
206
            $method = null;
207
            $text = $title ? $this->htmlEncode($title) : $this->htmlEncode($class);
208
        }
209
        $url = null;
210
        $shouldGenerateLink = true;
211
        if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
212
            $reflection = new \ReflectionClass($class);
213
            if ($reflection->hasMethod($method)) {
214
                $reflectionMethod = $reflection->getMethod($method);
215
                $shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
216
            } else {
217
                $shouldGenerateLink = false;
218
            }
219
        }
220
        if ($shouldGenerateLink) {
221
            $url = $this->getTypeUrl($class, $method);
222
        }
223
        if ($url === null) {
224
            return $text;
225
        }
226
        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
227
    }
228
229
    /**
230
     * Returns the informational link URL for a given PHP type/class.
231
     * @param string $class the type or class name.
232
     * @param string|null $method the method name.
233
     * @return string|null the informational link URL.
234
     * @see addTypeLinks()
235
     */
236
    private function getTypeUrl(?string $class, ?string $method): ?string
237
    {
238
        if (strncmp($class, 'Yiisoft\\', 8) !== 0) {
239
            return null;
240
        }
241
        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
242
        $url = "http://www.yiiframework.com/doc-3.0/$page.html";
243
        if ($method) {
244
            $url .= "#$method()-detail";
245
        }
246
        return $url;
247
    }
248
249
    /**
250
     * Converts arguments array to its string representation.
251
     *
252
     * @param array $args arguments array to be converted
253
     * @return string string representation of the arguments array
254
     */
255
    private function argumentsToString(array $args): string
256
    {
257
        $count = 0;
258
        $isAssoc = $args !== array_values($args);
259
        foreach ($args as $key => $value) {
260
            $count++;
261
            if ($count >= 5) {
262
                if ($count > 5) {
263
                    unset($args[$key]);
264
                } else {
265
                    $args[$key] = '...';
266
                }
267
                continue;
268
            }
269
            if (is_object($value)) {
270
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
271
            } elseif (is_bool($value)) {
272
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
273
            } elseif (is_string($value)) {
274
                $fullValue = $this->htmlEncode($value);
275
                if (mb_strlen($value, 'UTF-8') > 32) {
276
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
277
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
278
                } else {
279
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
280
                }
281
            } elseif (is_array($value)) {
282
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
283
            } elseif ($value === null) {
284
                $args[$key] = '<span class="keyword">null</span>';
285
            } elseif (is_resource($value)) {
286
                $args[$key] = '<span class="keyword">resource</span>';
287
            } else {
288
                $args[$key] = '<span class="number">' . $value . '</span>';
289
            }
290
            if (is_string($key)) {
291
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
292
            } elseif ($isAssoc) {
293
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
294
            }
295
        }
296
        return implode(', ', $args);
297
    }
298
299
    /**
300
     * Renders the information about request.
301
     * @return string the rendering result
302
     */
303
    private function renderRequest(): 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...
304
    {
305
        if ($this->request === null) {
306
            return '';
307
        }
308
309
        $request = $this->request;
310
        $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
311
312
        foreach ($request->getHeaders() as $name => $values) {
313
            if ($name === 'Host') {
314
                continue;
315
            }
316
317
            foreach ($values as $value) {
318
                $output .= "$name: $value\n";
319
            }
320
        }
321
322
        $output .= "\n" . $request->getBody() . "\n\n";
323
324
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
325
    }
326
327
    private function renderCurl(): 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...
328
    {
329
        try {
330
            $output = (new Command())->setRequest($this->request)->build();
331
        } catch (\Throwable $e) {
332
            $output = 'Error generating curl command: ' . $e->getMessage();
333
        }
334
335
        return $this->htmlEncode($output);
336
    }
337
338
339
    /**
340
     * Creates string containing HTML link which refers to the home page of determined web-server software
341
     * and its full name.
342
     * @return string server software information hyperlink.
343
     */
344
    private function createServerInformationLink(): 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...
345
    {
346
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
347
            $serverUrls = [
348
                'http://httpd.apache.org/' => ['apache'],
349
                'http://nginx.org/' => ['nginx'],
350
                'http://lighttpd.net/' => ['lighttpd'],
351
                'http://gwan.com/' => ['g-wan', 'gwan'],
352
                'http://iis.net/' => ['iis', 'services'],
353
                'https://secure.php.net/manual/en/features.commandline.webserver.php' => ['development'],
354
            ];
355
356
            foreach ($serverUrls as $url => $keywords) {
357
                foreach ($keywords as $keyword) {
358
                    if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
359
                        return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
360
                    }
361
                }
362
            }
363
        }
364
        return '';
365
    }
366
367
    /**
368
     * Creates string containing HTML link which refers to the page with the current version
369
     * of the framework and version number text.
370
     * @return string framework version information hyperlink.
371
     */
372
    public function createFrameworkVersionLink(): string
373
    {
374
        return '<a href="http://github.com/yiisoft/yii-web/" target="_blank">' . $this->htmlEncode(Info::frameworkVersion()) . '</a>';
375
    }
376
}
377