@@ -16,7 +16,7 @@ |
||
16 | 16 | <?php |
17 | 17 | |
18 | 18 | if (Debugger::$productionMode) { |
19 | - echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>'; |
|
19 | + echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>'; |
|
20 | 20 | } |
21 | 21 | |
22 | 22 | require __DIR__ . '/assets/E_COMPILE_ERROR.php'; |
@@ -6,39 +6,39 @@ |
||
6 | 6 | */ |
7 | 7 | |
8 | 8 | if (!function_exists('dump')) { |
9 | - /** |
|
10 | - * Tracy\Debugger::dump() shortcut. |
|
11 | - * @tracySkipLocation |
|
12 | - */ |
|
13 | - function dump($var) |
|
14 | - { |
|
15 | - array_map('Tracy\Debugger::dump', func_get_args()); |
|
16 | - return $var; |
|
17 | - } |
|
9 | + /** |
|
10 | + * Tracy\Debugger::dump() shortcut. |
|
11 | + * @tracySkipLocation |
|
12 | + */ |
|
13 | + function dump($var) |
|
14 | + { |
|
15 | + array_map('Tracy\Debugger::dump', func_get_args()); |
|
16 | + return $var; |
|
17 | + } |
|
18 | 18 | } |
19 | 19 | |
20 | 20 | if (!function_exists('dumpe')) { |
21 | - /** |
|
22 | - * Tracy\Debugger::dump() & exit shortcut. |
|
23 | - * @tracySkipLocation |
|
24 | - */ |
|
25 | - function dumpe($var) |
|
26 | - { |
|
27 | - array_map('Tracy\Debugger::dump', func_get_args()); |
|
28 | - if (!Tracy\Debugger::$productionMode) { |
|
29 | - exit; |
|
30 | - } |
|
31 | - } |
|
21 | + /** |
|
22 | + * Tracy\Debugger::dump() & exit shortcut. |
|
23 | + * @tracySkipLocation |
|
24 | + */ |
|
25 | + function dumpe($var) |
|
26 | + { |
|
27 | + array_map('Tracy\Debugger::dump', func_get_args()); |
|
28 | + if (!Tracy\Debugger::$productionMode) { |
|
29 | + exit; |
|
30 | + } |
|
31 | + } |
|
32 | 32 | } |
33 | 33 | |
34 | 34 | if (!function_exists('bdump')) { |
35 | - /** |
|
36 | - * Tracy\Debugger::barDump() shortcut. |
|
37 | - * @tracySkipLocation |
|
38 | - */ |
|
39 | - function bdump($var) |
|
40 | - { |
|
41 | - call_user_func_array('Tracy\Debugger::barDump', func_get_args()); |
|
42 | - return $var; |
|
43 | - } |
|
35 | + /** |
|
36 | + * Tracy\Debugger::barDump() shortcut. |
|
37 | + * @tracySkipLocation |
|
38 | + */ |
|
39 | + function bdump($var) |
|
40 | + { |
|
41 | + call_user_func_array('Tracy\Debugger::barDump', func_get_args()); |
|
42 | + return $var; |
|
43 | + } |
|
44 | 44 | } |
@@ -14,308 +14,308 @@ |
||
14 | 14 | class Helpers |
15 | 15 | { |
16 | 16 | |
17 | - /** |
|
18 | - * Returns HTML link to editor. |
|
19 | - * @return string |
|
20 | - */ |
|
21 | - public static function editorLink($file, $line = null) |
|
22 | - { |
|
23 | - $file = strtr($origFile = $file, Debugger::$editorMapping); |
|
24 | - if ($editor = self::editorUri($origFile, $line)) { |
|
25 | - $file = strtr($file, '\\', '/'); |
|
26 | - if (preg_match('#(^[a-z]:)?/.{1,50}$#i', $file, $m) && strlen($file) > strlen($m[0])) { |
|
27 | - $file = '...' . $m[0]; |
|
28 | - } |
|
29 | - $file = strtr($file, '/', DIRECTORY_SEPARATOR); |
|
30 | - return self::formatHtml('<a href="%" title="%">%<b>%</b>%</a>', |
|
31 | - $editor, |
|
32 | - $file . ($line ? ":$line" : ''), |
|
33 | - rtrim(dirname($file), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, |
|
34 | - basename($file), |
|
35 | - $line ? ":$line" : '' |
|
36 | - ); |
|
37 | - } else { |
|
38 | - return self::formatHtml('<span>%</span>', $file . ($line ? ":$line" : '')); |
|
39 | - } |
|
40 | - } |
|
41 | - |
|
42 | - |
|
43 | - /** |
|
44 | - * Returns link to editor. |
|
45 | - * @return string|null |
|
46 | - */ |
|
47 | - public static function editorUri($file, $line = null, $action = 'open', $search = null, $replace = null) |
|
48 | - { |
|
49 | - if (Debugger::$editor && $file && ($action === 'create' || is_file($file))) { |
|
50 | - $file = strtr($file, '/', DIRECTORY_SEPARATOR); |
|
51 | - $file = strtr($file, Debugger::$editorMapping); |
|
52 | - return strtr(Debugger::$editor, [ |
|
53 | - '%action' => $action, |
|
54 | - '%file' => rawurlencode($file), |
|
55 | - '%line' => $line ? (int) $line : 1, |
|
56 | - '%search' => rawurlencode($search), |
|
57 | - '%replace' => rawurlencode($replace), |
|
58 | - ]); |
|
59 | - } |
|
60 | - } |
|
61 | - |
|
62 | - |
|
63 | - public static function formatHtml($mask) |
|
64 | - { |
|
65 | - $args = func_get_args(); |
|
66 | - return preg_replace_callback('#%#', function () use (&$args, &$count) { |
|
67 | - return self::escapeHtml($args[++$count]); |
|
68 | - }, $mask); |
|
69 | - } |
|
70 | - |
|
71 | - |
|
72 | - public static function escapeHtml($s) |
|
73 | - { |
|
74 | - return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); |
|
75 | - } |
|
76 | - |
|
77 | - |
|
78 | - public static function findTrace(array $trace, $method, &$index = null) |
|
79 | - { |
|
80 | - $m = explode('::', $method); |
|
81 | - foreach ($trace as $i => $item) { |
|
82 | - if ( |
|
83 | - isset($item['function']) |
|
84 | - && $item['function'] === end($m) |
|
85 | - && isset($item['class']) === isset($m[1]) |
|
86 | - && (!isset($item['class']) || $m[0] === '*' || is_a($item['class'], $m[0], true)) |
|
87 | - ) { |
|
88 | - $index = $i; |
|
89 | - return $item; |
|
90 | - } |
|
91 | - } |
|
92 | - } |
|
93 | - |
|
94 | - |
|
95 | - /** |
|
96 | - * @return string |
|
97 | - */ |
|
98 | - public static function getClass($obj) |
|
99 | - { |
|
100 | - return explode("\x00", get_class($obj))[0]; |
|
101 | - } |
|
102 | - |
|
103 | - |
|
104 | - /** @internal */ |
|
105 | - public static function fixStack($exception) |
|
106 | - { |
|
107 | - if (function_exists('xdebug_get_function_stack')) { |
|
108 | - $stack = []; |
|
109 | - foreach (array_slice(array_reverse(xdebug_get_function_stack()), 2, -1) as $row) { |
|
110 | - $frame = [ |
|
111 | - 'file' => $row['file'], |
|
112 | - 'line' => $row['line'], |
|
113 | - 'function' => isset($row['function']) ? $row['function'] : '*unknown*', |
|
114 | - 'args' => [], |
|
115 | - ]; |
|
116 | - if (!empty($row['class'])) { |
|
117 | - $frame['type'] = isset($row['type']) && $row['type'] === 'dynamic' ? '->' : '::'; |
|
118 | - $frame['class'] = $row['class']; |
|
119 | - } |
|
120 | - $stack[] = $frame; |
|
121 | - } |
|
122 | - $ref = new \ReflectionProperty('Exception', 'trace'); |
|
123 | - $ref->setAccessible(true); |
|
124 | - $ref->setValue($exception, $stack); |
|
125 | - } |
|
126 | - return $exception; |
|
127 | - } |
|
128 | - |
|
129 | - |
|
130 | - /** @internal */ |
|
131 | - public static function fixEncoding($s) |
|
132 | - { |
|
133 | - return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES); |
|
134 | - } |
|
135 | - |
|
136 | - |
|
137 | - /** @internal */ |
|
138 | - public static function errorTypeToString($type) |
|
139 | - { |
|
140 | - $types = [ |
|
141 | - E_ERROR => 'Fatal Error', |
|
142 | - E_USER_ERROR => 'User Error', |
|
143 | - E_RECOVERABLE_ERROR => 'Recoverable Error', |
|
144 | - E_CORE_ERROR => 'Core Error', |
|
145 | - E_COMPILE_ERROR => 'Compile Error', |
|
146 | - E_PARSE => 'Parse Error', |
|
147 | - E_WARNING => 'Warning', |
|
148 | - E_CORE_WARNING => 'Core Warning', |
|
149 | - E_COMPILE_WARNING => 'Compile Warning', |
|
150 | - E_USER_WARNING => 'User Warning', |
|
151 | - E_NOTICE => 'Notice', |
|
152 | - E_USER_NOTICE => 'User Notice', |
|
153 | - E_STRICT => 'Strict standards', |
|
154 | - E_DEPRECATED => 'Deprecated', |
|
155 | - E_USER_DEPRECATED => 'User Deprecated', |
|
156 | - ]; |
|
157 | - return isset($types[$type]) ? $types[$type] : 'Unknown error'; |
|
158 | - } |
|
159 | - |
|
160 | - |
|
161 | - /** @internal */ |
|
162 | - public static function getSource() |
|
163 | - { |
|
164 | - if (isset($_SERVER['REQUEST_URI'])) { |
|
165 | - return (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://') |
|
166 | - . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') |
|
167 | - . $_SERVER['REQUEST_URI']; |
|
168 | - } else { |
|
169 | - return 'CLI (PID: ' . getmypid() . ')' |
|
170 | - . (empty($_SERVER['argv']) ? '' : ': ' . implode(' ', $_SERVER['argv'])); |
|
171 | - } |
|
172 | - } |
|
173 | - |
|
174 | - |
|
175 | - /** @internal */ |
|
176 | - public static function improveException($e) |
|
177 | - { |
|
178 | - $message = $e->getMessage(); |
|
179 | - |
|
180 | - if ($e instanceof \Nette\MemberAccessException && ($trace = $e->getTrace()) && isset($trace[1]['file'], $trace[1]['line'])) { |
|
181 | - if (preg_match('# property ([\w\\\\]+)::\$(\w+), did you mean \$(\w+)#', $message, $m)) { |
|
182 | - $replace = ["->$m[2]", "->$m[3]"]; |
|
183 | - } elseif (preg_match('# method ([\w\\\\]+)::(\w+)\(\), did you mean (\w+)\(#', $message, $m)) { |
|
184 | - $replace = ["$m[2](", "$m[3]("]; |
|
185 | - } else { |
|
186 | - return; |
|
187 | - } |
|
188 | - $e->tracyAction = [ |
|
189 | - 'link' => self::editorUri($trace[1]['file'], $trace[1]['line'], 'fix', $replace[0], $replace[1]), |
|
190 | - 'label' => 'fix it', |
|
191 | - ]; |
|
192 | - |
|
193 | - } elseif (!$e instanceof \Error && !$e instanceof \ErrorException) { |
|
194 | - // do nothing |
|
195 | - } elseif (preg_match('#^Call to undefined function (\S+\\\\)?(\w+)\(#', $message, $m)) { |
|
196 | - $funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']); |
|
197 | - $hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2]); |
|
198 | - $message = "Call to undefined function $m[2](), did you mean $hint()?"; |
|
199 | - $replace = ["$m[2](", "$hint("]; |
|
200 | - |
|
201 | - } elseif (preg_match('#^Call to undefined method ([\w\\\\]+)::(\w+)#', $message, $m)) { |
|
202 | - $hint = self::getSuggestion(get_class_methods($m[1]), $m[2]); |
|
203 | - $message .= ", did you mean $hint()?"; |
|
204 | - $replace = ["$m[2](", "$hint("]; |
|
205 | - |
|
206 | - } elseif (preg_match('#^Undefined variable: (\w+)#', $message, $m) && !empty($e->context)) { |
|
207 | - $hint = self::getSuggestion(array_keys($e->context), $m[1]); |
|
208 | - $message = "Undefined variable $$m[1], did you mean $$hint?"; |
|
209 | - $replace = ["$$m[1]", "$$hint"]; |
|
210 | - |
|
211 | - } elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) { |
|
212 | - $rc = new \ReflectionClass($m[1]); |
|
213 | - $items = array_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC)); |
|
214 | - $hint = self::getSuggestion($items, $m[2]); |
|
215 | - $message .= ", did you mean $$hint?"; |
|
216 | - $replace = ["->$m[2]", "->$hint"]; |
|
217 | - |
|
218 | - } elseif (preg_match('#^Access to undeclared static property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) { |
|
219 | - $rc = new \ReflectionClass($m[1]); |
|
220 | - $items = array_intersect($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC)); |
|
221 | - $hint = self::getSuggestion($items, $m[2]); |
|
222 | - $message .= ", did you mean $$hint?"; |
|
223 | - $replace = ["::$$m[2]", "::$$hint"]; |
|
224 | - } |
|
225 | - |
|
226 | - if (isset($hint)) { |
|
227 | - $ref = new \ReflectionProperty($e, 'message'); |
|
228 | - $ref->setAccessible(true); |
|
229 | - $ref->setValue($e, $message); |
|
230 | - $e->tracyAction = [ |
|
231 | - 'link' => self::editorUri($e->getFile(), $e->getLine(), 'fix', $replace[0], $replace[1]), |
|
232 | - 'label' => 'fix it', |
|
233 | - ]; |
|
234 | - } |
|
235 | - } |
|
236 | - |
|
237 | - |
|
238 | - /** @internal */ |
|
239 | - public static function improveError($message, array $context = []) |
|
240 | - { |
|
241 | - if (preg_match('#^Undefined variable: (\w+)#', $message, $m) && $context) { |
|
242 | - $hint = self::getSuggestion(array_keys($context), $m[1]); |
|
243 | - return $hint ? "Undefined variable $$m[1], did you mean $$hint?" : $message; |
|
244 | - |
|
245 | - } elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) { |
|
246 | - $rc = new \ReflectionClass($m[1]); |
|
247 | - $items = array_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC)); |
|
248 | - $hint = self::getSuggestion($items, $m[2]); |
|
249 | - return $hint ? $message . ", did you mean $$hint?" : $message; |
|
250 | - } |
|
251 | - return $message; |
|
252 | - } |
|
253 | - |
|
254 | - |
|
255 | - /** @internal */ |
|
256 | - public static function guessClassFile($class) |
|
257 | - { |
|
258 | - $segments = explode(DIRECTORY_SEPARATOR, $class); |
|
259 | - $res = null; |
|
260 | - $max = 0; |
|
261 | - foreach (get_declared_classes() as $class) { |
|
262 | - $parts = explode(DIRECTORY_SEPARATOR, $class); |
|
263 | - foreach ($parts as $i => $part) { |
|
264 | - if (!isset($segments[$i]) || $part !== $segments[$i]) { |
|
265 | - break; |
|
266 | - } |
|
267 | - } |
|
268 | - if ($i > $max && ($file = (new \ReflectionClass($class))->getFileName())) { |
|
269 | - $max = $i; |
|
270 | - $res = array_merge(array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, $i - count($parts)), array_slice($segments, $i)); |
|
271 | - $res = implode(DIRECTORY_SEPARATOR, $res) . '.php'; |
|
272 | - } |
|
273 | - } |
|
274 | - return $res; |
|
275 | - } |
|
276 | - |
|
277 | - |
|
278 | - /** |
|
279 | - * Finds the best suggestion. |
|
280 | - * @return string|null |
|
281 | - * @internal |
|
282 | - */ |
|
283 | - public static function getSuggestion(array $items, $value) |
|
284 | - { |
|
285 | - $best = null; |
|
286 | - $min = (strlen($value) / 4 + 1) * 10 + .1; |
|
287 | - foreach (array_unique($items, SORT_REGULAR) as $item) { |
|
288 | - $item = is_object($item) ? $item->getName() : $item; |
|
289 | - if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) { |
|
290 | - $min = $len; |
|
291 | - $best = $item; |
|
292 | - } |
|
293 | - } |
|
294 | - return $best; |
|
295 | - } |
|
296 | - |
|
297 | - |
|
298 | - /** @internal */ |
|
299 | - public static function isHtmlMode() |
|
300 | - { |
|
301 | - return empty($_SERVER['HTTP_X_REQUESTED_WITH']) && empty($_SERVER['HTTP_X_TRACY_AJAX']) |
|
302 | - && PHP_SAPI !== 'cli' |
|
303 | - && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list())); |
|
304 | - } |
|
305 | - |
|
306 | - |
|
307 | - /** @internal */ |
|
308 | - public static function isAjax() |
|
309 | - { |
|
310 | - return isset($_SERVER['HTTP_X_TRACY_AJAX']) && preg_match('#^\w{10}\z#', $_SERVER['HTTP_X_TRACY_AJAX']); |
|
311 | - } |
|
312 | - |
|
313 | - |
|
314 | - /** @internal */ |
|
315 | - public static function getNonce() |
|
316 | - { |
|
317 | - return preg_match('#^Content-Security-Policy(?:-Report-Only)?:.*\sscript-src\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi', implode("\n", headers_list()), $m) |
|
318 | - ? $m[1] |
|
319 | - : null; |
|
320 | - } |
|
17 | + /** |
|
18 | + * Returns HTML link to editor. |
|
19 | + * @return string |
|
20 | + */ |
|
21 | + public static function editorLink($file, $line = null) |
|
22 | + { |
|
23 | + $file = strtr($origFile = $file, Debugger::$editorMapping); |
|
24 | + if ($editor = self::editorUri($origFile, $line)) { |
|
25 | + $file = strtr($file, '\\', '/'); |
|
26 | + if (preg_match('#(^[a-z]:)?/.{1,50}$#i', $file, $m) && strlen($file) > strlen($m[0])) { |
|
27 | + $file = '...' . $m[0]; |
|
28 | + } |
|
29 | + $file = strtr($file, '/', DIRECTORY_SEPARATOR); |
|
30 | + return self::formatHtml('<a href="%" title="%">%<b>%</b>%</a>', |
|
31 | + $editor, |
|
32 | + $file . ($line ? ":$line" : ''), |
|
33 | + rtrim(dirname($file), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, |
|
34 | + basename($file), |
|
35 | + $line ? ":$line" : '' |
|
36 | + ); |
|
37 | + } else { |
|
38 | + return self::formatHtml('<span>%</span>', $file . ($line ? ":$line" : '')); |
|
39 | + } |
|
40 | + } |
|
41 | + |
|
42 | + |
|
43 | + /** |
|
44 | + * Returns link to editor. |
|
45 | + * @return string|null |
|
46 | + */ |
|
47 | + public static function editorUri($file, $line = null, $action = 'open', $search = null, $replace = null) |
|
48 | + { |
|
49 | + if (Debugger::$editor && $file && ($action === 'create' || is_file($file))) { |
|
50 | + $file = strtr($file, '/', DIRECTORY_SEPARATOR); |
|
51 | + $file = strtr($file, Debugger::$editorMapping); |
|
52 | + return strtr(Debugger::$editor, [ |
|
53 | + '%action' => $action, |
|
54 | + '%file' => rawurlencode($file), |
|
55 | + '%line' => $line ? (int) $line : 1, |
|
56 | + '%search' => rawurlencode($search), |
|
57 | + '%replace' => rawurlencode($replace), |
|
58 | + ]); |
|
59 | + } |
|
60 | + } |
|
61 | + |
|
62 | + |
|
63 | + public static function formatHtml($mask) |
|
64 | + { |
|
65 | + $args = func_get_args(); |
|
66 | + return preg_replace_callback('#%#', function () use (&$args, &$count) { |
|
67 | + return self::escapeHtml($args[++$count]); |
|
68 | + }, $mask); |
|
69 | + } |
|
70 | + |
|
71 | + |
|
72 | + public static function escapeHtml($s) |
|
73 | + { |
|
74 | + return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); |
|
75 | + } |
|
76 | + |
|
77 | + |
|
78 | + public static function findTrace(array $trace, $method, &$index = null) |
|
79 | + { |
|
80 | + $m = explode('::', $method); |
|
81 | + foreach ($trace as $i => $item) { |
|
82 | + if ( |
|
83 | + isset($item['function']) |
|
84 | + && $item['function'] === end($m) |
|
85 | + && isset($item['class']) === isset($m[1]) |
|
86 | + && (!isset($item['class']) || $m[0] === '*' || is_a($item['class'], $m[0], true)) |
|
87 | + ) { |
|
88 | + $index = $i; |
|
89 | + return $item; |
|
90 | + } |
|
91 | + } |
|
92 | + } |
|
93 | + |
|
94 | + |
|
95 | + /** |
|
96 | + * @return string |
|
97 | + */ |
|
98 | + public static function getClass($obj) |
|
99 | + { |
|
100 | + return explode("\x00", get_class($obj))[0]; |
|
101 | + } |
|
102 | + |
|
103 | + |
|
104 | + /** @internal */ |
|
105 | + public static function fixStack($exception) |
|
106 | + { |
|
107 | + if (function_exists('xdebug_get_function_stack')) { |
|
108 | + $stack = []; |
|
109 | + foreach (array_slice(array_reverse(xdebug_get_function_stack()), 2, -1) as $row) { |
|
110 | + $frame = [ |
|
111 | + 'file' => $row['file'], |
|
112 | + 'line' => $row['line'], |
|
113 | + 'function' => isset($row['function']) ? $row['function'] : '*unknown*', |
|
114 | + 'args' => [], |
|
115 | + ]; |
|
116 | + if (!empty($row['class'])) { |
|
117 | + $frame['type'] = isset($row['type']) && $row['type'] === 'dynamic' ? '->' : '::'; |
|
118 | + $frame['class'] = $row['class']; |
|
119 | + } |
|
120 | + $stack[] = $frame; |
|
121 | + } |
|
122 | + $ref = new \ReflectionProperty('Exception', 'trace'); |
|
123 | + $ref->setAccessible(true); |
|
124 | + $ref->setValue($exception, $stack); |
|
125 | + } |
|
126 | + return $exception; |
|
127 | + } |
|
128 | + |
|
129 | + |
|
130 | + /** @internal */ |
|
131 | + public static function fixEncoding($s) |
|
132 | + { |
|
133 | + return htmlspecialchars_decode(htmlspecialchars($s, ENT_NOQUOTES | ENT_IGNORE, 'UTF-8'), ENT_NOQUOTES); |
|
134 | + } |
|
135 | + |
|
136 | + |
|
137 | + /** @internal */ |
|
138 | + public static function errorTypeToString($type) |
|
139 | + { |
|
140 | + $types = [ |
|
141 | + E_ERROR => 'Fatal Error', |
|
142 | + E_USER_ERROR => 'User Error', |
|
143 | + E_RECOVERABLE_ERROR => 'Recoverable Error', |
|
144 | + E_CORE_ERROR => 'Core Error', |
|
145 | + E_COMPILE_ERROR => 'Compile Error', |
|
146 | + E_PARSE => 'Parse Error', |
|
147 | + E_WARNING => 'Warning', |
|
148 | + E_CORE_WARNING => 'Core Warning', |
|
149 | + E_COMPILE_WARNING => 'Compile Warning', |
|
150 | + E_USER_WARNING => 'User Warning', |
|
151 | + E_NOTICE => 'Notice', |
|
152 | + E_USER_NOTICE => 'User Notice', |
|
153 | + E_STRICT => 'Strict standards', |
|
154 | + E_DEPRECATED => 'Deprecated', |
|
155 | + E_USER_DEPRECATED => 'User Deprecated', |
|
156 | + ]; |
|
157 | + return isset($types[$type]) ? $types[$type] : 'Unknown error'; |
|
158 | + } |
|
159 | + |
|
160 | + |
|
161 | + /** @internal */ |
|
162 | + public static function getSource() |
|
163 | + { |
|
164 | + if (isset($_SERVER['REQUEST_URI'])) { |
|
165 | + return (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://') |
|
166 | + . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') |
|
167 | + . $_SERVER['REQUEST_URI']; |
|
168 | + } else { |
|
169 | + return 'CLI (PID: ' . getmypid() . ')' |
|
170 | + . (empty($_SERVER['argv']) ? '' : ': ' . implode(' ', $_SERVER['argv'])); |
|
171 | + } |
|
172 | + } |
|
173 | + |
|
174 | + |
|
175 | + /** @internal */ |
|
176 | + public static function improveException($e) |
|
177 | + { |
|
178 | + $message = $e->getMessage(); |
|
179 | + |
|
180 | + if ($e instanceof \Nette\MemberAccessException && ($trace = $e->getTrace()) && isset($trace[1]['file'], $trace[1]['line'])) { |
|
181 | + if (preg_match('# property ([\w\\\\]+)::\$(\w+), did you mean \$(\w+)#', $message, $m)) { |
|
182 | + $replace = ["->$m[2]", "->$m[3]"]; |
|
183 | + } elseif (preg_match('# method ([\w\\\\]+)::(\w+)\(\), did you mean (\w+)\(#', $message, $m)) { |
|
184 | + $replace = ["$m[2](", "$m[3]("]; |
|
185 | + } else { |
|
186 | + return; |
|
187 | + } |
|
188 | + $e->tracyAction = [ |
|
189 | + 'link' => self::editorUri($trace[1]['file'], $trace[1]['line'], 'fix', $replace[0], $replace[1]), |
|
190 | + 'label' => 'fix it', |
|
191 | + ]; |
|
192 | + |
|
193 | + } elseif (!$e instanceof \Error && !$e instanceof \ErrorException) { |
|
194 | + // do nothing |
|
195 | + } elseif (preg_match('#^Call to undefined function (\S+\\\\)?(\w+)\(#', $message, $m)) { |
|
196 | + $funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']); |
|
197 | + $hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2]); |
|
198 | + $message = "Call to undefined function $m[2](), did you mean $hint()?"; |
|
199 | + $replace = ["$m[2](", "$hint("]; |
|
200 | + |
|
201 | + } elseif (preg_match('#^Call to undefined method ([\w\\\\]+)::(\w+)#', $message, $m)) { |
|
202 | + $hint = self::getSuggestion(get_class_methods($m[1]), $m[2]); |
|
203 | + $message .= ", did you mean $hint()?"; |
|
204 | + $replace = ["$m[2](", "$hint("]; |
|
205 | + |
|
206 | + } elseif (preg_match('#^Undefined variable: (\w+)#', $message, $m) && !empty($e->context)) { |
|
207 | + $hint = self::getSuggestion(array_keys($e->context), $m[1]); |
|
208 | + $message = "Undefined variable $$m[1], did you mean $$hint?"; |
|
209 | + $replace = ["$$m[1]", "$$hint"]; |
|
210 | + |
|
211 | + } elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) { |
|
212 | + $rc = new \ReflectionClass($m[1]); |
|
213 | + $items = array_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC)); |
|
214 | + $hint = self::getSuggestion($items, $m[2]); |
|
215 | + $message .= ", did you mean $$hint?"; |
|
216 | + $replace = ["->$m[2]", "->$hint"]; |
|
217 | + |
|
218 | + } elseif (preg_match('#^Access to undeclared static property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) { |
|
219 | + $rc = new \ReflectionClass($m[1]); |
|
220 | + $items = array_intersect($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC)); |
|
221 | + $hint = self::getSuggestion($items, $m[2]); |
|
222 | + $message .= ", did you mean $$hint?"; |
|
223 | + $replace = ["::$$m[2]", "::$$hint"]; |
|
224 | + } |
|
225 | + |
|
226 | + if (isset($hint)) { |
|
227 | + $ref = new \ReflectionProperty($e, 'message'); |
|
228 | + $ref->setAccessible(true); |
|
229 | + $ref->setValue($e, $message); |
|
230 | + $e->tracyAction = [ |
|
231 | + 'link' => self::editorUri($e->getFile(), $e->getLine(), 'fix', $replace[0], $replace[1]), |
|
232 | + 'label' => 'fix it', |
|
233 | + ]; |
|
234 | + } |
|
235 | + } |
|
236 | + |
|
237 | + |
|
238 | + /** @internal */ |
|
239 | + public static function improveError($message, array $context = []) |
|
240 | + { |
|
241 | + if (preg_match('#^Undefined variable: (\w+)#', $message, $m) && $context) { |
|
242 | + $hint = self::getSuggestion(array_keys($context), $m[1]); |
|
243 | + return $hint ? "Undefined variable $$m[1], did you mean $$hint?" : $message; |
|
244 | + |
|
245 | + } elseif (preg_match('#^Undefined property: ([\w\\\\]+)::\$(\w+)#', $message, $m)) { |
|
246 | + $rc = new \ReflectionClass($m[1]); |
|
247 | + $items = array_diff($rc->getProperties(\ReflectionProperty::IS_PUBLIC), $rc->getProperties(\ReflectionProperty::IS_STATIC)); |
|
248 | + $hint = self::getSuggestion($items, $m[2]); |
|
249 | + return $hint ? $message . ", did you mean $$hint?" : $message; |
|
250 | + } |
|
251 | + return $message; |
|
252 | + } |
|
253 | + |
|
254 | + |
|
255 | + /** @internal */ |
|
256 | + public static function guessClassFile($class) |
|
257 | + { |
|
258 | + $segments = explode(DIRECTORY_SEPARATOR, $class); |
|
259 | + $res = null; |
|
260 | + $max = 0; |
|
261 | + foreach (get_declared_classes() as $class) { |
|
262 | + $parts = explode(DIRECTORY_SEPARATOR, $class); |
|
263 | + foreach ($parts as $i => $part) { |
|
264 | + if (!isset($segments[$i]) || $part !== $segments[$i]) { |
|
265 | + break; |
|
266 | + } |
|
267 | + } |
|
268 | + if ($i > $max && ($file = (new \ReflectionClass($class))->getFileName())) { |
|
269 | + $max = $i; |
|
270 | + $res = array_merge(array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, $i - count($parts)), array_slice($segments, $i)); |
|
271 | + $res = implode(DIRECTORY_SEPARATOR, $res) . '.php'; |
|
272 | + } |
|
273 | + } |
|
274 | + return $res; |
|
275 | + } |
|
276 | + |
|
277 | + |
|
278 | + /** |
|
279 | + * Finds the best suggestion. |
|
280 | + * @return string|null |
|
281 | + * @internal |
|
282 | + */ |
|
283 | + public static function getSuggestion(array $items, $value) |
|
284 | + { |
|
285 | + $best = null; |
|
286 | + $min = (strlen($value) / 4 + 1) * 10 + .1; |
|
287 | + foreach (array_unique($items, SORT_REGULAR) as $item) { |
|
288 | + $item = is_object($item) ? $item->getName() : $item; |
|
289 | + if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) { |
|
290 | + $min = $len; |
|
291 | + $best = $item; |
|
292 | + } |
|
293 | + } |
|
294 | + return $best; |
|
295 | + } |
|
296 | + |
|
297 | + |
|
298 | + /** @internal */ |
|
299 | + public static function isHtmlMode() |
|
300 | + { |
|
301 | + return empty($_SERVER['HTTP_X_REQUESTED_WITH']) && empty($_SERVER['HTTP_X_TRACY_AJAX']) |
|
302 | + && PHP_SAPI !== 'cli' |
|
303 | + && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list())); |
|
304 | + } |
|
305 | + |
|
306 | + |
|
307 | + /** @internal */ |
|
308 | + public static function isAjax() |
|
309 | + { |
|
310 | + return isset($_SERVER['HTTP_X_TRACY_AJAX']) && preg_match('#^\w{10}\z#', $_SERVER['HTTP_X_TRACY_AJAX']); |
|
311 | + } |
|
312 | + |
|
313 | + |
|
314 | + /** @internal */ |
|
315 | + public static function getNonce() |
|
316 | + { |
|
317 | + return preg_match('#^Content-Security-Policy(?:-Report-Only)?:.*\sscript-src\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi', implode("\n", headers_list()), $m) |
|
318 | + ? $m[1] |
|
319 | + : null; |
|
320 | + } |
|
321 | 321 | } |
@@ -13,583 +13,583 @@ |
||
13 | 13 | */ |
14 | 14 | class Dumper |
15 | 15 | { |
16 | - const |
|
17 | - DEPTH = 'depth', // how many nested levels of array/object properties display (defaults to 4) |
|
18 | - TRUNCATE = 'truncate', // how truncate long strings? (defaults to 150) |
|
19 | - COLLAPSE = 'collapse', // collapse top array/object or how big are collapsed? (defaults to 14) |
|
20 | - COLLAPSE_COUNT = 'collapsecount', // how big array/object are collapsed? (defaults to 7) |
|
21 | - LOCATION = 'location', // show location string? (defaults to 0) |
|
22 | - OBJECT_EXPORTERS = 'exporters', // custom exporters for objects (defaults to Dumper::$objectexporters) |
|
23 | - LIVE = 'live', // will be rendered using JavaScript |
|
24 | - DEBUGINFO = 'debuginfo', // use magic method __debugInfo if exists (defaults to false) |
|
25 | - KEYS_TO_HIDE = 'keystohide'; // sensitive keys not displayed (defaults to []) |
|
26 | - |
|
27 | - const |
|
28 | - LOCATION_SOURCE = 0b0001, // shows where dump was called |
|
29 | - LOCATION_LINK = 0b0010, // appends clickable anchor |
|
30 | - LOCATION_CLASS = 0b0100; // shows where class is defined |
|
31 | - |
|
32 | - const |
|
33 | - HIDDEN_VALUE = '*****'; |
|
34 | - |
|
35 | - /** @var array */ |
|
36 | - public static $terminalColors = [ |
|
37 | - 'bool' => '1;33', |
|
38 | - 'null' => '1;33', |
|
39 | - 'number' => '1;32', |
|
40 | - 'string' => '1;36', |
|
41 | - 'array' => '1;31', |
|
42 | - 'key' => '1;37', |
|
43 | - 'object' => '1;31', |
|
44 | - 'visibility' => '1;30', |
|
45 | - 'resource' => '1;37', |
|
46 | - 'indent' => '1;30', |
|
47 | - ]; |
|
48 | - |
|
49 | - /** @var array */ |
|
50 | - public static $resources = [ |
|
51 | - 'stream' => 'stream_get_meta_data', |
|
52 | - 'stream-context' => 'stream_context_get_options', |
|
53 | - 'curl' => 'curl_getinfo', |
|
54 | - ]; |
|
55 | - |
|
56 | - /** @var array */ |
|
57 | - public static $objectExporters = [ |
|
58 | - 'Closure' => 'Tracy\Dumper::exportClosure', |
|
59 | - 'SplFileInfo' => 'Tracy\Dumper::exportSplFileInfo', |
|
60 | - 'SplObjectStorage' => 'Tracy\Dumper::exportSplObjectStorage', |
|
61 | - '__PHP_Incomplete_Class' => 'Tracy\Dumper::exportPhpIncompleteClass', |
|
62 | - ]; |
|
63 | - |
|
64 | - /** @var string @internal */ |
|
65 | - public static $livePrefix; |
|
66 | - |
|
67 | - /** @var array */ |
|
68 | - private static $liveStorage = []; |
|
69 | - |
|
70 | - |
|
71 | - /** |
|
72 | - * Dumps variable to the output. |
|
73 | - * @return mixed variable |
|
74 | - */ |
|
75 | - public static function dump($var, array $options = null) |
|
76 | - { |
|
77 | - if (PHP_SAPI !== 'cli' && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))) { |
|
78 | - echo self::toHtml($var, $options); |
|
79 | - } elseif (self::detectColors()) { |
|
80 | - echo self::toTerminal($var, $options); |
|
81 | - } else { |
|
82 | - echo self::toText($var, $options); |
|
83 | - } |
|
84 | - return $var; |
|
85 | - } |
|
86 | - |
|
87 | - |
|
88 | - /** |
|
89 | - * Dumps variable to HTML. |
|
90 | - * @return string |
|
91 | - */ |
|
92 | - public static function toHtml($var, array $options = null) |
|
93 | - { |
|
94 | - $options = (array) $options + [ |
|
95 | - self::DEPTH => 4, |
|
96 | - self::TRUNCATE => 150, |
|
97 | - self::COLLAPSE => 14, |
|
98 | - self::COLLAPSE_COUNT => 7, |
|
99 | - self::OBJECT_EXPORTERS => null, |
|
100 | - self::DEBUGINFO => false, |
|
101 | - self::KEYS_TO_HIDE => [], |
|
102 | - ]; |
|
103 | - $loc = &$options[self::LOCATION]; |
|
104 | - $loc = $loc === true ? ~0 : (int) $loc; |
|
105 | - |
|
106 | - $options[self::KEYS_TO_HIDE] = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE])); |
|
107 | - $options[self::OBJECT_EXPORTERS] = (array) $options[self::OBJECT_EXPORTERS] + self::$objectExporters; |
|
108 | - uksort($options[self::OBJECT_EXPORTERS], function ($a, $b) { |
|
109 | - return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1; |
|
110 | - }); |
|
111 | - |
|
112 | - $live = !empty($options[self::LIVE]) && $var && (is_array($var) || is_object($var) || is_resource($var)); |
|
113 | - list($file, $line, $code) = $loc ? self::findLocation() : null; |
|
114 | - $locAttrs = $file && $loc & self::LOCATION_SOURCE ? Helpers::formatHtml( |
|
115 | - ' title="%in file % on line %" data-tracy-href="%"', "$code\n", $file, $line, Helpers::editorUri($file, $line) |
|
116 | - ) : null; |
|
117 | - |
|
118 | - return '<pre class="tracy-dump' . ($live && $options[self::COLLAPSE] === true ? ' tracy-collapsed' : '') . '"' |
|
119 | - . $locAttrs |
|
120 | - . ($live ? " data-tracy-dump='" . json_encode(self::toJson($var, $options), JSON_HEX_APOS | JSON_HEX_AMP) . "'>" : '>') |
|
121 | - . ($live ? '' : self::dumpVar($var, $options)) |
|
122 | - . ($file && $loc & self::LOCATION_LINK ? '<small>in ' . Helpers::editorLink($file, $line) . '</small>' : '') |
|
123 | - . "</pre>\n"; |
|
124 | - } |
|
125 | - |
|
126 | - |
|
127 | - /** |
|
128 | - * Dumps variable to plain text. |
|
129 | - * @return string |
|
130 | - */ |
|
131 | - public static function toText($var, array $options = null) |
|
132 | - { |
|
133 | - return htmlspecialchars_decode(strip_tags(self::toHtml($var, $options)), ENT_QUOTES); |
|
134 | - } |
|
135 | - |
|
136 | - |
|
137 | - /** |
|
138 | - * Dumps variable to x-terminal. |
|
139 | - * @return string |
|
140 | - */ |
|
141 | - public static function toTerminal($var, array $options = null) |
|
142 | - { |
|
143 | - return htmlspecialchars_decode(strip_tags(preg_replace_callback('#<span class="tracy-dump-(\w+)">|</span>#', function ($m) { |
|
144 | - return "\033[" . (isset($m[1], self::$terminalColors[$m[1]]) ? self::$terminalColors[$m[1]] : '0') . 'm'; |
|
145 | - }, self::toHtml($var, $options))), ENT_QUOTES); |
|
146 | - } |
|
147 | - |
|
148 | - |
|
149 | - /** |
|
150 | - * Internal toHtml() dump implementation. |
|
151 | - * @param mixed $var |
|
152 | - * @param array $options |
|
153 | - * @param int $level recursion level |
|
154 | - * @return string |
|
155 | - */ |
|
156 | - private static function dumpVar(&$var, array $options, $level = 0) |
|
157 | - { |
|
158 | - if (method_exists(__CLASS__, $m = 'dump' . gettype($var))) { |
|
159 | - return self::$m($var, $options, $level); |
|
160 | - } else { |
|
161 | - return "<span>unknown type</span>\n"; |
|
162 | - } |
|
163 | - } |
|
164 | - |
|
165 | - |
|
166 | - private static function dumpNull() |
|
167 | - { |
|
168 | - return "<span class=\"tracy-dump-null\">null</span>\n"; |
|
169 | - } |
|
170 | - |
|
171 | - |
|
172 | - private static function dumpBoolean(&$var) |
|
173 | - { |
|
174 | - return '<span class="tracy-dump-bool">' . ($var ? 'true' : 'false') . "</span>\n"; |
|
175 | - } |
|
176 | - |
|
177 | - |
|
178 | - private static function dumpInteger(&$var) |
|
179 | - { |
|
180 | - return "<span class=\"tracy-dump-number\">$var</span>\n"; |
|
181 | - } |
|
182 | - |
|
183 | - |
|
184 | - private static function dumpDouble(&$var) |
|
185 | - { |
|
186 | - $var = is_finite($var) |
|
187 | - ? ($tmp = json_encode($var)) . (strpos($tmp, '.') === false ? '.0' : '') |
|
188 | - : str_replace('.0', '', var_export($var, true)); // workaround for PHP 7.0.2 |
|
189 | - return "<span class=\"tracy-dump-number\">$var</span>\n"; |
|
190 | - } |
|
191 | - |
|
192 | - |
|
193 | - private static function dumpString(&$var, $options) |
|
194 | - { |
|
195 | - return '<span class="tracy-dump-string">"' |
|
196 | - . Helpers::escapeHtml(self::encodeString($var, $options[self::TRUNCATE])) |
|
197 | - . '"</span>' . (strlen($var) > 1 ? ' (' . strlen($var) . ')' : '') . "\n"; |
|
198 | - } |
|
199 | - |
|
200 | - |
|
201 | - private static function dumpArray(&$var, $options, $level) |
|
202 | - { |
|
203 | - static $marker; |
|
204 | - if ($marker === null) { |
|
205 | - $marker = uniqid("\x00", true); |
|
206 | - } |
|
207 | - |
|
208 | - $out = '<span class="tracy-dump-array">array</span> ('; |
|
209 | - |
|
210 | - if (empty($var)) { |
|
211 | - return $out . ")\n"; |
|
212 | - |
|
213 | - } elseif (isset($var[$marker])) { |
|
214 | - return $out . (count($var) - 1) . ") [ <i>RECURSION</i> ]\n"; |
|
215 | - |
|
216 | - } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH]) { |
|
217 | - $collapsed = $level ? count($var) >= $options[self::COLLAPSE_COUNT] |
|
218 | - : (is_int($options[self::COLLAPSE]) ? count($var) >= $options[self::COLLAPSE] : $options[self::COLLAPSE]); |
|
219 | - $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">' |
|
220 | - . $out . count($var) . ")</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>'; |
|
221 | - $var[$marker] = true; |
|
222 | - foreach ($var as $k => &$v) { |
|
223 | - if ($k !== $marker) { |
|
224 | - $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]) ? self::HIDDEN_VALUE : null; |
|
225 | - $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . Helpers::escapeHtml(self::encodeString($k, $options[self::TRUNCATE])) . '"'; |
|
226 | - $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' |
|
227 | - . '<span class="tracy-dump-key">' . $k . '</span> => ' |
|
228 | - . ($hide ? self::dumpString($hide, $options) : self::dumpVar($v, $options, $level + 1)); |
|
229 | - } |
|
230 | - } |
|
231 | - unset($var[$marker]); |
|
232 | - return $out . '</div>'; |
|
233 | - |
|
234 | - } else { |
|
235 | - return $out . count($var) . ") [ ... ]\n"; |
|
236 | - } |
|
237 | - } |
|
238 | - |
|
239 | - |
|
240 | - private static function dumpObject(&$var, $options, $level) |
|
241 | - { |
|
242 | - $fields = self::exportObject($var, $options[self::OBJECT_EXPORTERS], $options[self::DEBUGINFO]); |
|
243 | - |
|
244 | - $editorAttributes = ''; |
|
245 | - if ($options[self::LOCATION] & self::LOCATION_CLASS) { |
|
246 | - $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var); |
|
247 | - $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()); |
|
248 | - if ($editor) { |
|
249 | - $editorAttributes = Helpers::formatHtml( |
|
250 | - ' title="Declared in file % on line %" data-tracy-href="%"', |
|
251 | - $rc->getFileName(), |
|
252 | - $rc->getStartLine(), |
|
253 | - $editor |
|
254 | - ); |
|
255 | - } |
|
256 | - } |
|
257 | - $out = '<span class="tracy-dump-object"' . $editorAttributes . '>' |
|
258 | - . Helpers::escapeHtml(Helpers::getClass($var)) |
|
259 | - . '</span> <span class="tracy-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>'; |
|
260 | - |
|
261 | - static $list = []; |
|
262 | - |
|
263 | - if (empty($fields)) { |
|
264 | - return $out . "\n"; |
|
265 | - |
|
266 | - } elseif (in_array($var, $list, true)) { |
|
267 | - return $out . " { <i>RECURSION</i> }\n"; |
|
268 | - |
|
269 | - } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH] || $var instanceof \Closure) { |
|
270 | - $collapsed = $level ? count($fields) >= $options[self::COLLAPSE_COUNT] |
|
271 | - : (is_int($options[self::COLLAPSE]) ? count($fields) >= $options[self::COLLAPSE] : $options[self::COLLAPSE]); |
|
272 | - $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">' |
|
273 | - . $out . "</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>'; |
|
274 | - $list[] = $var; |
|
275 | - foreach ($fields as $k => &$v) { |
|
276 | - $vis = ''; |
|
277 | - if (isset($k[0]) && $k[0] === "\x00") { |
|
278 | - $vis = ' <span class="tracy-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>'; |
|
279 | - $k = substr($k, strrpos($k, "\x00") + 1); |
|
280 | - } |
|
281 | - $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]) ? self::HIDDEN_VALUE : null; |
|
282 | - $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . Helpers::escapeHtml(self::encodeString($k, $options[self::TRUNCATE])) . '"'; |
|
283 | - $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' |
|
284 | - . '<span class="tracy-dump-key">' . $k . "</span>$vis => " |
|
285 | - . ($hide ? self::dumpString($hide, $options) : self::dumpVar($v, $options, $level + 1)); |
|
286 | - } |
|
287 | - array_pop($list); |
|
288 | - return $out . '</div>'; |
|
289 | - |
|
290 | - } else { |
|
291 | - return $out . " { ... }\n"; |
|
292 | - } |
|
293 | - } |
|
294 | - |
|
295 | - |
|
296 | - private static function dumpResource(&$var, $options, $level) |
|
297 | - { |
|
298 | - $type = get_resource_type($var); |
|
299 | - $out = '<span class="tracy-dump-resource">' . Helpers::escapeHtml($type) . ' resource</span> ' |
|
300 | - . '<span class="tracy-dump-hash">#' . (int) $var . '</span>'; |
|
301 | - if (isset(self::$resources[$type])) { |
|
302 | - $out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">"; |
|
303 | - foreach (call_user_func(self::$resources[$type], $var) as $k => $v) { |
|
304 | - $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' |
|
305 | - . '<span class="tracy-dump-key">' . Helpers::escapeHtml($k) . '</span> => ' . self::dumpVar($v, $options, $level + 1); |
|
306 | - } |
|
307 | - return $out . '</div>'; |
|
308 | - } |
|
309 | - return "$out\n"; |
|
310 | - } |
|
311 | - |
|
312 | - |
|
313 | - /** |
|
314 | - * @return mixed |
|
315 | - */ |
|
316 | - private static function toJson(&$var, $options, $level = 0) |
|
317 | - { |
|
318 | - if (is_bool($var) || $var === null || is_int($var)) { |
|
319 | - return $var; |
|
320 | - |
|
321 | - } elseif (is_float($var)) { |
|
322 | - return is_finite($var) |
|
323 | - ? (strpos($tmp = json_encode($var), '.') ? $var : ['number' => "$tmp.0"]) |
|
324 | - : ['type' => (string) $var]; |
|
325 | - |
|
326 | - } elseif (is_string($var)) { |
|
327 | - return self::encodeString($var, $options[self::TRUNCATE]); |
|
328 | - |
|
329 | - } elseif (is_array($var)) { |
|
330 | - static $marker; |
|
331 | - if ($marker === null) { |
|
332 | - $marker = uniqid("\x00", true); |
|
333 | - } |
|
334 | - if (isset($var[$marker]) || $level >= $options[self::DEPTH]) { |
|
335 | - return [null]; |
|
336 | - } |
|
337 | - $res = []; |
|
338 | - $var[$marker] = true; |
|
339 | - foreach ($var as $k => &$v) { |
|
340 | - if ($k !== $marker) { |
|
341 | - $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]); |
|
342 | - $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"'; |
|
343 | - $res[] = [$k, $hide ? self::HIDDEN_VALUE : self::toJson($v, $options, $level + 1)]; |
|
344 | - } |
|
345 | - } |
|
346 | - unset($var[$marker]); |
|
347 | - return $res; |
|
348 | - |
|
349 | - } elseif (is_object($var)) { |
|
350 | - $obj = &self::$liveStorage[spl_object_hash($var)]; |
|
351 | - if ($obj && $obj['level'] <= $level) { |
|
352 | - return ['object' => $obj['id']]; |
|
353 | - } |
|
354 | - |
|
355 | - $editorInfo = null; |
|
356 | - if ($options[self::LOCATION] & self::LOCATION_CLASS) { |
|
357 | - $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var); |
|
358 | - $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()); |
|
359 | - $editorInfo = $editor ? ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor] : null; |
|
360 | - } |
|
361 | - static $counter = 1; |
|
362 | - $obj = $obj ?: [ |
|
363 | - 'id' => self::$livePrefix . '0' . $counter++, // differentiate from resources |
|
364 | - 'name' => Helpers::getClass($var), |
|
365 | - 'editor' => $editorInfo, |
|
366 | - 'level' => $level, |
|
367 | - 'object' => $var, |
|
368 | - ]; |
|
369 | - |
|
370 | - if ($level < $options[self::DEPTH] || !$options[self::DEPTH]) { |
|
371 | - $obj['level'] = $level; |
|
372 | - $obj['items'] = []; |
|
373 | - |
|
374 | - foreach (self::exportObject($var, $options[self::OBJECT_EXPORTERS], $options[self::DEBUGINFO]) as $k => $v) { |
|
375 | - $vis = 0; |
|
376 | - if (isset($k[0]) && $k[0] === "\x00") { |
|
377 | - $vis = $k[1] === '*' ? 1 : 2; |
|
378 | - $k = substr($k, strrpos($k, "\x00") + 1); |
|
379 | - } |
|
380 | - $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]); |
|
381 | - $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"'; |
|
382 | - $obj['items'][] = [$k, $hide ? self::HIDDEN_VALUE : self::toJson($v, $options, $level + 1), $vis]; |
|
383 | - } |
|
384 | - } |
|
385 | - return ['object' => $obj['id']]; |
|
386 | - |
|
387 | - } elseif (is_resource($var)) { |
|
388 | - $obj = &self::$liveStorage[(string) $var]; |
|
389 | - if (!$obj) { |
|
390 | - $type = get_resource_type($var); |
|
391 | - $obj = ['id' => self::$livePrefix . (int) $var, 'name' => $type . ' resource']; |
|
392 | - if (isset(self::$resources[$type])) { |
|
393 | - foreach (call_user_func(self::$resources[$type], $var) as $k => $v) { |
|
394 | - $obj['items'][] = [$k, self::toJson($v, $options, $level + 1)]; |
|
395 | - } |
|
396 | - } |
|
397 | - } |
|
398 | - return ['resource' => $obj['id']]; |
|
399 | - |
|
400 | - } else { |
|
401 | - return ['type' => 'unknown type']; |
|
402 | - } |
|
403 | - } |
|
404 | - |
|
405 | - |
|
406 | - /** @return array */ |
|
407 | - public static function fetchLiveData() |
|
408 | - { |
|
409 | - $res = []; |
|
410 | - foreach (self::$liveStorage as $obj) { |
|
411 | - $id = $obj['id']; |
|
412 | - unset($obj['level'], $obj['object'], $obj['id']); |
|
413 | - $res[$id] = $obj; |
|
414 | - } |
|
415 | - self::$liveStorage = []; |
|
416 | - return $res; |
|
417 | - } |
|
418 | - |
|
419 | - |
|
420 | - /** |
|
421 | - * @internal |
|
422 | - * @return string UTF-8 |
|
423 | - */ |
|
424 | - public static function encodeString($s, $maxLength = null) |
|
425 | - { |
|
426 | - static $table; |
|
427 | - if ($table === null) { |
|
428 | - foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) { |
|
429 | - $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT); |
|
430 | - } |
|
431 | - $table['\\'] = '\\\\'; |
|
432 | - $table["\r"] = '\r'; |
|
433 | - $table["\n"] = '\n'; |
|
434 | - $table["\t"] = '\t'; |
|
435 | - } |
|
436 | - |
|
437 | - if ($maxLength && strlen($s) > $maxLength) { // shortens to $maxLength in UTF-8 or longer |
|
438 | - if (function_exists('mb_substr')) { |
|
439 | - $s = mb_substr($tmp = $s, 0, $maxLength, 'UTF-8'); |
|
440 | - $shortened = $s !== $tmp; |
|
441 | - } else { |
|
442 | - $i = $len = 0; |
|
443 | - $maxI = $maxLength * 4; // max UTF-8 length |
|
444 | - do { |
|
445 | - if (($s[$i] < "\x80" || $s[$i] >= "\xC0") && (++$len > $maxLength) || $i >= $maxI) { |
|
446 | - $s = substr($s, 0, $i); |
|
447 | - $shortened = true; |
|
448 | - break; |
|
449 | - } |
|
450 | - } while (isset($s[++$i])); |
|
451 | - } |
|
452 | - } |
|
453 | - |
|
454 | - if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) { // is binary? |
|
455 | - if ($maxLength && strlen($s) > $maxLength) { |
|
456 | - $s = substr($s, 0, $maxLength); |
|
457 | - $shortened = true; |
|
458 | - } |
|
459 | - $s = strtr($s, $table); |
|
460 | - } |
|
461 | - |
|
462 | - return $s . (empty($shortened) ? '' : ' ... '); |
|
463 | - } |
|
464 | - |
|
465 | - |
|
466 | - /** |
|
467 | - * @return array |
|
468 | - */ |
|
469 | - private static function exportObject($obj, array $exporters, $useDebugInfo) |
|
470 | - { |
|
471 | - foreach ($exporters as $type => $dumper) { |
|
472 | - if (!$type || $obj instanceof $type) { |
|
473 | - return call_user_func($dumper, $obj); |
|
474 | - } |
|
475 | - } |
|
476 | - |
|
477 | - if ($useDebugInfo && method_exists($obj, '__debugInfo')) { |
|
478 | - return $obj->__debugInfo(); |
|
479 | - } |
|
480 | - |
|
481 | - return (array) $obj; |
|
482 | - } |
|
483 | - |
|
484 | - |
|
485 | - /** |
|
486 | - * @return array |
|
487 | - */ |
|
488 | - private static function exportClosure(\Closure $obj) |
|
489 | - { |
|
490 | - $rc = new \ReflectionFunction($obj); |
|
491 | - $res = []; |
|
492 | - foreach ($rc->getParameters() as $param) { |
|
493 | - $res[] = '$' . $param->getName(); |
|
494 | - } |
|
495 | - return [ |
|
496 | - 'file' => $rc->getFileName(), |
|
497 | - 'line' => $rc->getStartLine(), |
|
498 | - 'variables' => $rc->getStaticVariables(), |
|
499 | - 'parameters' => implode(', ', $res), |
|
500 | - ]; |
|
501 | - } |
|
502 | - |
|
503 | - |
|
504 | - /** |
|
505 | - * @return array |
|
506 | - */ |
|
507 | - private static function exportSplFileInfo(\SplFileInfo $obj) |
|
508 | - { |
|
509 | - return ['path' => $obj->getPathname()]; |
|
510 | - } |
|
511 | - |
|
512 | - |
|
513 | - /** |
|
514 | - * @return array |
|
515 | - */ |
|
516 | - private static function exportSplObjectStorage(\SplObjectStorage $obj) |
|
517 | - { |
|
518 | - $res = []; |
|
519 | - foreach (clone $obj as $item) { |
|
520 | - $res[] = ['object' => $item, 'data' => $obj[$item]]; |
|
521 | - } |
|
522 | - return $res; |
|
523 | - } |
|
524 | - |
|
525 | - |
|
526 | - /** |
|
527 | - * @return array |
|
528 | - */ |
|
529 | - private static function exportPhpIncompleteClass(\__PHP_Incomplete_Class $obj) |
|
530 | - { |
|
531 | - $info = ['className' => null, 'private' => [], 'protected' => [], 'public' => []]; |
|
532 | - foreach ((array) $obj as $name => $value) { |
|
533 | - if ($name === '__PHP_Incomplete_Class_Name') { |
|
534 | - $info['className'] = $value; |
|
535 | - } elseif (preg_match('#^\x0\*\x0(.+)\z#', $name, $m)) { |
|
536 | - $info['protected'][$m[1]] = $value; |
|
537 | - } elseif (preg_match('#^\x0(.+)\x0(.+)\z#', $name, $m)) { |
|
538 | - $info['private'][$m[1] . '::$' . $m[2]] = $value; |
|
539 | - } else { |
|
540 | - $info['public'][$name] = $value; |
|
541 | - } |
|
542 | - } |
|
543 | - return $info; |
|
544 | - } |
|
545 | - |
|
546 | - |
|
547 | - /** |
|
548 | - * Finds the location where dump was called. |
|
549 | - * @return array|null [file, line, code] |
|
550 | - */ |
|
551 | - private static function findLocation() |
|
552 | - { |
|
553 | - foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) { |
|
554 | - if (isset($item['class']) && $item['class'] === __CLASS__) { |
|
555 | - $location = $item; |
|
556 | - continue; |
|
557 | - } elseif (isset($item['function'])) { |
|
558 | - try { |
|
559 | - $reflection = isset($item['class']) |
|
560 | - ? new \ReflectionMethod($item['class'], $item['function']) |
|
561 | - : new \ReflectionFunction($item['function']); |
|
562 | - if ($reflection->isInternal() || preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())) { |
|
563 | - $location = $item; |
|
564 | - continue; |
|
565 | - } |
|
566 | - } catch (\ReflectionException $e) { |
|
567 | - } |
|
568 | - } |
|
569 | - break; |
|
570 | - } |
|
571 | - |
|
572 | - if (isset($location['file'], $location['line']) && is_file($location['file'])) { |
|
573 | - $lines = file($location['file']); |
|
574 | - $line = $lines[$location['line'] - 1]; |
|
575 | - return [ |
|
576 | - $location['file'], |
|
577 | - $location['line'], |
|
578 | - trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line), |
|
579 | - ]; |
|
580 | - } |
|
581 | - } |
|
582 | - |
|
583 | - |
|
584 | - /** |
|
585 | - * @return bool |
|
586 | - */ |
|
587 | - private static function detectColors() |
|
588 | - { |
|
589 | - return self::$terminalColors && |
|
590 | - (getenv('ConEmuANSI') === 'ON' |
|
591 | - || getenv('ANSICON') !== false |
|
592 | - || getenv('term') === 'xterm-256color' |
|
593 | - || (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT))); |
|
594 | - } |
|
16 | + const |
|
17 | + DEPTH = 'depth', // how many nested levels of array/object properties display (defaults to 4) |
|
18 | + TRUNCATE = 'truncate', // how truncate long strings? (defaults to 150) |
|
19 | + COLLAPSE = 'collapse', // collapse top array/object or how big are collapsed? (defaults to 14) |
|
20 | + COLLAPSE_COUNT = 'collapsecount', // how big array/object are collapsed? (defaults to 7) |
|
21 | + LOCATION = 'location', // show location string? (defaults to 0) |
|
22 | + OBJECT_EXPORTERS = 'exporters', // custom exporters for objects (defaults to Dumper::$objectexporters) |
|
23 | + LIVE = 'live', // will be rendered using JavaScript |
|
24 | + DEBUGINFO = 'debuginfo', // use magic method __debugInfo if exists (defaults to false) |
|
25 | + KEYS_TO_HIDE = 'keystohide'; // sensitive keys not displayed (defaults to []) |
|
26 | + |
|
27 | + const |
|
28 | + LOCATION_SOURCE = 0b0001, // shows where dump was called |
|
29 | + LOCATION_LINK = 0b0010, // appends clickable anchor |
|
30 | + LOCATION_CLASS = 0b0100; // shows where class is defined |
|
31 | + |
|
32 | + const |
|
33 | + HIDDEN_VALUE = '*****'; |
|
34 | + |
|
35 | + /** @var array */ |
|
36 | + public static $terminalColors = [ |
|
37 | + 'bool' => '1;33', |
|
38 | + 'null' => '1;33', |
|
39 | + 'number' => '1;32', |
|
40 | + 'string' => '1;36', |
|
41 | + 'array' => '1;31', |
|
42 | + 'key' => '1;37', |
|
43 | + 'object' => '1;31', |
|
44 | + 'visibility' => '1;30', |
|
45 | + 'resource' => '1;37', |
|
46 | + 'indent' => '1;30', |
|
47 | + ]; |
|
48 | + |
|
49 | + /** @var array */ |
|
50 | + public static $resources = [ |
|
51 | + 'stream' => 'stream_get_meta_data', |
|
52 | + 'stream-context' => 'stream_context_get_options', |
|
53 | + 'curl' => 'curl_getinfo', |
|
54 | + ]; |
|
55 | + |
|
56 | + /** @var array */ |
|
57 | + public static $objectExporters = [ |
|
58 | + 'Closure' => 'Tracy\Dumper::exportClosure', |
|
59 | + 'SplFileInfo' => 'Tracy\Dumper::exportSplFileInfo', |
|
60 | + 'SplObjectStorage' => 'Tracy\Dumper::exportSplObjectStorage', |
|
61 | + '__PHP_Incomplete_Class' => 'Tracy\Dumper::exportPhpIncompleteClass', |
|
62 | + ]; |
|
63 | + |
|
64 | + /** @var string @internal */ |
|
65 | + public static $livePrefix; |
|
66 | + |
|
67 | + /** @var array */ |
|
68 | + private static $liveStorage = []; |
|
69 | + |
|
70 | + |
|
71 | + /** |
|
72 | + * Dumps variable to the output. |
|
73 | + * @return mixed variable |
|
74 | + */ |
|
75 | + public static function dump($var, array $options = null) |
|
76 | + { |
|
77 | + if (PHP_SAPI !== 'cli' && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))) { |
|
78 | + echo self::toHtml($var, $options); |
|
79 | + } elseif (self::detectColors()) { |
|
80 | + echo self::toTerminal($var, $options); |
|
81 | + } else { |
|
82 | + echo self::toText($var, $options); |
|
83 | + } |
|
84 | + return $var; |
|
85 | + } |
|
86 | + |
|
87 | + |
|
88 | + /** |
|
89 | + * Dumps variable to HTML. |
|
90 | + * @return string |
|
91 | + */ |
|
92 | + public static function toHtml($var, array $options = null) |
|
93 | + { |
|
94 | + $options = (array) $options + [ |
|
95 | + self::DEPTH => 4, |
|
96 | + self::TRUNCATE => 150, |
|
97 | + self::COLLAPSE => 14, |
|
98 | + self::COLLAPSE_COUNT => 7, |
|
99 | + self::OBJECT_EXPORTERS => null, |
|
100 | + self::DEBUGINFO => false, |
|
101 | + self::KEYS_TO_HIDE => [], |
|
102 | + ]; |
|
103 | + $loc = &$options[self::LOCATION]; |
|
104 | + $loc = $loc === true ? ~0 : (int) $loc; |
|
105 | + |
|
106 | + $options[self::KEYS_TO_HIDE] = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE])); |
|
107 | + $options[self::OBJECT_EXPORTERS] = (array) $options[self::OBJECT_EXPORTERS] + self::$objectExporters; |
|
108 | + uksort($options[self::OBJECT_EXPORTERS], function ($a, $b) { |
|
109 | + return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1; |
|
110 | + }); |
|
111 | + |
|
112 | + $live = !empty($options[self::LIVE]) && $var && (is_array($var) || is_object($var) || is_resource($var)); |
|
113 | + list($file, $line, $code) = $loc ? self::findLocation() : null; |
|
114 | + $locAttrs = $file && $loc & self::LOCATION_SOURCE ? Helpers::formatHtml( |
|
115 | + ' title="%in file % on line %" data-tracy-href="%"', "$code\n", $file, $line, Helpers::editorUri($file, $line) |
|
116 | + ) : null; |
|
117 | + |
|
118 | + return '<pre class="tracy-dump' . ($live && $options[self::COLLAPSE] === true ? ' tracy-collapsed' : '') . '"' |
|
119 | + . $locAttrs |
|
120 | + . ($live ? " data-tracy-dump='" . json_encode(self::toJson($var, $options), JSON_HEX_APOS | JSON_HEX_AMP) . "'>" : '>') |
|
121 | + . ($live ? '' : self::dumpVar($var, $options)) |
|
122 | + . ($file && $loc & self::LOCATION_LINK ? '<small>in ' . Helpers::editorLink($file, $line) . '</small>' : '') |
|
123 | + . "</pre>\n"; |
|
124 | + } |
|
125 | + |
|
126 | + |
|
127 | + /** |
|
128 | + * Dumps variable to plain text. |
|
129 | + * @return string |
|
130 | + */ |
|
131 | + public static function toText($var, array $options = null) |
|
132 | + { |
|
133 | + return htmlspecialchars_decode(strip_tags(self::toHtml($var, $options)), ENT_QUOTES); |
|
134 | + } |
|
135 | + |
|
136 | + |
|
137 | + /** |
|
138 | + * Dumps variable to x-terminal. |
|
139 | + * @return string |
|
140 | + */ |
|
141 | + public static function toTerminal($var, array $options = null) |
|
142 | + { |
|
143 | + return htmlspecialchars_decode(strip_tags(preg_replace_callback('#<span class="tracy-dump-(\w+)">|</span>#', function ($m) { |
|
144 | + return "\033[" . (isset($m[1], self::$terminalColors[$m[1]]) ? self::$terminalColors[$m[1]] : '0') . 'm'; |
|
145 | + }, self::toHtml($var, $options))), ENT_QUOTES); |
|
146 | + } |
|
147 | + |
|
148 | + |
|
149 | + /** |
|
150 | + * Internal toHtml() dump implementation. |
|
151 | + * @param mixed $var |
|
152 | + * @param array $options |
|
153 | + * @param int $level recursion level |
|
154 | + * @return string |
|
155 | + */ |
|
156 | + private static function dumpVar(&$var, array $options, $level = 0) |
|
157 | + { |
|
158 | + if (method_exists(__CLASS__, $m = 'dump' . gettype($var))) { |
|
159 | + return self::$m($var, $options, $level); |
|
160 | + } else { |
|
161 | + return "<span>unknown type</span>\n"; |
|
162 | + } |
|
163 | + } |
|
164 | + |
|
165 | + |
|
166 | + private static function dumpNull() |
|
167 | + { |
|
168 | + return "<span class=\"tracy-dump-null\">null</span>\n"; |
|
169 | + } |
|
170 | + |
|
171 | + |
|
172 | + private static function dumpBoolean(&$var) |
|
173 | + { |
|
174 | + return '<span class="tracy-dump-bool">' . ($var ? 'true' : 'false') . "</span>\n"; |
|
175 | + } |
|
176 | + |
|
177 | + |
|
178 | + private static function dumpInteger(&$var) |
|
179 | + { |
|
180 | + return "<span class=\"tracy-dump-number\">$var</span>\n"; |
|
181 | + } |
|
182 | + |
|
183 | + |
|
184 | + private static function dumpDouble(&$var) |
|
185 | + { |
|
186 | + $var = is_finite($var) |
|
187 | + ? ($tmp = json_encode($var)) . (strpos($tmp, '.') === false ? '.0' : '') |
|
188 | + : str_replace('.0', '', var_export($var, true)); // workaround for PHP 7.0.2 |
|
189 | + return "<span class=\"tracy-dump-number\">$var</span>\n"; |
|
190 | + } |
|
191 | + |
|
192 | + |
|
193 | + private static function dumpString(&$var, $options) |
|
194 | + { |
|
195 | + return '<span class="tracy-dump-string">"' |
|
196 | + . Helpers::escapeHtml(self::encodeString($var, $options[self::TRUNCATE])) |
|
197 | + . '"</span>' . (strlen($var) > 1 ? ' (' . strlen($var) . ')' : '') . "\n"; |
|
198 | + } |
|
199 | + |
|
200 | + |
|
201 | + private static function dumpArray(&$var, $options, $level) |
|
202 | + { |
|
203 | + static $marker; |
|
204 | + if ($marker === null) { |
|
205 | + $marker = uniqid("\x00", true); |
|
206 | + } |
|
207 | + |
|
208 | + $out = '<span class="tracy-dump-array">array</span> ('; |
|
209 | + |
|
210 | + if (empty($var)) { |
|
211 | + return $out . ")\n"; |
|
212 | + |
|
213 | + } elseif (isset($var[$marker])) { |
|
214 | + return $out . (count($var) - 1) . ") [ <i>RECURSION</i> ]\n"; |
|
215 | + |
|
216 | + } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH]) { |
|
217 | + $collapsed = $level ? count($var) >= $options[self::COLLAPSE_COUNT] |
|
218 | + : (is_int($options[self::COLLAPSE]) ? count($var) >= $options[self::COLLAPSE] : $options[self::COLLAPSE]); |
|
219 | + $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">' |
|
220 | + . $out . count($var) . ")</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>'; |
|
221 | + $var[$marker] = true; |
|
222 | + foreach ($var as $k => &$v) { |
|
223 | + if ($k !== $marker) { |
|
224 | + $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]) ? self::HIDDEN_VALUE : null; |
|
225 | + $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . Helpers::escapeHtml(self::encodeString($k, $options[self::TRUNCATE])) . '"'; |
|
226 | + $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' |
|
227 | + . '<span class="tracy-dump-key">' . $k . '</span> => ' |
|
228 | + . ($hide ? self::dumpString($hide, $options) : self::dumpVar($v, $options, $level + 1)); |
|
229 | + } |
|
230 | + } |
|
231 | + unset($var[$marker]); |
|
232 | + return $out . '</div>'; |
|
233 | + |
|
234 | + } else { |
|
235 | + return $out . count($var) . ") [ ... ]\n"; |
|
236 | + } |
|
237 | + } |
|
238 | + |
|
239 | + |
|
240 | + private static function dumpObject(&$var, $options, $level) |
|
241 | + { |
|
242 | + $fields = self::exportObject($var, $options[self::OBJECT_EXPORTERS], $options[self::DEBUGINFO]); |
|
243 | + |
|
244 | + $editorAttributes = ''; |
|
245 | + if ($options[self::LOCATION] & self::LOCATION_CLASS) { |
|
246 | + $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var); |
|
247 | + $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()); |
|
248 | + if ($editor) { |
|
249 | + $editorAttributes = Helpers::formatHtml( |
|
250 | + ' title="Declared in file % on line %" data-tracy-href="%"', |
|
251 | + $rc->getFileName(), |
|
252 | + $rc->getStartLine(), |
|
253 | + $editor |
|
254 | + ); |
|
255 | + } |
|
256 | + } |
|
257 | + $out = '<span class="tracy-dump-object"' . $editorAttributes . '>' |
|
258 | + . Helpers::escapeHtml(Helpers::getClass($var)) |
|
259 | + . '</span> <span class="tracy-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>'; |
|
260 | + |
|
261 | + static $list = []; |
|
262 | + |
|
263 | + if (empty($fields)) { |
|
264 | + return $out . "\n"; |
|
265 | + |
|
266 | + } elseif (in_array($var, $list, true)) { |
|
267 | + return $out . " { <i>RECURSION</i> }\n"; |
|
268 | + |
|
269 | + } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH] || $var instanceof \Closure) { |
|
270 | + $collapsed = $level ? count($fields) >= $options[self::COLLAPSE_COUNT] |
|
271 | + : (is_int($options[self::COLLAPSE]) ? count($fields) >= $options[self::COLLAPSE] : $options[self::COLLAPSE]); |
|
272 | + $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">' |
|
273 | + . $out . "</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>'; |
|
274 | + $list[] = $var; |
|
275 | + foreach ($fields as $k => &$v) { |
|
276 | + $vis = ''; |
|
277 | + if (isset($k[0]) && $k[0] === "\x00") { |
|
278 | + $vis = ' <span class="tracy-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>'; |
|
279 | + $k = substr($k, strrpos($k, "\x00") + 1); |
|
280 | + } |
|
281 | + $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]) ? self::HIDDEN_VALUE : null; |
|
282 | + $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . Helpers::escapeHtml(self::encodeString($k, $options[self::TRUNCATE])) . '"'; |
|
283 | + $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' |
|
284 | + . '<span class="tracy-dump-key">' . $k . "</span>$vis => " |
|
285 | + . ($hide ? self::dumpString($hide, $options) : self::dumpVar($v, $options, $level + 1)); |
|
286 | + } |
|
287 | + array_pop($list); |
|
288 | + return $out . '</div>'; |
|
289 | + |
|
290 | + } else { |
|
291 | + return $out . " { ... }\n"; |
|
292 | + } |
|
293 | + } |
|
294 | + |
|
295 | + |
|
296 | + private static function dumpResource(&$var, $options, $level) |
|
297 | + { |
|
298 | + $type = get_resource_type($var); |
|
299 | + $out = '<span class="tracy-dump-resource">' . Helpers::escapeHtml($type) . ' resource</span> ' |
|
300 | + . '<span class="tracy-dump-hash">#' . (int) $var . '</span>'; |
|
301 | + if (isset(self::$resources[$type])) { |
|
302 | + $out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">"; |
|
303 | + foreach (call_user_func(self::$resources[$type], $var) as $k => $v) { |
|
304 | + $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' |
|
305 | + . '<span class="tracy-dump-key">' . Helpers::escapeHtml($k) . '</span> => ' . self::dumpVar($v, $options, $level + 1); |
|
306 | + } |
|
307 | + return $out . '</div>'; |
|
308 | + } |
|
309 | + return "$out\n"; |
|
310 | + } |
|
311 | + |
|
312 | + |
|
313 | + /** |
|
314 | + * @return mixed |
|
315 | + */ |
|
316 | + private static function toJson(&$var, $options, $level = 0) |
|
317 | + { |
|
318 | + if (is_bool($var) || $var === null || is_int($var)) { |
|
319 | + return $var; |
|
320 | + |
|
321 | + } elseif (is_float($var)) { |
|
322 | + return is_finite($var) |
|
323 | + ? (strpos($tmp = json_encode($var), '.') ? $var : ['number' => "$tmp.0"]) |
|
324 | + : ['type' => (string) $var]; |
|
325 | + |
|
326 | + } elseif (is_string($var)) { |
|
327 | + return self::encodeString($var, $options[self::TRUNCATE]); |
|
328 | + |
|
329 | + } elseif (is_array($var)) { |
|
330 | + static $marker; |
|
331 | + if ($marker === null) { |
|
332 | + $marker = uniqid("\x00", true); |
|
333 | + } |
|
334 | + if (isset($var[$marker]) || $level >= $options[self::DEPTH]) { |
|
335 | + return [null]; |
|
336 | + } |
|
337 | + $res = []; |
|
338 | + $var[$marker] = true; |
|
339 | + foreach ($var as $k => &$v) { |
|
340 | + if ($k !== $marker) { |
|
341 | + $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]); |
|
342 | + $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"'; |
|
343 | + $res[] = [$k, $hide ? self::HIDDEN_VALUE : self::toJson($v, $options, $level + 1)]; |
|
344 | + } |
|
345 | + } |
|
346 | + unset($var[$marker]); |
|
347 | + return $res; |
|
348 | + |
|
349 | + } elseif (is_object($var)) { |
|
350 | + $obj = &self::$liveStorage[spl_object_hash($var)]; |
|
351 | + if ($obj && $obj['level'] <= $level) { |
|
352 | + return ['object' => $obj['id']]; |
|
353 | + } |
|
354 | + |
|
355 | + $editorInfo = null; |
|
356 | + if ($options[self::LOCATION] & self::LOCATION_CLASS) { |
|
357 | + $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var); |
|
358 | + $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()); |
|
359 | + $editorInfo = $editor ? ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor] : null; |
|
360 | + } |
|
361 | + static $counter = 1; |
|
362 | + $obj = $obj ?: [ |
|
363 | + 'id' => self::$livePrefix . '0' . $counter++, // differentiate from resources |
|
364 | + 'name' => Helpers::getClass($var), |
|
365 | + 'editor' => $editorInfo, |
|
366 | + 'level' => $level, |
|
367 | + 'object' => $var, |
|
368 | + ]; |
|
369 | + |
|
370 | + if ($level < $options[self::DEPTH] || !$options[self::DEPTH]) { |
|
371 | + $obj['level'] = $level; |
|
372 | + $obj['items'] = []; |
|
373 | + |
|
374 | + foreach (self::exportObject($var, $options[self::OBJECT_EXPORTERS], $options[self::DEBUGINFO]) as $k => $v) { |
|
375 | + $vis = 0; |
|
376 | + if (isset($k[0]) && $k[0] === "\x00") { |
|
377 | + $vis = $k[1] === '*' ? 1 : 2; |
|
378 | + $k = substr($k, strrpos($k, "\x00") + 1); |
|
379 | + } |
|
380 | + $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]); |
|
381 | + $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"'; |
|
382 | + $obj['items'][] = [$k, $hide ? self::HIDDEN_VALUE : self::toJson($v, $options, $level + 1), $vis]; |
|
383 | + } |
|
384 | + } |
|
385 | + return ['object' => $obj['id']]; |
|
386 | + |
|
387 | + } elseif (is_resource($var)) { |
|
388 | + $obj = &self::$liveStorage[(string) $var]; |
|
389 | + if (!$obj) { |
|
390 | + $type = get_resource_type($var); |
|
391 | + $obj = ['id' => self::$livePrefix . (int) $var, 'name' => $type . ' resource']; |
|
392 | + if (isset(self::$resources[$type])) { |
|
393 | + foreach (call_user_func(self::$resources[$type], $var) as $k => $v) { |
|
394 | + $obj['items'][] = [$k, self::toJson($v, $options, $level + 1)]; |
|
395 | + } |
|
396 | + } |
|
397 | + } |
|
398 | + return ['resource' => $obj['id']]; |
|
399 | + |
|
400 | + } else { |
|
401 | + return ['type' => 'unknown type']; |
|
402 | + } |
|
403 | + } |
|
404 | + |
|
405 | + |
|
406 | + /** @return array */ |
|
407 | + public static function fetchLiveData() |
|
408 | + { |
|
409 | + $res = []; |
|
410 | + foreach (self::$liveStorage as $obj) { |
|
411 | + $id = $obj['id']; |
|
412 | + unset($obj['level'], $obj['object'], $obj['id']); |
|
413 | + $res[$id] = $obj; |
|
414 | + } |
|
415 | + self::$liveStorage = []; |
|
416 | + return $res; |
|
417 | + } |
|
418 | + |
|
419 | + |
|
420 | + /** |
|
421 | + * @internal |
|
422 | + * @return string UTF-8 |
|
423 | + */ |
|
424 | + public static function encodeString($s, $maxLength = null) |
|
425 | + { |
|
426 | + static $table; |
|
427 | + if ($table === null) { |
|
428 | + foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) { |
|
429 | + $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT); |
|
430 | + } |
|
431 | + $table['\\'] = '\\\\'; |
|
432 | + $table["\r"] = '\r'; |
|
433 | + $table["\n"] = '\n'; |
|
434 | + $table["\t"] = '\t'; |
|
435 | + } |
|
436 | + |
|
437 | + if ($maxLength && strlen($s) > $maxLength) { // shortens to $maxLength in UTF-8 or longer |
|
438 | + if (function_exists('mb_substr')) { |
|
439 | + $s = mb_substr($tmp = $s, 0, $maxLength, 'UTF-8'); |
|
440 | + $shortened = $s !== $tmp; |
|
441 | + } else { |
|
442 | + $i = $len = 0; |
|
443 | + $maxI = $maxLength * 4; // max UTF-8 length |
|
444 | + do { |
|
445 | + if (($s[$i] < "\x80" || $s[$i] >= "\xC0") && (++$len > $maxLength) || $i >= $maxI) { |
|
446 | + $s = substr($s, 0, $i); |
|
447 | + $shortened = true; |
|
448 | + break; |
|
449 | + } |
|
450 | + } while (isset($s[++$i])); |
|
451 | + } |
|
452 | + } |
|
453 | + |
|
454 | + if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) { // is binary? |
|
455 | + if ($maxLength && strlen($s) > $maxLength) { |
|
456 | + $s = substr($s, 0, $maxLength); |
|
457 | + $shortened = true; |
|
458 | + } |
|
459 | + $s = strtr($s, $table); |
|
460 | + } |
|
461 | + |
|
462 | + return $s . (empty($shortened) ? '' : ' ... '); |
|
463 | + } |
|
464 | + |
|
465 | + |
|
466 | + /** |
|
467 | + * @return array |
|
468 | + */ |
|
469 | + private static function exportObject($obj, array $exporters, $useDebugInfo) |
|
470 | + { |
|
471 | + foreach ($exporters as $type => $dumper) { |
|
472 | + if (!$type || $obj instanceof $type) { |
|
473 | + return call_user_func($dumper, $obj); |
|
474 | + } |
|
475 | + } |
|
476 | + |
|
477 | + if ($useDebugInfo && method_exists($obj, '__debugInfo')) { |
|
478 | + return $obj->__debugInfo(); |
|
479 | + } |
|
480 | + |
|
481 | + return (array) $obj; |
|
482 | + } |
|
483 | + |
|
484 | + |
|
485 | + /** |
|
486 | + * @return array |
|
487 | + */ |
|
488 | + private static function exportClosure(\Closure $obj) |
|
489 | + { |
|
490 | + $rc = new \ReflectionFunction($obj); |
|
491 | + $res = []; |
|
492 | + foreach ($rc->getParameters() as $param) { |
|
493 | + $res[] = '$' . $param->getName(); |
|
494 | + } |
|
495 | + return [ |
|
496 | + 'file' => $rc->getFileName(), |
|
497 | + 'line' => $rc->getStartLine(), |
|
498 | + 'variables' => $rc->getStaticVariables(), |
|
499 | + 'parameters' => implode(', ', $res), |
|
500 | + ]; |
|
501 | + } |
|
502 | + |
|
503 | + |
|
504 | + /** |
|
505 | + * @return array |
|
506 | + */ |
|
507 | + private static function exportSplFileInfo(\SplFileInfo $obj) |
|
508 | + { |
|
509 | + return ['path' => $obj->getPathname()]; |
|
510 | + } |
|
511 | + |
|
512 | + |
|
513 | + /** |
|
514 | + * @return array |
|
515 | + */ |
|
516 | + private static function exportSplObjectStorage(\SplObjectStorage $obj) |
|
517 | + { |
|
518 | + $res = []; |
|
519 | + foreach (clone $obj as $item) { |
|
520 | + $res[] = ['object' => $item, 'data' => $obj[$item]]; |
|
521 | + } |
|
522 | + return $res; |
|
523 | + } |
|
524 | + |
|
525 | + |
|
526 | + /** |
|
527 | + * @return array |
|
528 | + */ |
|
529 | + private static function exportPhpIncompleteClass(\__PHP_Incomplete_Class $obj) |
|
530 | + { |
|
531 | + $info = ['className' => null, 'private' => [], 'protected' => [], 'public' => []]; |
|
532 | + foreach ((array) $obj as $name => $value) { |
|
533 | + if ($name === '__PHP_Incomplete_Class_Name') { |
|
534 | + $info['className'] = $value; |
|
535 | + } elseif (preg_match('#^\x0\*\x0(.+)\z#', $name, $m)) { |
|
536 | + $info['protected'][$m[1]] = $value; |
|
537 | + } elseif (preg_match('#^\x0(.+)\x0(.+)\z#', $name, $m)) { |
|
538 | + $info['private'][$m[1] . '::$' . $m[2]] = $value; |
|
539 | + } else { |
|
540 | + $info['public'][$name] = $value; |
|
541 | + } |
|
542 | + } |
|
543 | + return $info; |
|
544 | + } |
|
545 | + |
|
546 | + |
|
547 | + /** |
|
548 | + * Finds the location where dump was called. |
|
549 | + * @return array|null [file, line, code] |
|
550 | + */ |
|
551 | + private static function findLocation() |
|
552 | + { |
|
553 | + foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) { |
|
554 | + if (isset($item['class']) && $item['class'] === __CLASS__) { |
|
555 | + $location = $item; |
|
556 | + continue; |
|
557 | + } elseif (isset($item['function'])) { |
|
558 | + try { |
|
559 | + $reflection = isset($item['class']) |
|
560 | + ? new \ReflectionMethod($item['class'], $item['function']) |
|
561 | + : new \ReflectionFunction($item['function']); |
|
562 | + if ($reflection->isInternal() || preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())) { |
|
563 | + $location = $item; |
|
564 | + continue; |
|
565 | + } |
|
566 | + } catch (\ReflectionException $e) { |
|
567 | + } |
|
568 | + } |
|
569 | + break; |
|
570 | + } |
|
571 | + |
|
572 | + if (isset($location['file'], $location['line']) && is_file($location['file'])) { |
|
573 | + $lines = file($location['file']); |
|
574 | + $line = $lines[$location['line'] - 1]; |
|
575 | + return [ |
|
576 | + $location['file'], |
|
577 | + $location['line'], |
|
578 | + trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line), |
|
579 | + ]; |
|
580 | + } |
|
581 | + } |
|
582 | + |
|
583 | + |
|
584 | + /** |
|
585 | + * @return bool |
|
586 | + */ |
|
587 | + private static function detectColors() |
|
588 | + { |
|
589 | + return self::$terminalColors && |
|
590 | + (getenv('ConEmuANSI') === 'ON' |
|
591 | + || getenv('ANSICON') !== false |
|
592 | + || getenv('term') === 'xterm-256color' |
|
593 | + || (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT))); |
|
594 | + } |
|
595 | 595 | } |
@@ -13,233 +13,233 @@ discard block |
||
13 | 13 | */ |
14 | 14 | class Bar |
15 | 15 | { |
16 | - /** @var IBarPanel[] */ |
|
17 | - private $panels = []; |
|
18 | - |
|
19 | - /** @var bool initialized by dispatchAssets() */ |
|
20 | - private $useSession = false; |
|
21 | - |
|
22 | - /** @var string|NULL generated by renderLoader() */ |
|
23 | - private $contentId; |
|
24 | - |
|
25 | - |
|
26 | - /** |
|
27 | - * Add custom panel. |
|
28 | - * @param IBarPanel $panel |
|
29 | - * @param string $id |
|
30 | - * @return static |
|
31 | - */ |
|
32 | - public function addPanel(IBarPanel $panel, $id = null) |
|
33 | - { |
|
34 | - if ($id === null) { |
|
35 | - $c = 0; |
|
36 | - do { |
|
37 | - $id = get_class($panel) . ($c++ ? "-$c" : ''); |
|
38 | - } while (isset($this->panels[$id])); |
|
39 | - } |
|
40 | - $this->panels[$id] = $panel; |
|
41 | - return $this; |
|
42 | - } |
|
43 | - |
|
44 | - |
|
45 | - /** |
|
46 | - * Returns panel with given id |
|
47 | - * @param string $id |
|
48 | - * @return IBarPanel|null |
|
49 | - */ |
|
50 | - public function getPanel($id) |
|
51 | - { |
|
52 | - return isset($this->panels[$id]) ? $this->panels[$id] : null; |
|
53 | - } |
|
54 | - |
|
55 | - |
|
56 | - /** |
|
57 | - * Renders loading <script> |
|
58 | - * @return void |
|
59 | - */ |
|
60 | - public function renderLoader() |
|
61 | - { |
|
62 | - if (!$this->useSession) { |
|
63 | - throw new \LogicException('Start session before Tracy is enabled.'); |
|
64 | - } |
|
65 | - $contentId = $this->contentId = $this->contentId ?: substr(md5(uniqid('', true)), 0, 10); |
|
66 | - $nonce = Helpers::getNonce(); |
|
67 | - $async = true; |
|
68 | - require __DIR__ . '/assets/Bar/loader.phtml'; |
|
69 | - } |
|
70 | - |
|
71 | - |
|
72 | - /** |
|
73 | - * Renders debug bar. |
|
74 | - * @return void |
|
75 | - */ |
|
76 | - public function render() |
|
77 | - { |
|
78 | - $useSession = $this->useSession && session_status() === PHP_SESSION_ACTIVE; |
|
79 | - $redirectQueue = &$_SESSION['_tracy']['redirect']; |
|
80 | - |
|
81 | - foreach (['bar', 'redirect', 'bluescreen'] as $key) { |
|
82 | - $queue = &$_SESSION['_tracy'][$key]; |
|
83 | - $queue = array_slice((array) $queue, -10, null, true); |
|
84 | - $queue = array_filter($queue, function ($item) { |
|
85 | - return isset($item['time']) && $item['time'] > time() - 60; |
|
86 | - }); |
|
87 | - } |
|
88 | - |
|
89 | - $rows = []; |
|
90 | - |
|
91 | - if (Helpers::isAjax()) { |
|
92 | - if ($useSession) { |
|
93 | - $rows[] = (object) ['type' => 'ajax', 'panels' => $this->renderPanels('-ajax')]; |
|
94 | - $contentId = $_SERVER['HTTP_X_TRACY_AJAX'] . '-ajax'; |
|
95 | - $_SESSION['_tracy']['bar'][$contentId] = ['content' => self::renderHtmlRows($rows), 'dumps' => Dumper::fetchLiveData(), 'time' => time()]; |
|
96 | - } |
|
97 | - |
|
98 | - } elseif (preg_match('#^Location:#im', implode("\n", headers_list()))) { // redirect |
|
99 | - if ($useSession) { |
|
100 | - Dumper::fetchLiveData(); |
|
101 | - Dumper::$livePrefix = count($redirectQueue) . 'p'; |
|
102 | - $redirectQueue[] = [ |
|
103 | - 'panels' => $this->renderPanels('-r' . count($redirectQueue)), |
|
104 | - 'dumps' => Dumper::fetchLiveData(), |
|
105 | - 'time' => time(), |
|
106 | - ]; |
|
107 | - } |
|
108 | - |
|
109 | - } elseif (Helpers::isHtmlMode()) { |
|
110 | - $rows[] = (object) ['type' => 'main', 'panels' => $this->renderPanels()]; |
|
111 | - $dumps = Dumper::fetchLiveData(); |
|
112 | - foreach (array_reverse((array) $redirectQueue) as $info) { |
|
113 | - $rows[] = (object) ['type' => 'redirect', 'panels' => $info['panels']]; |
|
114 | - $dumps += $info['dumps']; |
|
115 | - } |
|
116 | - $redirectQueue = null; |
|
117 | - $content = self::renderHtmlRows($rows); |
|
118 | - |
|
119 | - if ($this->contentId) { |
|
120 | - $_SESSION['_tracy']['bar'][$this->contentId] = ['content' => $content, 'dumps' => $dumps, 'time' => time()]; |
|
121 | - } else { |
|
122 | - $contentId = substr(md5(uniqid('', true)), 0, 10); |
|
123 | - $nonce = Helpers::getNonce(); |
|
124 | - $async = false; |
|
125 | - require __DIR__ . '/assets/Bar/loader.phtml'; |
|
126 | - } |
|
127 | - } |
|
128 | - } |
|
129 | - |
|
130 | - |
|
131 | - /** |
|
132 | - * @return string |
|
133 | - */ |
|
134 | - private static function renderHtmlRows(array $rows) |
|
135 | - { |
|
136 | - ob_start(function () {}); |
|
137 | - require __DIR__ . '/assets/Bar/panels.phtml'; |
|
138 | - require __DIR__ . '/assets/Bar/bar.phtml'; |
|
139 | - return Helpers::fixEncoding(ob_get_clean()); |
|
140 | - } |
|
141 | - |
|
142 | - |
|
143 | - /** |
|
144 | - * @return array |
|
145 | - */ |
|
146 | - private function renderPanels($suffix = null) |
|
147 | - { |
|
148 | - set_error_handler(function ($severity, $message, $file, $line) { |
|
149 | - if (error_reporting() & $severity) { |
|
150 | - throw new \ErrorException($message, 0, $severity, $file, $line); |
|
151 | - } |
|
152 | - }); |
|
153 | - |
|
154 | - $obLevel = ob_get_level(); |
|
155 | - $panels = []; |
|
156 | - |
|
157 | - foreach ($this->panels as $id => $panel) { |
|
158 | - $idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix; |
|
159 | - try { |
|
160 | - $tab = (string) $panel->getTab(); |
|
161 | - $panelHtml = $tab ? (string) $panel->getPanel() : null; |
|
162 | - if ($tab && $panel instanceof \Nette\Diagnostics\IBarPanel) { |
|
163 | - $e = new \Exception('Support for Nette\Diagnostics\IBarPanel is deprecated'); |
|
164 | - } |
|
165 | - |
|
166 | - } catch (\Exception $e) { |
|
167 | - } catch (\Throwable $e) { |
|
168 | - } |
|
169 | - if (isset($e)) { |
|
170 | - while (ob_get_level() > $obLevel) { // restore ob-level if broken |
|
171 | - ob_end_clean(); |
|
172 | - } |
|
173 | - $idHtml = "error-$idHtml"; |
|
174 | - $tab = "Error in $id"; |
|
175 | - $panelHtml = "<h1>Error: $id</h1><div class='tracy-inner'>" . nl2br(Helpers::escapeHtml($e)) . '</div>'; |
|
176 | - unset($e); |
|
177 | - } |
|
178 | - $panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml]; |
|
179 | - } |
|
180 | - |
|
181 | - restore_error_handler(); |
|
182 | - return $panels; |
|
183 | - } |
|
184 | - |
|
185 | - |
|
186 | - /** |
|
187 | - * Renders debug bar assets. |
|
188 | - * @return bool |
|
189 | - */ |
|
190 | - public function dispatchAssets() |
|
191 | - { |
|
192 | - $asset = isset($_GET['_tracy_bar']) ? $_GET['_tracy_bar'] : null; |
|
193 | - if ($asset === 'js') { |
|
194 | - header('Content-Type: application/javascript'); |
|
195 | - header('Cache-Control: max-age=864000'); |
|
196 | - header_remove('Pragma'); |
|
197 | - header_remove('Set-Cookie'); |
|
198 | - $this->renderAssets(); |
|
199 | - return true; |
|
200 | - } |
|
201 | - |
|
202 | - $this->useSession = session_status() === PHP_SESSION_ACTIVE; |
|
203 | - |
|
204 | - if ($this->useSession && Helpers::isAjax()) { |
|
205 | - header('X-Tracy-Ajax: 1'); // session must be already locked |
|
206 | - } |
|
207 | - |
|
208 | - if ($this->useSession && $asset && preg_match('#^content(-ajax)?\.(\w+)$#', $asset, $m)) { |
|
209 | - $session = &$_SESSION['_tracy']['bar'][$m[2] . $m[1]]; |
|
210 | - header('Content-Type: application/javascript'); |
|
211 | - header('Cache-Control: max-age=60'); |
|
212 | - header_remove('Set-Cookie'); |
|
213 | - if (!$m[1]) { |
|
214 | - $this->renderAssets(); |
|
215 | - } |
|
216 | - if ($session) { |
|
217 | - $method = $m[1] ? 'loadAjax' : 'init'; |
|
218 | - echo "Tracy.Debug.$method(", json_encode($session['content']), ', ', json_encode($session['dumps']), ');'; |
|
219 | - $session = null; |
|
220 | - } |
|
221 | - $session = &$_SESSION['_tracy']['bluescreen'][$m[2]]; |
|
222 | - if ($session) { |
|
223 | - echo 'Tracy.BlueScreen.loadAjax(', json_encode($session['content']), ', ', json_encode($session['dumps']), ');'; |
|
224 | - $session = null; |
|
225 | - } |
|
226 | - return true; |
|
227 | - } |
|
228 | - |
|
229 | - return false; |
|
230 | - } |
|
231 | - |
|
232 | - |
|
233 | - private function renderAssets() |
|
234 | - { |
|
235 | - $css = array_map('file_get_contents', array_merge([ |
|
236 | - __DIR__ . '/assets/Bar/bar.css', |
|
237 | - __DIR__ . '/assets/Toggle/toggle.css', |
|
238 | - __DIR__ . '/assets/Dumper/dumper.css', |
|
239 | - __DIR__ . '/assets/BlueScreen/bluescreen.css', |
|
240 | - ], Debugger::$customCssFiles)); |
|
241 | - |
|
242 | - echo |
|
16 | + /** @var IBarPanel[] */ |
|
17 | + private $panels = []; |
|
18 | + |
|
19 | + /** @var bool initialized by dispatchAssets() */ |
|
20 | + private $useSession = false; |
|
21 | + |
|
22 | + /** @var string|NULL generated by renderLoader() */ |
|
23 | + private $contentId; |
|
24 | + |
|
25 | + |
|
26 | + /** |
|
27 | + * Add custom panel. |
|
28 | + * @param IBarPanel $panel |
|
29 | + * @param string $id |
|
30 | + * @return static |
|
31 | + */ |
|
32 | + public function addPanel(IBarPanel $panel, $id = null) |
|
33 | + { |
|
34 | + if ($id === null) { |
|
35 | + $c = 0; |
|
36 | + do { |
|
37 | + $id = get_class($panel) . ($c++ ? "-$c" : ''); |
|
38 | + } while (isset($this->panels[$id])); |
|
39 | + } |
|
40 | + $this->panels[$id] = $panel; |
|
41 | + return $this; |
|
42 | + } |
|
43 | + |
|
44 | + |
|
45 | + /** |
|
46 | + * Returns panel with given id |
|
47 | + * @param string $id |
|
48 | + * @return IBarPanel|null |
|
49 | + */ |
|
50 | + public function getPanel($id) |
|
51 | + { |
|
52 | + return isset($this->panels[$id]) ? $this->panels[$id] : null; |
|
53 | + } |
|
54 | + |
|
55 | + |
|
56 | + /** |
|
57 | + * Renders loading <script> |
|
58 | + * @return void |
|
59 | + */ |
|
60 | + public function renderLoader() |
|
61 | + { |
|
62 | + if (!$this->useSession) { |
|
63 | + throw new \LogicException('Start session before Tracy is enabled.'); |
|
64 | + } |
|
65 | + $contentId = $this->contentId = $this->contentId ?: substr(md5(uniqid('', true)), 0, 10); |
|
66 | + $nonce = Helpers::getNonce(); |
|
67 | + $async = true; |
|
68 | + require __DIR__ . '/assets/Bar/loader.phtml'; |
|
69 | + } |
|
70 | + |
|
71 | + |
|
72 | + /** |
|
73 | + * Renders debug bar. |
|
74 | + * @return void |
|
75 | + */ |
|
76 | + public function render() |
|
77 | + { |
|
78 | + $useSession = $this->useSession && session_status() === PHP_SESSION_ACTIVE; |
|
79 | + $redirectQueue = &$_SESSION['_tracy']['redirect']; |
|
80 | + |
|
81 | + foreach (['bar', 'redirect', 'bluescreen'] as $key) { |
|
82 | + $queue = &$_SESSION['_tracy'][$key]; |
|
83 | + $queue = array_slice((array) $queue, -10, null, true); |
|
84 | + $queue = array_filter($queue, function ($item) { |
|
85 | + return isset($item['time']) && $item['time'] > time() - 60; |
|
86 | + }); |
|
87 | + } |
|
88 | + |
|
89 | + $rows = []; |
|
90 | + |
|
91 | + if (Helpers::isAjax()) { |
|
92 | + if ($useSession) { |
|
93 | + $rows[] = (object) ['type' => 'ajax', 'panels' => $this->renderPanels('-ajax')]; |
|
94 | + $contentId = $_SERVER['HTTP_X_TRACY_AJAX'] . '-ajax'; |
|
95 | + $_SESSION['_tracy']['bar'][$contentId] = ['content' => self::renderHtmlRows($rows), 'dumps' => Dumper::fetchLiveData(), 'time' => time()]; |
|
96 | + } |
|
97 | + |
|
98 | + } elseif (preg_match('#^Location:#im', implode("\n", headers_list()))) { // redirect |
|
99 | + if ($useSession) { |
|
100 | + Dumper::fetchLiveData(); |
|
101 | + Dumper::$livePrefix = count($redirectQueue) . 'p'; |
|
102 | + $redirectQueue[] = [ |
|
103 | + 'panels' => $this->renderPanels('-r' . count($redirectQueue)), |
|
104 | + 'dumps' => Dumper::fetchLiveData(), |
|
105 | + 'time' => time(), |
|
106 | + ]; |
|
107 | + } |
|
108 | + |
|
109 | + } elseif (Helpers::isHtmlMode()) { |
|
110 | + $rows[] = (object) ['type' => 'main', 'panels' => $this->renderPanels()]; |
|
111 | + $dumps = Dumper::fetchLiveData(); |
|
112 | + foreach (array_reverse((array) $redirectQueue) as $info) { |
|
113 | + $rows[] = (object) ['type' => 'redirect', 'panels' => $info['panels']]; |
|
114 | + $dumps += $info['dumps']; |
|
115 | + } |
|
116 | + $redirectQueue = null; |
|
117 | + $content = self::renderHtmlRows($rows); |
|
118 | + |
|
119 | + if ($this->contentId) { |
|
120 | + $_SESSION['_tracy']['bar'][$this->contentId] = ['content' => $content, 'dumps' => $dumps, 'time' => time()]; |
|
121 | + } else { |
|
122 | + $contentId = substr(md5(uniqid('', true)), 0, 10); |
|
123 | + $nonce = Helpers::getNonce(); |
|
124 | + $async = false; |
|
125 | + require __DIR__ . '/assets/Bar/loader.phtml'; |
|
126 | + } |
|
127 | + } |
|
128 | + } |
|
129 | + |
|
130 | + |
|
131 | + /** |
|
132 | + * @return string |
|
133 | + */ |
|
134 | + private static function renderHtmlRows(array $rows) |
|
135 | + { |
|
136 | + ob_start(function () {}); |
|
137 | + require __DIR__ . '/assets/Bar/panels.phtml'; |
|
138 | + require __DIR__ . '/assets/Bar/bar.phtml'; |
|
139 | + return Helpers::fixEncoding(ob_get_clean()); |
|
140 | + } |
|
141 | + |
|
142 | + |
|
143 | + /** |
|
144 | + * @return array |
|
145 | + */ |
|
146 | + private function renderPanels($suffix = null) |
|
147 | + { |
|
148 | + set_error_handler(function ($severity, $message, $file, $line) { |
|
149 | + if (error_reporting() & $severity) { |
|
150 | + throw new \ErrorException($message, 0, $severity, $file, $line); |
|
151 | + } |
|
152 | + }); |
|
153 | + |
|
154 | + $obLevel = ob_get_level(); |
|
155 | + $panels = []; |
|
156 | + |
|
157 | + foreach ($this->panels as $id => $panel) { |
|
158 | + $idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix; |
|
159 | + try { |
|
160 | + $tab = (string) $panel->getTab(); |
|
161 | + $panelHtml = $tab ? (string) $panel->getPanel() : null; |
|
162 | + if ($tab && $panel instanceof \Nette\Diagnostics\IBarPanel) { |
|
163 | + $e = new \Exception('Support for Nette\Diagnostics\IBarPanel is deprecated'); |
|
164 | + } |
|
165 | + |
|
166 | + } catch (\Exception $e) { |
|
167 | + } catch (\Throwable $e) { |
|
168 | + } |
|
169 | + if (isset($e)) { |
|
170 | + while (ob_get_level() > $obLevel) { // restore ob-level if broken |
|
171 | + ob_end_clean(); |
|
172 | + } |
|
173 | + $idHtml = "error-$idHtml"; |
|
174 | + $tab = "Error in $id"; |
|
175 | + $panelHtml = "<h1>Error: $id</h1><div class='tracy-inner'>" . nl2br(Helpers::escapeHtml($e)) . '</div>'; |
|
176 | + unset($e); |
|
177 | + } |
|
178 | + $panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml]; |
|
179 | + } |
|
180 | + |
|
181 | + restore_error_handler(); |
|
182 | + return $panels; |
|
183 | + } |
|
184 | + |
|
185 | + |
|
186 | + /** |
|
187 | + * Renders debug bar assets. |
|
188 | + * @return bool |
|
189 | + */ |
|
190 | + public function dispatchAssets() |
|
191 | + { |
|
192 | + $asset = isset($_GET['_tracy_bar']) ? $_GET['_tracy_bar'] : null; |
|
193 | + if ($asset === 'js') { |
|
194 | + header('Content-Type: application/javascript'); |
|
195 | + header('Cache-Control: max-age=864000'); |
|
196 | + header_remove('Pragma'); |
|
197 | + header_remove('Set-Cookie'); |
|
198 | + $this->renderAssets(); |
|
199 | + return true; |
|
200 | + } |
|
201 | + |
|
202 | + $this->useSession = session_status() === PHP_SESSION_ACTIVE; |
|
203 | + |
|
204 | + if ($this->useSession && Helpers::isAjax()) { |
|
205 | + header('X-Tracy-Ajax: 1'); // session must be already locked |
|
206 | + } |
|
207 | + |
|
208 | + if ($this->useSession && $asset && preg_match('#^content(-ajax)?\.(\w+)$#', $asset, $m)) { |
|
209 | + $session = &$_SESSION['_tracy']['bar'][$m[2] . $m[1]]; |
|
210 | + header('Content-Type: application/javascript'); |
|
211 | + header('Cache-Control: max-age=60'); |
|
212 | + header_remove('Set-Cookie'); |
|
213 | + if (!$m[1]) { |
|
214 | + $this->renderAssets(); |
|
215 | + } |
|
216 | + if ($session) { |
|
217 | + $method = $m[1] ? 'loadAjax' : 'init'; |
|
218 | + echo "Tracy.Debug.$method(", json_encode($session['content']), ', ', json_encode($session['dumps']), ');'; |
|
219 | + $session = null; |
|
220 | + } |
|
221 | + $session = &$_SESSION['_tracy']['bluescreen'][$m[2]]; |
|
222 | + if ($session) { |
|
223 | + echo 'Tracy.BlueScreen.loadAjax(', json_encode($session['content']), ', ', json_encode($session['dumps']), ');'; |
|
224 | + $session = null; |
|
225 | + } |
|
226 | + return true; |
|
227 | + } |
|
228 | + |
|
229 | + return false; |
|
230 | + } |
|
231 | + |
|
232 | + |
|
233 | + private function renderAssets() |
|
234 | + { |
|
235 | + $css = array_map('file_get_contents', array_merge([ |
|
236 | + __DIR__ . '/assets/Bar/bar.css', |
|
237 | + __DIR__ . '/assets/Toggle/toggle.css', |
|
238 | + __DIR__ . '/assets/Dumper/dumper.css', |
|
239 | + __DIR__ . '/assets/BlueScreen/bluescreen.css', |
|
240 | + ], Debugger::$customCssFiles)); |
|
241 | + |
|
242 | + echo |
|
243 | 243 | "(function(){ |
244 | 244 | var el = document.createElement('style'); |
245 | 245 | el.setAttribute('nonce', document.currentScript.getAttribute('nonce') || document.currentScript.nonce); |
@@ -248,11 +248,11 @@ discard block |
||
248 | 248 | document.head.appendChild(el);}) |
249 | 249 | ();\n"; |
250 | 250 | |
251 | - array_map('readfile', array_merge([ |
|
252 | - __DIR__ . '/assets/Bar/bar.js', |
|
253 | - __DIR__ . '/assets/Toggle/toggle.js', |
|
254 | - __DIR__ . '/assets/Dumper/dumper.js', |
|
255 | - __DIR__ . '/assets/BlueScreen/bluescreen.js', |
|
256 | - ], Debugger::$customJsFiles)); |
|
257 | - } |
|
251 | + array_map('readfile', array_merge([ |
|
252 | + __DIR__ . '/assets/Bar/bar.js', |
|
253 | + __DIR__ . '/assets/Toggle/toggle.js', |
|
254 | + __DIR__ . '/assets/Dumper/dumper.js', |
|
255 | + __DIR__ . '/assets/BlueScreen/bluescreen.js', |
|
256 | + ], Debugger::$customJsFiles)); |
|
257 | + } |
|
258 | 258 | } |
@@ -13,66 +13,66 @@ |
||
13 | 13 | */ |
14 | 14 | class OutputDebugger |
15 | 15 | { |
16 | - const BOM = "\xEF\xBB\xBF"; |
|
16 | + const BOM = "\xEF\xBB\xBF"; |
|
17 | 17 | |
18 | - /** @var array of [file, line, output, stack] */ |
|
19 | - private $list = []; |
|
18 | + /** @var array of [file, line, output, stack] */ |
|
19 | + private $list = []; |
|
20 | 20 | |
21 | 21 | |
22 | - public static function enable() |
|
23 | - { |
|
24 | - $me = new static; |
|
25 | - $me->start(); |
|
26 | - } |
|
22 | + public static function enable() |
|
23 | + { |
|
24 | + $me = new static; |
|
25 | + $me->start(); |
|
26 | + } |
|
27 | 27 | |
28 | 28 | |
29 | - public function start() |
|
30 | - { |
|
31 | - foreach (get_included_files() as $file) { |
|
32 | - if (fread(fopen($file, 'r'), 3) === self::BOM) { |
|
33 | - $this->list[] = [$file, 1, self::BOM]; |
|
34 | - } |
|
35 | - } |
|
36 | - ob_start([$this, 'handler'], 1); |
|
37 | - } |
|
29 | + public function start() |
|
30 | + { |
|
31 | + foreach (get_included_files() as $file) { |
|
32 | + if (fread(fopen($file, 'r'), 3) === self::BOM) { |
|
33 | + $this->list[] = [$file, 1, self::BOM]; |
|
34 | + } |
|
35 | + } |
|
36 | + ob_start([$this, 'handler'], 1); |
|
37 | + } |
|
38 | 38 | |
39 | 39 | |
40 | - /** @internal */ |
|
41 | - public function handler($s, $phase) |
|
42 | - { |
|
43 | - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
|
44 | - if (isset($trace[0]['file'], $trace[0]['line'])) { |
|
45 | - $stack = $trace; |
|
46 | - unset($stack[0]['line'], $stack[0]['args']); |
|
47 | - $i = count($this->list); |
|
48 | - if ($i && $this->list[$i - 1][3] === $stack) { |
|
49 | - $this->list[$i - 1][2] .= $s; |
|
50 | - } else { |
|
51 | - $this->list[] = [$trace[0]['file'], $trace[0]['line'], $s, $stack]; |
|
52 | - } |
|
53 | - } |
|
54 | - if ($phase === PHP_OUTPUT_HANDLER_FINAL) { |
|
55 | - return $this->renderHtml(); |
|
56 | - } |
|
57 | - } |
|
40 | + /** @internal */ |
|
41 | + public function handler($s, $phase) |
|
42 | + { |
|
43 | + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
|
44 | + if (isset($trace[0]['file'], $trace[0]['line'])) { |
|
45 | + $stack = $trace; |
|
46 | + unset($stack[0]['line'], $stack[0]['args']); |
|
47 | + $i = count($this->list); |
|
48 | + if ($i && $this->list[$i - 1][3] === $stack) { |
|
49 | + $this->list[$i - 1][2] .= $s; |
|
50 | + } else { |
|
51 | + $this->list[] = [$trace[0]['file'], $trace[0]['line'], $s, $stack]; |
|
52 | + } |
|
53 | + } |
|
54 | + if ($phase === PHP_OUTPUT_HANDLER_FINAL) { |
|
55 | + return $this->renderHtml(); |
|
56 | + } |
|
57 | + } |
|
58 | 58 | |
59 | 59 | |
60 | - private function renderHtml() |
|
61 | - { |
|
62 | - $res = '<style>code, pre {white-space:nowrap} a {text-decoration:none} pre {color:gray;display:inline} big {color:red}</style><code>'; |
|
63 | - foreach ($this->list as $item) { |
|
64 | - $stack = []; |
|
65 | - foreach (array_slice($item[3], 1) as $t) { |
|
66 | - $t += ['class' => '', 'type' => '', 'function' => '']; |
|
67 | - $stack[] = "$t[class]$t[type]$t[function]()" |
|
68 | - . (isset($t['file'], $t['line']) ? ' in ' . basename($t['file']) . ":$t[line]" : ''); |
|
69 | - } |
|
60 | + private function renderHtml() |
|
61 | + { |
|
62 | + $res = '<style>code, pre {white-space:nowrap} a {text-decoration:none} pre {color:gray;display:inline} big {color:red}</style><code>'; |
|
63 | + foreach ($this->list as $item) { |
|
64 | + $stack = []; |
|
65 | + foreach (array_slice($item[3], 1) as $t) { |
|
66 | + $t += ['class' => '', 'type' => '', 'function' => '']; |
|
67 | + $stack[] = "$t[class]$t[type]$t[function]()" |
|
68 | + . (isset($t['file'], $t['line']) ? ' in ' . basename($t['file']) . ":$t[line]" : ''); |
|
69 | + } |
|
70 | 70 | |
71 | - $res .= '<span title="' . Helpers::escapeHtml(implode("\n", $stack)) . '">' |
|
72 | - . Helpers::editorLink($item[0], $item[1]) . ' ' |
|
73 | - . str_replace(self::BOM, '<big>BOM</big>', Dumper::toHtml($item[2])) |
|
74 | - . "</span><br>\n"; |
|
75 | - } |
|
76 | - return $res . '</code>'; |
|
77 | - } |
|
71 | + $res .= '<span title="' . Helpers::escapeHtml(implode("\n", $stack)) . '">' |
|
72 | + . Helpers::editorLink($item[0], $item[1]) . ' ' |
|
73 | + . str_replace(self::BOM, '<big>BOM</big>', Dumper::toHtml($item[2])) |
|
74 | + . "</span><br>\n"; |
|
75 | + } |
|
76 | + return $res . '</code>'; |
|
77 | + } |
|
78 | 78 | } |
@@ -13,197 +13,197 @@ |
||
13 | 13 | */ |
14 | 14 | class Logger implements ILogger |
15 | 15 | { |
16 | - /** @var string|null name of the directory where errors should be logged */ |
|
17 | - public $directory; |
|
18 | - |
|
19 | - /** @var string|array|null email or emails to which send error notifications */ |
|
20 | - public $email; |
|
21 | - |
|
22 | - /** @var string|null sender of email notifications */ |
|
23 | - public $fromEmail; |
|
24 | - |
|
25 | - /** @var mixed interval for sending email is 2 days */ |
|
26 | - public $emailSnooze = '2 days'; |
|
27 | - |
|
28 | - /** @var callable handler for sending emails */ |
|
29 | - public $mailer; |
|
30 | - |
|
31 | - /** @var BlueScreen|null */ |
|
32 | - private $blueScreen; |
|
33 | - |
|
34 | - |
|
35 | - /** |
|
36 | - * @param string|null $directory |
|
37 | - * @param string|array|null $email |
|
38 | - */ |
|
39 | - public function __construct($directory, $email = null, BlueScreen $blueScreen = null) |
|
40 | - { |
|
41 | - $this->directory = $directory; |
|
42 | - $this->email = $email; |
|
43 | - $this->blueScreen = $blueScreen; |
|
44 | - $this->mailer = [$this, 'defaultMailer']; |
|
45 | - } |
|
46 | - |
|
47 | - |
|
48 | - /** |
|
49 | - * Logs message or exception to file and sends email notification. |
|
50 | - * @param mixed $message |
|
51 | - * @param string $priority one of constant ILogger::INFO, WARNING, ERROR (sends email), EXCEPTION (sends email), CRITICAL (sends email) |
|
52 | - * @return string|null logged error filename |
|
53 | - */ |
|
54 | - public function log($message, $priority = self::INFO) |
|
55 | - { |
|
56 | - if (!$this->directory) { |
|
57 | - throw new \LogicException('Logging directory is not specified.'); |
|
58 | - } elseif (!is_dir($this->directory)) { |
|
59 | - throw new \RuntimeException("Logging directory '$this->directory' is not found or is not directory."); |
|
60 | - } |
|
61 | - |
|
62 | - $exceptionFile = $message instanceof \Exception || $message instanceof \Throwable |
|
63 | - ? $this->getExceptionFile($message) |
|
64 | - : null; |
|
65 | - $line = static::formatLogLine($message, $exceptionFile); |
|
66 | - $file = $this->directory . '/' . strtolower($priority ?: self::INFO) . '.log'; |
|
67 | - |
|
68 | - if (!@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX)) { // @ is escalated to exception |
|
69 | - throw new \RuntimeException("Unable to write to log file '$file'. Is directory writable?"); |
|
70 | - } |
|
71 | - |
|
72 | - if ($exceptionFile) { |
|
73 | - $this->logException($message, $exceptionFile); |
|
74 | - } |
|
75 | - |
|
76 | - if (in_array($priority, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) { |
|
77 | - $this->sendEmail($message); |
|
78 | - } |
|
79 | - |
|
80 | - return $exceptionFile; |
|
81 | - } |
|
82 | - |
|
83 | - |
|
84 | - /** |
|
85 | - * @param mixed $message |
|
86 | - * @return string |
|
87 | - */ |
|
88 | - public static function formatMessage($message) |
|
89 | - { |
|
90 | - if ($message instanceof \Exception || $message instanceof \Throwable) { |
|
91 | - while ($message) { |
|
92 | - $tmp[] = ($message instanceof \ErrorException |
|
93 | - ? Helpers::errorTypeToString($message->getSeverity()) . ': ' . $message->getMessage() |
|
94 | - : Helpers::getClass($message) . ': ' . $message->getMessage() . ($message->getCode() ? ' #' . $message->getCode() : '') |
|
95 | - ) . ' in ' . $message->getFile() . ':' . $message->getLine(); |
|
96 | - $message = $message->getPrevious(); |
|
97 | - } |
|
98 | - $message = implode("\ncaused by ", $tmp); |
|
99 | - |
|
100 | - } elseif (!is_string($message)) { |
|
101 | - $message = Dumper::toText($message); |
|
102 | - } |
|
103 | - |
|
104 | - return trim($message); |
|
105 | - } |
|
106 | - |
|
107 | - |
|
108 | - /** |
|
109 | - * @param mixed $message |
|
110 | - * @return string |
|
111 | - */ |
|
112 | - public static function formatLogLine($message, $exceptionFile = null) |
|
113 | - { |
|
114 | - return implode(' ', [ |
|
115 | - @date('[Y-m-d H-i-s]'), // @ timezone may not be set |
|
116 | - preg_replace('#\s*\r?\n\s*#', ' ', static::formatMessage($message)), |
|
117 | - ' @ ' . Helpers::getSource(), |
|
118 | - $exceptionFile ? ' @@ ' . basename($exceptionFile) : null, |
|
119 | - ]); |
|
120 | - } |
|
121 | - |
|
122 | - |
|
123 | - /** |
|
124 | - * @param \Exception|\Throwable $exception |
|
125 | - * @return string |
|
126 | - */ |
|
127 | - public function getExceptionFile($exception) |
|
128 | - { |
|
129 | - while ($exception) { |
|
130 | - $data[] = [ |
|
131 | - get_class($exception), $exception->getMessage(), $exception->getCode(), $exception->getFile(), $exception->getLine(), |
|
132 | - array_map(function ($item) { unset($item['args']); return $item; }, $exception->getTrace()), |
|
133 | - ]; |
|
134 | - $exception = $exception->getPrevious(); |
|
135 | - } |
|
136 | - $hash = substr(md5(serialize($data)), 0, 10); |
|
137 | - $dir = strtr($this->directory . '/', '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR); |
|
138 | - foreach (new \DirectoryIterator($this->directory) as $file) { |
|
139 | - if (strpos($file->getBasename(), $hash)) { |
|
140 | - return $dir . $file; |
|
141 | - } |
|
142 | - } |
|
143 | - return $dir . 'exception--' . @date('Y-m-d--H-i') . "--$hash.html"; // @ timezone may not be set |
|
144 | - } |
|
145 | - |
|
146 | - |
|
147 | - /** |
|
148 | - * Logs exception to the file if file doesn't exist. |
|
149 | - * @param \Exception|\Throwable $exception |
|
150 | - * @return string logged error filename |
|
151 | - */ |
|
152 | - protected function logException($exception, $file = null) |
|
153 | - { |
|
154 | - $file = $file ?: $this->getExceptionFile($exception); |
|
155 | - $bs = $this->blueScreen ?: new BlueScreen; |
|
156 | - $bs->renderToFile($exception, $file); |
|
157 | - return $file; |
|
158 | - } |
|
159 | - |
|
160 | - |
|
161 | - /** |
|
162 | - * @param mixed $message |
|
163 | - * @return void |
|
164 | - */ |
|
165 | - protected function sendEmail($message) |
|
166 | - { |
|
167 | - $snooze = is_numeric($this->emailSnooze) |
|
168 | - ? $this->emailSnooze |
|
169 | - : @strtotime($this->emailSnooze) - time(); // @ timezone may not be set |
|
170 | - |
|
171 | - if ( |
|
172 | - $this->email |
|
173 | - && $this->mailer |
|
174 | - && @filemtime($this->directory . '/email-sent') + $snooze < time() // @ file may not exist |
|
175 | - && @file_put_contents($this->directory . '/email-sent', 'sent') // @ file may not be writable |
|
176 | - ) { |
|
177 | - call_user_func($this->mailer, $message, implode(', ', (array) $this->email)); |
|
178 | - } |
|
179 | - } |
|
180 | - |
|
181 | - |
|
182 | - /** |
|
183 | - * Default mailer. |
|
184 | - * @param mixed $message |
|
185 | - * @param string $email |
|
186 | - * @return void |
|
187 | - * @internal |
|
188 | - */ |
|
189 | - public function defaultMailer($message, $email) |
|
190 | - { |
|
191 | - $host = preg_replace('#[^\w.-]+#', '', isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n')); |
|
192 | - $parts = str_replace( |
|
193 | - ["\r\n", "\n"], |
|
194 | - ["\n", PHP_EOL], |
|
195 | - [ |
|
196 | - 'headers' => implode("\n", [ |
|
197 | - 'From: ' . ($this->fromEmail ?: "noreply@$host"), |
|
198 | - 'X-Mailer: Tracy', |
|
199 | - 'Content-Type: text/plain; charset=UTF-8', |
|
200 | - 'Content-Transfer-Encoding: 8bit', |
|
201 | - ]) . "\n", |
|
202 | - 'subject' => "PHP: An error occurred on the server $host", |
|
203 | - 'body' => static::formatMessage($message) . "\n\nsource: " . Helpers::getSource(), |
|
204 | - ] |
|
205 | - ); |
|
206 | - |
|
207 | - mail($email, $parts['subject'], $parts['body'], $parts['headers']); |
|
208 | - } |
|
16 | + /** @var string|null name of the directory where errors should be logged */ |
|
17 | + public $directory; |
|
18 | + |
|
19 | + /** @var string|array|null email or emails to which send error notifications */ |
|
20 | + public $email; |
|
21 | + |
|
22 | + /** @var string|null sender of email notifications */ |
|
23 | + public $fromEmail; |
|
24 | + |
|
25 | + /** @var mixed interval for sending email is 2 days */ |
|
26 | + public $emailSnooze = '2 days'; |
|
27 | + |
|
28 | + /** @var callable handler for sending emails */ |
|
29 | + public $mailer; |
|
30 | + |
|
31 | + /** @var BlueScreen|null */ |
|
32 | + private $blueScreen; |
|
33 | + |
|
34 | + |
|
35 | + /** |
|
36 | + * @param string|null $directory |
|
37 | + * @param string|array|null $email |
|
38 | + */ |
|
39 | + public function __construct($directory, $email = null, BlueScreen $blueScreen = null) |
|
40 | + { |
|
41 | + $this->directory = $directory; |
|
42 | + $this->email = $email; |
|
43 | + $this->blueScreen = $blueScreen; |
|
44 | + $this->mailer = [$this, 'defaultMailer']; |
|
45 | + } |
|
46 | + |
|
47 | + |
|
48 | + /** |
|
49 | + * Logs message or exception to file and sends email notification. |
|
50 | + * @param mixed $message |
|
51 | + * @param string $priority one of constant ILogger::INFO, WARNING, ERROR (sends email), EXCEPTION (sends email), CRITICAL (sends email) |
|
52 | + * @return string|null logged error filename |
|
53 | + */ |
|
54 | + public function log($message, $priority = self::INFO) |
|
55 | + { |
|
56 | + if (!$this->directory) { |
|
57 | + throw new \LogicException('Logging directory is not specified.'); |
|
58 | + } elseif (!is_dir($this->directory)) { |
|
59 | + throw new \RuntimeException("Logging directory '$this->directory' is not found or is not directory."); |
|
60 | + } |
|
61 | + |
|
62 | + $exceptionFile = $message instanceof \Exception || $message instanceof \Throwable |
|
63 | + ? $this->getExceptionFile($message) |
|
64 | + : null; |
|
65 | + $line = static::formatLogLine($message, $exceptionFile); |
|
66 | + $file = $this->directory . '/' . strtolower($priority ?: self::INFO) . '.log'; |
|
67 | + |
|
68 | + if (!@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX)) { // @ is escalated to exception |
|
69 | + throw new \RuntimeException("Unable to write to log file '$file'. Is directory writable?"); |
|
70 | + } |
|
71 | + |
|
72 | + if ($exceptionFile) { |
|
73 | + $this->logException($message, $exceptionFile); |
|
74 | + } |
|
75 | + |
|
76 | + if (in_array($priority, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) { |
|
77 | + $this->sendEmail($message); |
|
78 | + } |
|
79 | + |
|
80 | + return $exceptionFile; |
|
81 | + } |
|
82 | + |
|
83 | + |
|
84 | + /** |
|
85 | + * @param mixed $message |
|
86 | + * @return string |
|
87 | + */ |
|
88 | + public static function formatMessage($message) |
|
89 | + { |
|
90 | + if ($message instanceof \Exception || $message instanceof \Throwable) { |
|
91 | + while ($message) { |
|
92 | + $tmp[] = ($message instanceof \ErrorException |
|
93 | + ? Helpers::errorTypeToString($message->getSeverity()) . ': ' . $message->getMessage() |
|
94 | + : Helpers::getClass($message) . ': ' . $message->getMessage() . ($message->getCode() ? ' #' . $message->getCode() : '') |
|
95 | + ) . ' in ' . $message->getFile() . ':' . $message->getLine(); |
|
96 | + $message = $message->getPrevious(); |
|
97 | + } |
|
98 | + $message = implode("\ncaused by ", $tmp); |
|
99 | + |
|
100 | + } elseif (!is_string($message)) { |
|
101 | + $message = Dumper::toText($message); |
|
102 | + } |
|
103 | + |
|
104 | + return trim($message); |
|
105 | + } |
|
106 | + |
|
107 | + |
|
108 | + /** |
|
109 | + * @param mixed $message |
|
110 | + * @return string |
|
111 | + */ |
|
112 | + public static function formatLogLine($message, $exceptionFile = null) |
|
113 | + { |
|
114 | + return implode(' ', [ |
|
115 | + @date('[Y-m-d H-i-s]'), // @ timezone may not be set |
|
116 | + preg_replace('#\s*\r?\n\s*#', ' ', static::formatMessage($message)), |
|
117 | + ' @ ' . Helpers::getSource(), |
|
118 | + $exceptionFile ? ' @@ ' . basename($exceptionFile) : null, |
|
119 | + ]); |
|
120 | + } |
|
121 | + |
|
122 | + |
|
123 | + /** |
|
124 | + * @param \Exception|\Throwable $exception |
|
125 | + * @return string |
|
126 | + */ |
|
127 | + public function getExceptionFile($exception) |
|
128 | + { |
|
129 | + while ($exception) { |
|
130 | + $data[] = [ |
|
131 | + get_class($exception), $exception->getMessage(), $exception->getCode(), $exception->getFile(), $exception->getLine(), |
|
132 | + array_map(function ($item) { unset($item['args']); return $item; }, $exception->getTrace()), |
|
133 | + ]; |
|
134 | + $exception = $exception->getPrevious(); |
|
135 | + } |
|
136 | + $hash = substr(md5(serialize($data)), 0, 10); |
|
137 | + $dir = strtr($this->directory . '/', '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR); |
|
138 | + foreach (new \DirectoryIterator($this->directory) as $file) { |
|
139 | + if (strpos($file->getBasename(), $hash)) { |
|
140 | + return $dir . $file; |
|
141 | + } |
|
142 | + } |
|
143 | + return $dir . 'exception--' . @date('Y-m-d--H-i') . "--$hash.html"; // @ timezone may not be set |
|
144 | + } |
|
145 | + |
|
146 | + |
|
147 | + /** |
|
148 | + * Logs exception to the file if file doesn't exist. |
|
149 | + * @param \Exception|\Throwable $exception |
|
150 | + * @return string logged error filename |
|
151 | + */ |
|
152 | + protected function logException($exception, $file = null) |
|
153 | + { |
|
154 | + $file = $file ?: $this->getExceptionFile($exception); |
|
155 | + $bs = $this->blueScreen ?: new BlueScreen; |
|
156 | + $bs->renderToFile($exception, $file); |
|
157 | + return $file; |
|
158 | + } |
|
159 | + |
|
160 | + |
|
161 | + /** |
|
162 | + * @param mixed $message |
|
163 | + * @return void |
|
164 | + */ |
|
165 | + protected function sendEmail($message) |
|
166 | + { |
|
167 | + $snooze = is_numeric($this->emailSnooze) |
|
168 | + ? $this->emailSnooze |
|
169 | + : @strtotime($this->emailSnooze) - time(); // @ timezone may not be set |
|
170 | + |
|
171 | + if ( |
|
172 | + $this->email |
|
173 | + && $this->mailer |
|
174 | + && @filemtime($this->directory . '/email-sent') + $snooze < time() // @ file may not exist |
|
175 | + && @file_put_contents($this->directory . '/email-sent', 'sent') // @ file may not be writable |
|
176 | + ) { |
|
177 | + call_user_func($this->mailer, $message, implode(', ', (array) $this->email)); |
|
178 | + } |
|
179 | + } |
|
180 | + |
|
181 | + |
|
182 | + /** |
|
183 | + * Default mailer. |
|
184 | + * @param mixed $message |
|
185 | + * @param string $email |
|
186 | + * @return void |
|
187 | + * @internal |
|
188 | + */ |
|
189 | + public function defaultMailer($message, $email) |
|
190 | + { |
|
191 | + $host = preg_replace('#[^\w.-]+#', '', isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n')); |
|
192 | + $parts = str_replace( |
|
193 | + ["\r\n", "\n"], |
|
194 | + ["\n", PHP_EOL], |
|
195 | + [ |
|
196 | + 'headers' => implode("\n", [ |
|
197 | + 'From: ' . ($this->fromEmail ?: "noreply@$host"), |
|
198 | + 'X-Mailer: Tracy', |
|
199 | + 'Content-Type: text/plain; charset=UTF-8', |
|
200 | + 'Content-Transfer-Encoding: 8bit', |
|
201 | + ]) . "\n", |
|
202 | + 'subject' => "PHP: An error occurred on the server $host", |
|
203 | + 'body' => static::formatMessage($message) . "\n\nsource: " . Helpers::getSource(), |
|
204 | + ] |
|
205 | + ); |
|
206 | + |
|
207 | + mail($email, $parts['subject'], $parts['body'], $parts['headers']); |
|
208 | + } |
|
209 | 209 | } |
@@ -13,347 +13,347 @@ |
||
13 | 13 | */ |
14 | 14 | class BlueScreen |
15 | 15 | { |
16 | - /** @var string[] */ |
|
17 | - public $info = []; |
|
18 | - |
|
19 | - /** @var string[] paths to be collapsed in stack trace (e.g. core libraries) */ |
|
20 | - public $collapsePaths = []; |
|
21 | - |
|
22 | - /** @var int */ |
|
23 | - public $maxDepth = 3; |
|
24 | - |
|
25 | - /** @var int */ |
|
26 | - public $maxLength = 150; |
|
27 | - |
|
28 | - /** @var string[] */ |
|
29 | - public $keysToHide = ['password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin']; |
|
30 | - |
|
31 | - /** @var callable[] */ |
|
32 | - private $panels = []; |
|
33 | - |
|
34 | - /** @var callable[] functions that returns action for exceptions */ |
|
35 | - private $actions = []; |
|
36 | - |
|
37 | - |
|
38 | - public function __construct() |
|
39 | - { |
|
40 | - $this->collapsePaths[] = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy$#', strtr(__DIR__, '\\', '/'), $m) |
|
41 | - ? $m[1] |
|
42 | - : __DIR__; |
|
43 | - } |
|
44 | - |
|
45 | - |
|
46 | - /** |
|
47 | - * Add custom panel. |
|
48 | - * @param callable $panel |
|
49 | - * @return static |
|
50 | - */ |
|
51 | - public function addPanel($panel) |
|
52 | - { |
|
53 | - if (!in_array($panel, $this->panels, true)) { |
|
54 | - $this->panels[] = $panel; |
|
55 | - } |
|
56 | - return $this; |
|
57 | - } |
|
58 | - |
|
59 | - |
|
60 | - /** |
|
61 | - * Add action. |
|
62 | - * @param callable $action |
|
63 | - * @return static |
|
64 | - */ |
|
65 | - public function addAction($action) |
|
66 | - { |
|
67 | - $this->actions[] = $action; |
|
68 | - return $this; |
|
69 | - } |
|
70 | - |
|
71 | - |
|
72 | - /** |
|
73 | - * Renders blue screen. |
|
74 | - * @param \Exception|\Throwable $exception |
|
75 | - * @return void |
|
76 | - */ |
|
77 | - public function render($exception) |
|
78 | - { |
|
79 | - if (Helpers::isAjax() && session_status() === PHP_SESSION_ACTIVE) { |
|
80 | - ob_start(function () {}); |
|
81 | - $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/content.phtml'); |
|
82 | - $contentId = $_SERVER['HTTP_X_TRACY_AJAX']; |
|
83 | - $_SESSION['_tracy']['bluescreen'][$contentId] = ['content' => ob_get_clean(), 'dumps' => Dumper::fetchLiveData(), 'time' => time()]; |
|
84 | - |
|
85 | - } else { |
|
86 | - $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/page.phtml'); |
|
87 | - } |
|
88 | - } |
|
89 | - |
|
90 | - |
|
91 | - /** |
|
92 | - * Renders blue screen to file (if file exists, it will not be overwritten). |
|
93 | - * @param \Exception|\Throwable $exception |
|
94 | - * @param string $file file path |
|
95 | - * @return void |
|
96 | - */ |
|
97 | - public function renderToFile($exception, $file) |
|
98 | - { |
|
99 | - if ($handle = @fopen($file, 'x')) { |
|
100 | - ob_start(); // double buffer prevents sending HTTP headers in some PHP |
|
101 | - ob_start(function ($buffer) use ($handle) { fwrite($handle, $buffer); }, 4096); |
|
102 | - $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/page.phtml', false); |
|
103 | - ob_end_flush(); |
|
104 | - ob_end_clean(); |
|
105 | - fclose($handle); |
|
106 | - } |
|
107 | - } |
|
108 | - |
|
109 | - |
|
110 | - private function renderTemplate($exception, $template, $toScreen = true) |
|
111 | - { |
|
112 | - $messageHtml = preg_replace( |
|
113 | - '#\'\S[^\']*\S\'|"\S[^"]*\S"#U', |
|
114 | - '<i>$0</i>', |
|
115 | - htmlspecialchars((string) $exception->getMessage(), ENT_SUBSTITUTE, 'UTF-8') |
|
116 | - ); |
|
117 | - $info = array_filter($this->info); |
|
118 | - $source = Helpers::getSource(); |
|
119 | - $sourceIsUrl = preg_match('#^https?://#', $source); |
|
120 | - $title = $exception instanceof \ErrorException |
|
121 | - ? Helpers::errorTypeToString($exception->getSeverity()) |
|
122 | - : Helpers::getClass($exception); |
|
123 | - $lastError = $exception instanceof \ErrorException || $exception instanceof \Error ? null : error_get_last(); |
|
124 | - |
|
125 | - $keysToHide = array_flip(array_map('strtolower', $this->keysToHide)); |
|
126 | - $dump = function ($v, $k = null) use ($keysToHide) { |
|
127 | - if (is_string($k) && isset($keysToHide[strtolower($k)])) { |
|
128 | - $v = Dumper::HIDDEN_VALUE; |
|
129 | - } |
|
130 | - return Dumper::toHtml($v, [ |
|
131 | - Dumper::DEPTH => $this->maxDepth, |
|
132 | - Dumper::TRUNCATE => $this->maxLength, |
|
133 | - Dumper::LIVE => true, |
|
134 | - Dumper::LOCATION => Dumper::LOCATION_CLASS, |
|
135 | - Dumper::KEYS_TO_HIDE => $this->keysToHide, |
|
136 | - ]); |
|
137 | - }; |
|
138 | - $css = array_map('file_get_contents', array_merge([ |
|
139 | - __DIR__ . '/assets/BlueScreen/bluescreen.css', |
|
140 | - ], Debugger::$customCssFiles)); |
|
141 | - $css = preg_replace('#\s+#u', ' ', implode($css)); |
|
142 | - |
|
143 | - $nonce = $toScreen ? Helpers::getNonce() : null; |
|
144 | - $actions = $toScreen ? $this->renderActions($exception) : []; |
|
145 | - |
|
146 | - require $template; |
|
147 | - } |
|
148 | - |
|
149 | - |
|
150 | - /** |
|
151 | - * @return \stdClass[] |
|
152 | - */ |
|
153 | - private function renderPanels($ex) |
|
154 | - { |
|
155 | - $obLevel = ob_get_level(); |
|
156 | - $res = []; |
|
157 | - foreach ($this->panels as $callback) { |
|
158 | - try { |
|
159 | - $panel = call_user_func($callback, $ex); |
|
160 | - if (empty($panel['tab']) || empty($panel['panel'])) { |
|
161 | - continue; |
|
162 | - } |
|
163 | - $res[] = (object) $panel; |
|
164 | - continue; |
|
165 | - } catch (\Exception $e) { |
|
166 | - } catch (\Throwable $e) { |
|
167 | - } |
|
168 | - while (ob_get_level() > $obLevel) { // restore ob-level if broken |
|
169 | - ob_end_clean(); |
|
170 | - } |
|
171 | - is_callable($callback, true, $name); |
|
172 | - $res[] = (object) [ |
|
173 | - 'tab' => "Error in panel $name", |
|
174 | - 'panel' => nl2br(Helpers::escapeHtml($e)), |
|
175 | - ]; |
|
176 | - } |
|
177 | - return $res; |
|
178 | - } |
|
179 | - |
|
180 | - |
|
181 | - /** |
|
182 | - * @return array[] |
|
183 | - */ |
|
184 | - private function renderActions($ex) |
|
185 | - { |
|
186 | - $actions = []; |
|
187 | - foreach ($this->actions as $callback) { |
|
188 | - $action = call_user_func($callback, $ex); |
|
189 | - if (!empty($action['link']) && !empty($action['label'])) { |
|
190 | - $actions[] = $action; |
|
191 | - } |
|
192 | - } |
|
193 | - |
|
194 | - if (property_exists($ex, 'tracyAction') && !empty($ex->tracyAction['link']) && !empty($ex->tracyAction['label'])) { |
|
195 | - $actions[] = $ex->tracyAction; |
|
196 | - } |
|
197 | - |
|
198 | - if (preg_match('# ([\'"])(\w{3,}(?:\\\\\w{3,})+)\\1#i', $ex->getMessage(), $m)) { |
|
199 | - $class = $m[2]; |
|
200 | - if ( |
|
201 | - !class_exists($class) && !interface_exists($class) && !trait_exists($class) |
|
202 | - && ($file = Helpers::guessClassFile($class)) && !is_file($file) |
|
203 | - ) { |
|
204 | - $actions[] = [ |
|
205 | - 'link' => Helpers::editorUri($file, 1, 'create'), |
|
206 | - 'label' => 'create class', |
|
207 | - ]; |
|
208 | - } |
|
209 | - } |
|
210 | - |
|
211 | - if (preg_match('# ([\'"])((?:/|[a-z]:[/\\\\])\w[^\'"]+\.\w{2,5})\\1#i', $ex->getMessage(), $m)) { |
|
212 | - $file = $m[2]; |
|
213 | - $actions[] = [ |
|
214 | - 'link' => Helpers::editorUri($file, 1, $label = is_file($file) ? 'open' : 'create'), |
|
215 | - 'label' => $label . ' file', |
|
216 | - ]; |
|
217 | - } |
|
218 | - |
|
219 | - $query = ($ex instanceof \ErrorException ? '' : Helpers::getClass($ex) . ' ') |
|
220 | - . preg_replace('#\'.*\'|".*"#Us', '', $ex->getMessage()); |
|
221 | - $actions[] = [ |
|
222 | - 'link' => 'https://www.google.com/search?sourceid=tracy&q=' . urlencode($query), |
|
223 | - 'label' => 'search', |
|
224 | - 'external' => true, |
|
225 | - ]; |
|
226 | - |
|
227 | - if ( |
|
228 | - $ex instanceof \ErrorException |
|
229 | - && !empty($ex->skippable) |
|
230 | - && preg_match('#^https?://#', $source = Helpers::getSource()) |
|
231 | - ) { |
|
232 | - $actions[] = [ |
|
233 | - 'link' => $source . (strpos($source, '?') ? '&' : '?') . '_tracy_skip_error', |
|
234 | - 'label' => 'skip error', |
|
235 | - ]; |
|
236 | - } |
|
237 | - return $actions; |
|
238 | - } |
|
239 | - |
|
240 | - |
|
241 | - /** |
|
242 | - * Returns syntax highlighted source code. |
|
243 | - * @param string $file |
|
244 | - * @param int $line |
|
245 | - * @param int $lines |
|
246 | - * @return string|null |
|
247 | - */ |
|
248 | - public static function highlightFile($file, $line, $lines = 15, array $vars = null) |
|
249 | - { |
|
250 | - $source = @file_get_contents($file); // @ file may not exist |
|
251 | - if ($source) { |
|
252 | - $source = static::highlightPhp($source, $line, $lines, $vars); |
|
253 | - if ($editor = Helpers::editorUri($file, $line)) { |
|
254 | - $source = substr_replace($source, ' data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0); |
|
255 | - } |
|
256 | - return $source; |
|
257 | - } |
|
258 | - } |
|
259 | - |
|
260 | - |
|
261 | - /** |
|
262 | - * Returns syntax highlighted source code. |
|
263 | - * @param string $source |
|
264 | - * @param int $line |
|
265 | - * @param int $lines |
|
266 | - * @return string |
|
267 | - */ |
|
268 | - public static function highlightPhp($source, $line, $lines = 15, array $vars = null) |
|
269 | - { |
|
270 | - if (function_exists('ini_set')) { |
|
271 | - ini_set('highlight.comment', '#998; font-style: italic'); |
|
272 | - ini_set('highlight.default', '#000'); |
|
273 | - ini_set('highlight.html', '#06B'); |
|
274 | - ini_set('highlight.keyword', '#D24; font-weight: bold'); |
|
275 | - ini_set('highlight.string', '#080'); |
|
276 | - } |
|
277 | - |
|
278 | - $source = str_replace(["\r\n", "\r"], "\n", $source); |
|
279 | - $source = explode("\n", highlight_string($source, true)); |
|
280 | - $out = $source[0]; // <code><span color=highlight.html> |
|
281 | - $source = str_replace('<br />', "\n", $source[1]); |
|
282 | - $out .= static::highlightLine($source, $line, $lines); |
|
283 | - |
|
284 | - if ($vars) { |
|
285 | - $out = preg_replace_callback('#">\$(\w+)( )?</span>#', function ($m) use ($vars) { |
|
286 | - return array_key_exists($m[1], $vars) |
|
287 | - ? '" title="' |
|
288 | - . str_replace('"', '"', trim(strip_tags(Dumper::toHtml($vars[$m[1]], [Dumper::DEPTH => 1])))) |
|
289 | - . $m[0] |
|
290 | - : $m[0]; |
|
291 | - }, $out); |
|
292 | - } |
|
293 | - |
|
294 | - $out = str_replace(' ', ' ', $out); |
|
295 | - return "<pre class='code'><div>$out</div></pre>"; |
|
296 | - } |
|
297 | - |
|
298 | - |
|
299 | - /** |
|
300 | - * Returns highlighted line in HTML code. |
|
301 | - * @return string |
|
302 | - */ |
|
303 | - public static function highlightLine($html, $line, $lines = 15) |
|
304 | - { |
|
305 | - $source = explode("\n", "\n" . str_replace("\r\n", "\n", $html)); |
|
306 | - $out = ''; |
|
307 | - $spans = 1; |
|
308 | - $start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3)); |
|
309 | - while (--$i >= 1) { // find last highlighted block |
|
310 | - if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) { |
|
311 | - if ($m[1] !== '</span>') { |
|
312 | - $spans++; |
|
313 | - $out .= $m[1]; |
|
314 | - } |
|
315 | - break; |
|
316 | - } |
|
317 | - } |
|
318 | - |
|
319 | - $source = array_slice($source, $start, $lines, true); |
|
320 | - end($source); |
|
321 | - $numWidth = strlen((string) key($source)); |
|
322 | - |
|
323 | - foreach ($source as $n => $s) { |
|
324 | - $spans += substr_count($s, '<span') - substr_count($s, '</span'); |
|
325 | - $s = str_replace(["\r", "\n"], ['', ''], $s); |
|
326 | - preg_match_all('#<[^>]+>#', $s, $tags); |
|
327 | - if ($n == $line) { |
|
328 | - $out .= sprintf( |
|
329 | - "<span class='highlight'>%{$numWidth}s: %s\n</span>%s", |
|
330 | - $n, |
|
331 | - strip_tags($s), |
|
332 | - implode('', $tags[0]) |
|
333 | - ); |
|
334 | - } else { |
|
335 | - $out .= sprintf("<span class='line'>%{$numWidth}s:</span> %s\n", $n, $s); |
|
336 | - } |
|
337 | - } |
|
338 | - $out .= str_repeat('</span>', $spans) . '</code>'; |
|
339 | - return $out; |
|
340 | - } |
|
341 | - |
|
342 | - |
|
343 | - /** |
|
344 | - * Should a file be collapsed in stack trace? |
|
345 | - * @param string $file |
|
346 | - * @return bool |
|
347 | - */ |
|
348 | - public function isCollapsed($file) |
|
349 | - { |
|
350 | - $file = strtr($file, '\\', '/') . '/'; |
|
351 | - foreach ($this->collapsePaths as $path) { |
|
352 | - $path = strtr($path, '\\', '/') . '/'; |
|
353 | - if (strncmp($file, $path, strlen($path)) === 0) { |
|
354 | - return true; |
|
355 | - } |
|
356 | - } |
|
357 | - return false; |
|
358 | - } |
|
16 | + /** @var string[] */ |
|
17 | + public $info = []; |
|
18 | + |
|
19 | + /** @var string[] paths to be collapsed in stack trace (e.g. core libraries) */ |
|
20 | + public $collapsePaths = []; |
|
21 | + |
|
22 | + /** @var int */ |
|
23 | + public $maxDepth = 3; |
|
24 | + |
|
25 | + /** @var int */ |
|
26 | + public $maxLength = 150; |
|
27 | + |
|
28 | + /** @var string[] */ |
|
29 | + public $keysToHide = ['password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin']; |
|
30 | + |
|
31 | + /** @var callable[] */ |
|
32 | + private $panels = []; |
|
33 | + |
|
34 | + /** @var callable[] functions that returns action for exceptions */ |
|
35 | + private $actions = []; |
|
36 | + |
|
37 | + |
|
38 | + public function __construct() |
|
39 | + { |
|
40 | + $this->collapsePaths[] = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy$#', strtr(__DIR__, '\\', '/'), $m) |
|
41 | + ? $m[1] |
|
42 | + : __DIR__; |
|
43 | + } |
|
44 | + |
|
45 | + |
|
46 | + /** |
|
47 | + * Add custom panel. |
|
48 | + * @param callable $panel |
|
49 | + * @return static |
|
50 | + */ |
|
51 | + public function addPanel($panel) |
|
52 | + { |
|
53 | + if (!in_array($panel, $this->panels, true)) { |
|
54 | + $this->panels[] = $panel; |
|
55 | + } |
|
56 | + return $this; |
|
57 | + } |
|
58 | + |
|
59 | + |
|
60 | + /** |
|
61 | + * Add action. |
|
62 | + * @param callable $action |
|
63 | + * @return static |
|
64 | + */ |
|
65 | + public function addAction($action) |
|
66 | + { |
|
67 | + $this->actions[] = $action; |
|
68 | + return $this; |
|
69 | + } |
|
70 | + |
|
71 | + |
|
72 | + /** |
|
73 | + * Renders blue screen. |
|
74 | + * @param \Exception|\Throwable $exception |
|
75 | + * @return void |
|
76 | + */ |
|
77 | + public function render($exception) |
|
78 | + { |
|
79 | + if (Helpers::isAjax() && session_status() === PHP_SESSION_ACTIVE) { |
|
80 | + ob_start(function () {}); |
|
81 | + $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/content.phtml'); |
|
82 | + $contentId = $_SERVER['HTTP_X_TRACY_AJAX']; |
|
83 | + $_SESSION['_tracy']['bluescreen'][$contentId] = ['content' => ob_get_clean(), 'dumps' => Dumper::fetchLiveData(), 'time' => time()]; |
|
84 | + |
|
85 | + } else { |
|
86 | + $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/page.phtml'); |
|
87 | + } |
|
88 | + } |
|
89 | + |
|
90 | + |
|
91 | + /** |
|
92 | + * Renders blue screen to file (if file exists, it will not be overwritten). |
|
93 | + * @param \Exception|\Throwable $exception |
|
94 | + * @param string $file file path |
|
95 | + * @return void |
|
96 | + */ |
|
97 | + public function renderToFile($exception, $file) |
|
98 | + { |
|
99 | + if ($handle = @fopen($file, 'x')) { |
|
100 | + ob_start(); // double buffer prevents sending HTTP headers in some PHP |
|
101 | + ob_start(function ($buffer) use ($handle) { fwrite($handle, $buffer); }, 4096); |
|
102 | + $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/page.phtml', false); |
|
103 | + ob_end_flush(); |
|
104 | + ob_end_clean(); |
|
105 | + fclose($handle); |
|
106 | + } |
|
107 | + } |
|
108 | + |
|
109 | + |
|
110 | + private function renderTemplate($exception, $template, $toScreen = true) |
|
111 | + { |
|
112 | + $messageHtml = preg_replace( |
|
113 | + '#\'\S[^\']*\S\'|"\S[^"]*\S"#U', |
|
114 | + '<i>$0</i>', |
|
115 | + htmlspecialchars((string) $exception->getMessage(), ENT_SUBSTITUTE, 'UTF-8') |
|
116 | + ); |
|
117 | + $info = array_filter($this->info); |
|
118 | + $source = Helpers::getSource(); |
|
119 | + $sourceIsUrl = preg_match('#^https?://#', $source); |
|
120 | + $title = $exception instanceof \ErrorException |
|
121 | + ? Helpers::errorTypeToString($exception->getSeverity()) |
|
122 | + : Helpers::getClass($exception); |
|
123 | + $lastError = $exception instanceof \ErrorException || $exception instanceof \Error ? null : error_get_last(); |
|
124 | + |
|
125 | + $keysToHide = array_flip(array_map('strtolower', $this->keysToHide)); |
|
126 | + $dump = function ($v, $k = null) use ($keysToHide) { |
|
127 | + if (is_string($k) && isset($keysToHide[strtolower($k)])) { |
|
128 | + $v = Dumper::HIDDEN_VALUE; |
|
129 | + } |
|
130 | + return Dumper::toHtml($v, [ |
|
131 | + Dumper::DEPTH => $this->maxDepth, |
|
132 | + Dumper::TRUNCATE => $this->maxLength, |
|
133 | + Dumper::LIVE => true, |
|
134 | + Dumper::LOCATION => Dumper::LOCATION_CLASS, |
|
135 | + Dumper::KEYS_TO_HIDE => $this->keysToHide, |
|
136 | + ]); |
|
137 | + }; |
|
138 | + $css = array_map('file_get_contents', array_merge([ |
|
139 | + __DIR__ . '/assets/BlueScreen/bluescreen.css', |
|
140 | + ], Debugger::$customCssFiles)); |
|
141 | + $css = preg_replace('#\s+#u', ' ', implode($css)); |
|
142 | + |
|
143 | + $nonce = $toScreen ? Helpers::getNonce() : null; |
|
144 | + $actions = $toScreen ? $this->renderActions($exception) : []; |
|
145 | + |
|
146 | + require $template; |
|
147 | + } |
|
148 | + |
|
149 | + |
|
150 | + /** |
|
151 | + * @return \stdClass[] |
|
152 | + */ |
|
153 | + private function renderPanels($ex) |
|
154 | + { |
|
155 | + $obLevel = ob_get_level(); |
|
156 | + $res = []; |
|
157 | + foreach ($this->panels as $callback) { |
|
158 | + try { |
|
159 | + $panel = call_user_func($callback, $ex); |
|
160 | + if (empty($panel['tab']) || empty($panel['panel'])) { |
|
161 | + continue; |
|
162 | + } |
|
163 | + $res[] = (object) $panel; |
|
164 | + continue; |
|
165 | + } catch (\Exception $e) { |
|
166 | + } catch (\Throwable $e) { |
|
167 | + } |
|
168 | + while (ob_get_level() > $obLevel) { // restore ob-level if broken |
|
169 | + ob_end_clean(); |
|
170 | + } |
|
171 | + is_callable($callback, true, $name); |
|
172 | + $res[] = (object) [ |
|
173 | + 'tab' => "Error in panel $name", |
|
174 | + 'panel' => nl2br(Helpers::escapeHtml($e)), |
|
175 | + ]; |
|
176 | + } |
|
177 | + return $res; |
|
178 | + } |
|
179 | + |
|
180 | + |
|
181 | + /** |
|
182 | + * @return array[] |
|
183 | + */ |
|
184 | + private function renderActions($ex) |
|
185 | + { |
|
186 | + $actions = []; |
|
187 | + foreach ($this->actions as $callback) { |
|
188 | + $action = call_user_func($callback, $ex); |
|
189 | + if (!empty($action['link']) && !empty($action['label'])) { |
|
190 | + $actions[] = $action; |
|
191 | + } |
|
192 | + } |
|
193 | + |
|
194 | + if (property_exists($ex, 'tracyAction') && !empty($ex->tracyAction['link']) && !empty($ex->tracyAction['label'])) { |
|
195 | + $actions[] = $ex->tracyAction; |
|
196 | + } |
|
197 | + |
|
198 | + if (preg_match('# ([\'"])(\w{3,}(?:\\\\\w{3,})+)\\1#i', $ex->getMessage(), $m)) { |
|
199 | + $class = $m[2]; |
|
200 | + if ( |
|
201 | + !class_exists($class) && !interface_exists($class) && !trait_exists($class) |
|
202 | + && ($file = Helpers::guessClassFile($class)) && !is_file($file) |
|
203 | + ) { |
|
204 | + $actions[] = [ |
|
205 | + 'link' => Helpers::editorUri($file, 1, 'create'), |
|
206 | + 'label' => 'create class', |
|
207 | + ]; |
|
208 | + } |
|
209 | + } |
|
210 | + |
|
211 | + if (preg_match('# ([\'"])((?:/|[a-z]:[/\\\\])\w[^\'"]+\.\w{2,5})\\1#i', $ex->getMessage(), $m)) { |
|
212 | + $file = $m[2]; |
|
213 | + $actions[] = [ |
|
214 | + 'link' => Helpers::editorUri($file, 1, $label = is_file($file) ? 'open' : 'create'), |
|
215 | + 'label' => $label . ' file', |
|
216 | + ]; |
|
217 | + } |
|
218 | + |
|
219 | + $query = ($ex instanceof \ErrorException ? '' : Helpers::getClass($ex) . ' ') |
|
220 | + . preg_replace('#\'.*\'|".*"#Us', '', $ex->getMessage()); |
|
221 | + $actions[] = [ |
|
222 | + 'link' => 'https://www.google.com/search?sourceid=tracy&q=' . urlencode($query), |
|
223 | + 'label' => 'search', |
|
224 | + 'external' => true, |
|
225 | + ]; |
|
226 | + |
|
227 | + if ( |
|
228 | + $ex instanceof \ErrorException |
|
229 | + && !empty($ex->skippable) |
|
230 | + && preg_match('#^https?://#', $source = Helpers::getSource()) |
|
231 | + ) { |
|
232 | + $actions[] = [ |
|
233 | + 'link' => $source . (strpos($source, '?') ? '&' : '?') . '_tracy_skip_error', |
|
234 | + 'label' => 'skip error', |
|
235 | + ]; |
|
236 | + } |
|
237 | + return $actions; |
|
238 | + } |
|
239 | + |
|
240 | + |
|
241 | + /** |
|
242 | + * Returns syntax highlighted source code. |
|
243 | + * @param string $file |
|
244 | + * @param int $line |
|
245 | + * @param int $lines |
|
246 | + * @return string|null |
|
247 | + */ |
|
248 | + public static function highlightFile($file, $line, $lines = 15, array $vars = null) |
|
249 | + { |
|
250 | + $source = @file_get_contents($file); // @ file may not exist |
|
251 | + if ($source) { |
|
252 | + $source = static::highlightPhp($source, $line, $lines, $vars); |
|
253 | + if ($editor = Helpers::editorUri($file, $line)) { |
|
254 | + $source = substr_replace($source, ' data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0); |
|
255 | + } |
|
256 | + return $source; |
|
257 | + } |
|
258 | + } |
|
259 | + |
|
260 | + |
|
261 | + /** |
|
262 | + * Returns syntax highlighted source code. |
|
263 | + * @param string $source |
|
264 | + * @param int $line |
|
265 | + * @param int $lines |
|
266 | + * @return string |
|
267 | + */ |
|
268 | + public static function highlightPhp($source, $line, $lines = 15, array $vars = null) |
|
269 | + { |
|
270 | + if (function_exists('ini_set')) { |
|
271 | + ini_set('highlight.comment', '#998; font-style: italic'); |
|
272 | + ini_set('highlight.default', '#000'); |
|
273 | + ini_set('highlight.html', '#06B'); |
|
274 | + ini_set('highlight.keyword', '#D24; font-weight: bold'); |
|
275 | + ini_set('highlight.string', '#080'); |
|
276 | + } |
|
277 | + |
|
278 | + $source = str_replace(["\r\n", "\r"], "\n", $source); |
|
279 | + $source = explode("\n", highlight_string($source, true)); |
|
280 | + $out = $source[0]; // <code><span color=highlight.html> |
|
281 | + $source = str_replace('<br />', "\n", $source[1]); |
|
282 | + $out .= static::highlightLine($source, $line, $lines); |
|
283 | + |
|
284 | + if ($vars) { |
|
285 | + $out = preg_replace_callback('#">\$(\w+)( )?</span>#', function ($m) use ($vars) { |
|
286 | + return array_key_exists($m[1], $vars) |
|
287 | + ? '" title="' |
|
288 | + . str_replace('"', '"', trim(strip_tags(Dumper::toHtml($vars[$m[1]], [Dumper::DEPTH => 1])))) |
|
289 | + . $m[0] |
|
290 | + : $m[0]; |
|
291 | + }, $out); |
|
292 | + } |
|
293 | + |
|
294 | + $out = str_replace(' ', ' ', $out); |
|
295 | + return "<pre class='code'><div>$out</div></pre>"; |
|
296 | + } |
|
297 | + |
|
298 | + |
|
299 | + /** |
|
300 | + * Returns highlighted line in HTML code. |
|
301 | + * @return string |
|
302 | + */ |
|
303 | + public static function highlightLine($html, $line, $lines = 15) |
|
304 | + { |
|
305 | + $source = explode("\n", "\n" . str_replace("\r\n", "\n", $html)); |
|
306 | + $out = ''; |
|
307 | + $spans = 1; |
|
308 | + $start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3)); |
|
309 | + while (--$i >= 1) { // find last highlighted block |
|
310 | + if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) { |
|
311 | + if ($m[1] !== '</span>') { |
|
312 | + $spans++; |
|
313 | + $out .= $m[1]; |
|
314 | + } |
|
315 | + break; |
|
316 | + } |
|
317 | + } |
|
318 | + |
|
319 | + $source = array_slice($source, $start, $lines, true); |
|
320 | + end($source); |
|
321 | + $numWidth = strlen((string) key($source)); |
|
322 | + |
|
323 | + foreach ($source as $n => $s) { |
|
324 | + $spans += substr_count($s, '<span') - substr_count($s, '</span'); |
|
325 | + $s = str_replace(["\r", "\n"], ['', ''], $s); |
|
326 | + preg_match_all('#<[^>]+>#', $s, $tags); |
|
327 | + if ($n == $line) { |
|
328 | + $out .= sprintf( |
|
329 | + "<span class='highlight'>%{$numWidth}s: %s\n</span>%s", |
|
330 | + $n, |
|
331 | + strip_tags($s), |
|
332 | + implode('', $tags[0]) |
|
333 | + ); |
|
334 | + } else { |
|
335 | + $out .= sprintf("<span class='line'>%{$numWidth}s:</span> %s\n", $n, $s); |
|
336 | + } |
|
337 | + } |
|
338 | + $out .= str_repeat('</span>', $spans) . '</code>'; |
|
339 | + return $out; |
|
340 | + } |
|
341 | + |
|
342 | + |
|
343 | + /** |
|
344 | + * Should a file be collapsed in stack trace? |
|
345 | + * @param string $file |
|
346 | + * @return bool |
|
347 | + */ |
|
348 | + public function isCollapsed($file) |
|
349 | + { |
|
350 | + $file = strtr($file, '\\', '/') . '/'; |
|
351 | + foreach ($this->collapsePaths as $path) { |
|
352 | + $path = strtr($path, '\\', '/') . '/'; |
|
353 | + if (strncmp($file, $path, strlen($path)) === 0) { |
|
354 | + return true; |
|
355 | + } |
|
356 | + } |
|
357 | + return false; |
|
358 | + } |
|
359 | 359 | } |
@@ -13,13 +13,13 @@ |
||
13 | 13 | */ |
14 | 14 | interface ILogger |
15 | 15 | { |
16 | - const |
|
17 | - DEBUG = 'debug', |
|
18 | - INFO = 'info', |
|
19 | - WARNING = 'warning', |
|
20 | - ERROR = 'error', |
|
21 | - EXCEPTION = 'exception', |
|
22 | - CRITICAL = 'critical'; |
|
16 | + const |
|
17 | + DEBUG = 'debug', |
|
18 | + INFO = 'info', |
|
19 | + WARNING = 'warning', |
|
20 | + ERROR = 'error', |
|
21 | + EXCEPTION = 'exception', |
|
22 | + CRITICAL = 'critical'; |
|
23 | 23 | |
24 | - function log($value, $priority = self::INFO); |
|
24 | + function log($value, $priority = self::INFO); |
|
25 | 25 | } |