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