Completed
Push — master ( fcdf89...4a0ecc )
by Alexander
14:58
created

src/ErrorHandler/HtmlRenderer.php (4 issues)

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