Test Failed
Pull Request — master (#334)
by Alexander
02:54
created

HtmlRenderer::withTraceLine()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\ErrorHandler;
6
7
use Alexkart\CurlBuilder\Command;
0 ignored issues
show
Bug introduced by
The type Alexkart\CurlBuilder\Command was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Yiisoft\Yii\Web\Info;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Yii\Web\Info was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
10
final class HtmlRenderer extends ThrowableRenderer
11
{
12
    private int $maxSourceLines = 19;
13
    private int $maxTraceLines = 13;
14
15
    private string $traceLine = '{html}';
16
17
    private string $templatePath;
18
19
    private string $errorTemplate;
20
    private string $exceptionTemplate;
21
22
    public function __construct(array $templates = [])
23
    {
24
        $this->templatePath = $templates['path'] ?? __DIR__ . '/templates';
25
        $this->errorTemplate = $templates['error'] ?? $this->templatePath . '/error.php';
26
        $this->exceptionTemplate = $templates['exception'] ?? $this->templatePath . '/exception.php';
27
    }
28
29
    public function withMaxSourceLines(int $maxSourceLines): self
30
    {
31
        $new = clone $this;
32
        $new->maxSourceLines = $maxSourceLines;
33
        return $new;
34
    }
35
36
    public function withMaxTraceLines(int $maxTraceLines): self
37
    {
38
        $new = clone $this;
39
        $new->maxTraceLines = $maxTraceLines;
40
        return $new;
41
    }
42
43
    public function withTraceLine(string $traceLine): self
44
    {
45
        $new = clone $this;
46
        $new->traceLine = $traceLine;
47
        return $new;
48
    }
49
50
    public function render(\Throwable $t): string
51
    {
52
        return $this->renderTemplate($this->errorTemplate, [
53
            'throwable' => $t,
54
        ]);
55
    }
56
57
    public function renderVerbose(\Throwable $t): string
58
    {
59
        return $this->renderTemplate($this->exceptionTemplate, [
60
            'throwable' => $t,
61
        ]);
62
    }
63
64
    private function htmlEncode(string $text): string
65
    {
66
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
67
    }
68
69
    private function renderTemplate(string $path, array $params): string
70
    {
71
        if (!file_exists($path)) {
72
            throw new \RuntimeException("Template not found at $path");
73
        }
74
75
        $renderer = function (): void {
76
            extract(func_get_arg(1), EXTR_OVERWRITE);
77
            require func_get_arg(0);
78
        };
79
80
        $obInitialLevel = ob_get_level();
81
        ob_start();
82
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
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

82
        PHP_VERSION_ID >= 80000 ? ob_implicit_flush(/** @scrutinizer ignore-type */ false) : ob_implicit_flush(0);
Loading history...
83
        try {
84
            $renderer->bindTo($this)($path, $params);
85
            return ob_get_clean();
86
        } catch (\Throwable $e) {
87
            while (ob_get_level() > $obInitialLevel) {
88
                if (!@ob_end_clean()) {
89
                    ob_clean();
90
                }
91
            }
92
            throw $e;
93
        }
94
    }
95
96
    /**
97
     * Renders the previous exception stack for a given Exception.
98
     *
99
     * @param \Throwable $t the exception whose precursors should be rendered.
100
     *
101
     * @throws \Throwable
102
     *
103
     * @return string HTML content of the rendered previous exceptions.
104
     * Empty string if there are none.
105
     */
106
    public function renderPreviousExceptions(\Throwable $t): string
107
    {
108
        if (($previous = $t->getPrevious()) !== null) {
109
            $templatePath = $this->templatePath . '/previousException.php';
110
            return $this->renderTemplate($templatePath, ['throwable' => $previous]);
111
        }
112
        return '';
113
    }
114
115
    /**
116
     * Renders a single call stack element.
117
     *
118
     * @param string|null $file name where call has happened.
119
     * @param int|null $line number on which call has happened.
120
     * @param string|null $class called class name.
121
     * @param string|null $method called function/method name.
122
     * @param array $args array of method arguments.
123
     * @param int $index number of the call stack element.
124
     *
125
     * @throws \Throwable
126
     *
127
     * @return string HTML content of the rendered call stack element.
128
     */
129
    private function renderCallStackItem(?string $file, ?int $line, ?string $class, ?string $method, array $args, int $index): string
130
    {
131
        $lines = [];
132
        $begin = $end = 0;
133
        if ($file !== null && $line !== null) {
134
            $line--; // adjust line number from one-based to zero-based
135
            $lines = @file($file);
136
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
137
                return '';
138
            }
139
            $half = (int)(($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
140
            $begin = $line - $half > 0 ? $line - $half : 0;
141
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
142
        }
143
        $templatePath = $this->templatePath . '/callStackItem.php';
144
        return $this->renderTemplate($templatePath, [
145
            'file' => $file,
146
            'line' => $line,
147
            'class' => $class,
148
            'method' => $method,
149
            'index' => $index,
150
            'lines' => $lines,
151
            'begin' => $begin,
152
            'end' => $end,
153
            'args' => $args,
154
        ]);
155
    }
156
157
    /**
158
     * Renders call stack.
159
     *
160
     * @param \Throwable $t exception to get call stack from
161
     *
162
     * @throws \Throwable
163
     *
164
     * @return string HTML content of the rendered call stack.
165
     */
166
    public function renderCallStack(\Throwable $t): string
167
    {
168
        $out = '<ul>';
169
        $out .= $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1);
170
        for ($i = 0, $trace = $t->getTrace(), $length = count($trace); $i < $length; ++$i) {
171
            $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null;
172
            $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null;
173
            $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
174
            $function = null;
175
            if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
176
                $function = $trace[$i]['function'];
177
            }
178
            $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
179
            $out .= $this->renderCallStackItem($file, $line, $class, $function, $args, $i + 2);
180
        }
181
        $out .= '</ul>';
182
        return $out;
183
    }
184
185
    /**
186
     * Determines whether given name of the file belongs to the framework.
187
     *
188
     * @param string|null $file name to be checked.
189
     *
190
     * @return bool whether given name of the file belongs to the framework.
191
     */
192
    public function isCoreFile(?string $file): bool
193
    {
194
        return $file === null || strpos(realpath($file), Info::frameworkPath() . DIRECTORY_SEPARATOR) === 0;
195
    }
196
197
    /**
198
     * Adds informational links to the given PHP type/class.
199
     *
200
     * @param string $code type/class name to be linkified.
201
     * @param string|null $title custom title to use
202
     *
203
     * @return string linkified with HTML type/class name.
204
     */
205
    private function addTypeLinks(string $code, string $title = null): 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...
206
    {
207
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
208
            [, $class, $method] = $matches;
209
            $text = $title ? $this->htmlEncode($title) : $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
210
        } else {
211
            $class = $code;
212
            $method = null;
213
            $text = $title ? $this->htmlEncode($title) : $this->htmlEncode($class);
214
        }
215
        $url = null;
216
        $shouldGenerateLink = true;
217
        if ($method !== null && substr_compare($method, '{closure}', -9) !== 0) {
218
            try {
219
                $reflection = new \ReflectionClass($class);
220
                if ($reflection->hasMethod($method)) {
221
                    $reflectionMethod = $reflection->getMethod($method);
222
                    $shouldGenerateLink = $reflectionMethod->isPublic() || $reflectionMethod->isProtected();
223
                } else {
224
                    $shouldGenerateLink = false;
225
                }
226
            } catch (\Throwable $e) {
227
                $shouldGenerateLink = false;
228
            }
229
        }
230
        if ($shouldGenerateLink) {
231
            $url = $this->getTypeUrl($class, $method);
232
        }
233
        if ($url === null) {
234
            return $text;
235
        }
236
        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
237
    }
