Passed
Push — master ( 24ab68...9218d1 )
by Alexander
04:58 queued 57s
created

HtmlRenderer::renderRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 9
rs 10
c 0
b 0
f 0
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
introduced by Alexander Makarov
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 ([email protected]_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
Unused Code introduced by Alexander Makarov
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
Unused Code introduced by Alexander Makarov
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);
0 ignored issues
show
Bug introduced by Alexander Makarov
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

130
            $out .= $this->renderCallStackItem(/** @scrutinizer ignore-type */ $file, $line, $class, $function, $args, $i + 2);
Loading history...
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
0 ignored issues
show
Unused Code introduced by Alexander Makarov
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...
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
0 ignored issues
show
Unused Code introduced by Alexander Makarov
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...
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
Unused Code introduced by Alexander Makarov
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
0 ignored issues
show
Unused Code introduced by Alexander Makarov
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...
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