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

src/ErrorHandler/HtmlRenderer.php (1 issue)

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)
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)
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
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