238
239
    /**
240
     * Returns the informational link URL for a given PHP type/class.
241
     *
242
     * @param string|null $class the type or class name.
243
     * @param string|null $method the method name.
244
     *
245
     * @return string|null the informational link URL.
246
     *
247
     * @see addTypeLinks()
248
     */
249
    private function getTypeUrl(?string $class, ?string $method): ?string
250
    {
251
        if (strncmp($class, 'Yiisoft\\', 8) !== 0) {
252
            return null;
253
        }
254
        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
255
        $url = "http://www.yiiframework.com/doc-3.0/$page.html";
256
        if ($method) {
257
            $url .= "#$method()-detail";
258
        }
259
        return $url;
260
    }
261
262
    /**
263
     * Converts arguments array to its string representation.
264
     *
265
     * @param array $args arguments array to be converted
266
     *
267
     * @return string string representation of the arguments array
268
     */
269
    public function argumentsToString(array $args): string
270
    {
271
        $count = 0;
272
        $isAssoc = $args !== array_values($args);
273
        foreach ($args as $key => $value) {
274
            $count++;
275
            if ($count >= 5) {
276
                if ($count > 5) {
277
                    unset($args[$key]);
278
                } else {
279
                    $args[$key] = '...';
280
                }
281
                continue;
282
            }
283
            if (is_object($value)) {
284
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
285
            } elseif (is_bool($value)) {
286
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
287
            } elseif (is_string($value)) {
288
                $fullValue = $this->htmlEncode($value);
289
                if (mb_strlen($value, 'UTF-8') > 32) {
290
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
291
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
292
                } else {
293
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
294
                }
295
            } elseif (is_array($value)) {
296
                unset($args[$key]);
297
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
298
            } elseif ($value === null) {
299
                $args[$key] = '<span class="keyword">null</span>';
300
            } elseif (is_resource($value)) {
301
                $args[$key] = '<span class="keyword">resource</span>';
302
            } else {
303
                $args[$key] = '<span class="number">' . $value . '</span>';
304
            }
305
            if (is_string($key)) {
306
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
307
            } elseif ($isAssoc) {
308
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
309
            }
310
        }
311
312
        ksort($args);
313
        return implode(', ', $args);
314
    }
