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

HtmlRenderer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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