Completed
Push — master ( b09865...c05ece )
by Alexander
03:58
created

src/ErrorHandler/HtmlRenderer.php (7 issues)

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