Completed
Push — master ( 6cd300...8845a1 )
by Alexander
02:30
created

src/ErrorHandler/HtmlRenderer.php (1 issue)

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}';
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)
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)
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);
0 ignored issues
show
It seems like $file can also be of type null; however, parameter $file of Yiisoft\Yii\Web\ErrorHan...::renderCallStackItem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

127
            $out .= $this->renderCallStackItem(/** @scrutinizer ignore-type */ $file, $line, $class, $function, $args, $i + 2);
Loading history...
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
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
     * @return string linkified with HTML type/class name.
147
     * @throws \ReflectionException
148
     */
149
    private function addTypeLinks(string $code): string
150
    {
151
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
152
            [,$class,$method] = $matches;
153
            $text = $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
154
        } else {
155
            $class = $code;
156
            $method = null;
157
            $text = $this->htmlEncode($class);
158
        }
159
        $url = null;
160
        $shouldGenerateLink = true;
161
        if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
162
            $reflection = new \ReflectionClass($class);
163
            if ($reflection->hasMethod($method)) {
164
                $reflectionMethod = $reflection->getMethod($method);
165
                $shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
166
            } else {
167
                $shouldGenerateLink = false;
168
            }
169
        }
170
        if ($shouldGenerateLink) {
171
            $url = $this->getTypeUrl($class, $method);
172
        }
173
        if ($url === null) {
174
            return $text;
175
        }
176
        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
177
    }
178
179
    /**
180
     * Returns the informational link URL for a given PHP type/class.
181
     * @param string $class the type or class name.
182
     * @param string|null $method the method name.
183
     * @return string|null the informational link URL.
184
     * @see addTypeLinks()
185
     */
186
    private function getTypeUrl(?string $class, ?string $method): ?string
187
    {
188
        if (strncmp($class, 'Yiisoft\\', 8) !== 0) {
189
            return null;
190
        }
191
        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
192
        $url = "http://www.yiiframework.com/doc-3.0/$page.html";
193
        if ($method) {
194
            $url .= "#$method()-detail";
195
        }
196
        return $url;
197
    }
198
199
    /**
200
     * Converts arguments array to its string representation.
201
     *
202
     * @param array $args arguments array to be converted
203
     * @return string string representation of the arguments array
204
     */
205
    private function argumentsToString(array $args): string
206
    {
207
        $count = 0;
208
        $isAssoc = $args !== array_values($args);
209
        foreach ($args as $key => $value) {
210
            $count++;
211
            if ($count >= 5) {
212
                if ($count > 5) {
213
                    unset($args[$key]);
214
                } else {
215
                    $args[$key] = '...';
216
                }
217
                continue;
218
            }
219
            if (is_object($value)) {
220
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
221
            } elseif (is_bool($value)) {
222
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
223
            } elseif (is_string($value)) {
224
                $fullValue = $this->htmlEncode($value);
225
                if (mb_strlen($value, 'UTF-8') > 32) {
226
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
227
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
228
                } else {
229
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
230
                }
231
            } elseif (is_array($value)) {
232
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
233
            } elseif ($value === null) {
234
                $args[$key] = '<span class="keyword">null</span>';
235
            } elseif (is_resource($value)) {
236
                $args[$key] = '<span class="keyword">resource</span>';
237
            } else {
238
                $args[$key] = '<span class="number">' . $value . '</span>';
239
            }
240
            if (is_string($key)) {
241
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
242
            } elseif ($isAssoc) {
243
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
244
            }
245
        }
246
        return implode(', ', $args);
247
    }
248
249
    /**
250
     * Renders the information about request.
251
     * @return string the rendering result
252
     */
253
    private function renderRequest(): string
254
    {
255
        if ($this->request === null) {
256
            return '';
257
        }
258
259
        $request = $this->request;
260
261
        $output = '';
262
263
        $output .= $request->getMethod() . ' ' .  $request->getUri() . "\n\n";
264
265
        foreach ($request->getHeaders() as $name => $values) {
266
            if ($name === 'Host') {
267
                continue;
268
            }
269
270
            foreach ($values as $value) {
271
                $output .= "$name = $value\n";
272
            }
273
        }
274
275
        $output .= "\n" . $request->getBody()->getContents() . "\n\n";
276
277
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
278
    }
279
280
281
    /**
282
     * Creates string containing HTML link which refers to the home page of determined web-server software
283
     * and its full name.
284
     * @return string server software information hyperlink.
285
     */
286
    private function createServerInformationLink(): string
287
    {
288
        $serverUrls = [
289
            'http://httpd.apache.org/' => ['apache'],
290
            'http://nginx.org/' => ['nginx'],
291
            'http://lighttpd.net/' => ['lighttpd'],
292
            'http://gwan.com/' => ['g-wan', 'gwan'],
293
            'http://iis.net/' => ['iis', 'services'],
294
            'https://secure.php.net/manual/en/features.commandline.webserver.php' => ['development'],
295
        ];
296
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
297
            foreach ($serverUrls as $url => $keywords) {
298
                foreach ($keywords as $keyword) {
299
                    if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
300
                        return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
301
                    }
302
                }
303
            }
304
        }
305
        return '';
306
    }
307
308
    /**
309
     * Creates string containing HTML link which refers to the page with the current version
310
     * of the framework and version number text.
311
     * @return string framework version information hyperlink.
312
     */
313
    public function createFrameworkVersionLink(): string
314
    {
315
        return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Info::frameworkVersion()) . '</a>';
316
    }
317
}
318