Completed
Pull Request — master (#88)
by Alexander
02:14
created

HtmlRenderer::createServerInformationLink()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 3
nop 0
dl 0
loc 20
rs 9.5222
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
    private const MAX_SOURCE_LINES = 19;
12
    private const MAX_TRACE_LINES = 13;
13
14
    public $displayVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION'];
15
    public $traceLine = '{html}';
16
17
    public function render(\Throwable $e): string
18
    {
19
        return $this->renderTemplate('exception', [
20
            'exception' => $e,
21
        ]);
22
    }
23
24
    private function htmlEncode(string $text): string
25
    {
26
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
27
    }
28
29
    private function renderTemplate(string $template, array $params): string
30
    {
31
        $path = __DIR__ . '/templates/' . $template . '.php';
32
        if (!file_exists($path)) {
33
            throw new RuntimeException("$template not found at $path");
34
        }
35
36
        $renderer = function () use ($path, $params) {
37
            extract($params, EXTR_OVERWRITE);
38
            require $path;
39
        };
40
41
        $obInitialLevel = ob_get_level();
42
        ob_start();
43
        ob_implicit_flush(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $flag of ob_implicit_flush(). ( Ignorable by Annotation )

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

43
        ob_implicit_flush(/** @scrutinizer ignore-type */ false);
Loading history...
44
        try {
45
            $renderer->bindTo($this)();
46
            return ob_get_clean();
47
        } catch (\Throwable $e) {
48
            while (ob_get_level() > $obInitialLevel) {
49
                if (!@ob_end_clean()) {
50
                    ob_clean();
51
                }
52
            }
53
            throw $e;
54
        }
55
    }
56
57
    /**
58
     * Renders the previous exception stack for a given Exception.
59
     * @param \Throwable $exception the exception whose precursors should be rendered.
60
     * @return string HTML content of the rendered previous exceptions.
61
     * Empty string if there are none.
62
     * @throws \Throwable
63
     */
64
    private function renderPreviousExceptions(\Throwable $exception)
0 ignored issues
show
Unused Code introduced by
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...
65
    {
66
        if (($previous = $exception->getPrevious()) !== null) {
67
            return $this->renderTemplate('previousException', ['exception' => $previous]);
68
        }
69
        return '';
70
    }
71
72
    /**
73
     * Renders a single call stack element.
74
     * @param string|null $file name where call has happened.
75
     * @param int|null $line number on which call has happened.
76
     * @param string|null $class called class name.
77
     * @param string|null $method called function/method name.
78
     * @param array $args array of method arguments.
79
     * @param int $index number of the call stack element.
80
     * @return string HTML content of the rendered call stack element.
81
     */
82
    private function renderCallStackItem(string $file, ?int $line, ?string $class, ?string $method, array $args, int $index): string
83
    {
84
        $lines = [];
85
        $begin = $end = 0;
86
        if ($file !== null && $line !== null) {
87
            $line--; // adjust line number from one-based to zero-based
88
            $lines = @file($file);
89
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
90
                return '';
91
            }
92
            $half = (int)(($index === 1 ? self::MAX_SOURCE_LINES : self::MAX_TRACE_LINES) / 2);
93
            $begin = $line - $half > 0 ? $line - $half : 0;
94
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
95
        }
96
        return $this->renderTemplate('callStackItem', [
97
            'file' => $file,
98
            'line' => $line,
99
            'class' => $class,
100
            'method' => $method,
101
            'index' => $index,
102
            'lines' => $lines,
103
            'begin' => $begin,
104
            'end' => $end,
105
            'args' => $args,
106
        ]);
107
    }
108
109
    /**
110
     * Renders call stack.
111
     * @param \Throwable $exception exception to get call stack from
112
     * @return string HTML content of the rendered call stack.
113
     */
114
    private function renderCallStack(\Throwable $exception)
0 ignored issues
show
Unused Code introduced by
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($exception->getFile(), $exception->getLine(), null, null, [], 1);
118
        for ($i = 0, $trace = $exception->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
Bug introduced by
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
0 ignored issues
show
Unused Code introduced by
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::getFrameworkPath() . 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
0 ignored issues
show
Unused Code introduced by
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...
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 global variables of the request.
251
     * List of global variables is defined in [[displayVars]].
252
     * @return string the rendering result
253
     * @see displayVars
254
     */
255
    private function renderRequest(): string
0 ignored issues
show
Unused Code introduced by
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...
256
    {
257
        $request = '';
258
        foreach ($this->displayVars as $name) {
259
            if (!empty($GLOBALS[$name])) {
260
                $request .= '$' . $name . ' = ' . VarDumper::export($GLOBALS[$name]) . ";\n\n";
261
            }
262
        }
263
        return '<pre>' . $this->htmlEncode(rtrim($request, "\n")) . '</pre>';
264
    }
265
266
267
    /**
268
     * Creates string containing HTML link which refers to the home page of determined web-server software
269
     * and its full name.
270
     * @return string server software information hyperlink.
271
     */
272
    private function createServerInformationLink(): string
0 ignored issues
show
Unused Code introduced by
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...
273
    {
274
        $serverUrls = [
275
            'http://httpd.apache.org/' => ['apache'],
276
            'http://nginx.org/' => ['nginx'],
277
            'http://lighttpd.net/' => ['lighttpd'],
278
            'http://gwan.com/' => ['g-wan', 'gwan'],
279
            'http://iis.net/' => ['iis', 'services'],
280
            'https://secure.php.net/manual/en/features.commandline.webserver.php' => ['development'],
281
        ];
282
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
283
            foreach ($serverUrls as $url => $keywords) {
284
                foreach ($keywords as $keyword) {
285
                    if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
286
                        return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
287
                    }
288
                }
289
            }
290
        }
291
        return '';
292
    }
293
294
    /**
295
     * Creates string containing HTML link which refers to the page with the current version
296
     * of the framework and version number text.
297
     * @return string framework version information hyperlink.
298
     */
299
    public function createFrameworkVersionLink(): string
300
    {
301
        return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Info::getVersion()) . '</a>';
302
    }
303
}
304