315
316
    /**
317
     * Renders the information about request.
318
     *
319
     * @return string the rendering result
320
     */
321
    public function renderRequest(): string
322
    {
323
        if ($this->request === null) {
324
            return '';
325
        }
326
327
        $request = $this->request;
328
        $output = $request->getMethod() . ' ' . $request->getUri() . "\n";
329
330
        foreach ($request->getHeaders() as $name => $values) {
331
            if ($name === 'Host') {
332
                continue;
333
            }
334
335
            foreach ($values as $value) {
336
                $output .= "$name: $value\n";
337
            }
338
        }
339
340
        $output .= "\n" . $request->getBody() . "\n\n";
341
342
        return '<pre>' . $this->htmlEncode(rtrim($output, "\n")) . '</pre>';
343
    }
344
345
    public function renderCurl(): string
346
    {
347
        try {
348
            $output = (new Command())->setRequest($this->request)->build();
349
        } catch (\Throwable $e) {
350
            $output = 'Error generating curl command: ' . $e->getMessage();
351
        }
352
353
        return $this->htmlEncode($output);
354
    }
355
356
    /**
357
     * Creates string containing HTML link which refers to the home page of determined web-server software
358
     * and its full name.
359
     *
360
     * @return string server software information hyperlink.
361
     */
362
    public function createServerInformationLink(): string
363
    {
364
        if ($this->request === null) {
365
            return '';
366
        }
367
368
369
        $serverSoftware = $this->request->getServerParams()['SERVER_SOFTWARE'] ?? null;
370
        if ($serverSoftware === null) {
371
            return '';
372
        }
373
374
        $serverUrls = [
375
            'http://httpd.apache.org/' => ['apache'],
376
            'http://nginx.org/' => ['nginx'],
377
            'http://lighttpd.net/' => ['lighttpd'],
378
            'http://gwan.com/' => ['g-wan', 'gwan'],
379
            'http://iis.net/' => ['iis', 'services'],
380
            'https://secure.php.net/manual/en/features.commandline.webserver.php' => ['development'],
381
        ];
382
383
        foreach ($serverUrls as $url => $keywords) {
384
            foreach ($keywords as $keyword) {
385
                if (stripos($serverSoftware, $keyword) !== false) {
386
                    return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($serverSoftware) . '</a>';
387
                }
388
            }
389
        }
390
        return '';
391
    }
392
393
    /**
394
     * Creates string containing HTML link which refers to the page with the current version
395
     * of the framework and version number text.
396
     *
397
     * @return string framework version information hyperlink.
398
     */
399
    public function createFrameworkVersionLink(): string
400
    {
401
        return '<a href="http://github.com/yiisoft/app/" target="_blank">' . $this->htmlEncode(Info::frameworkVersion()) . '</a>';
402
    }
403
}
404