Passed
Pull Request — master (#168)
by Alexander
11:02
created

HtmlRenderer::renderVerbose()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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