1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tracy;
9:
10:
11: 12: 13:
14: class BlueScreen
15: {
16:
17: public $info = [];
18:
19:
20: public $collapsePaths = [];
21:
22:
23: public $maxDepth = 3;
24:
25:
26: public $maxLength = 150;
27:
28:
29: public $keysToHide = ['password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin'];
30:
31:
32: private $panels = [];
33:
34:
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: 48: 49: 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: 62: 63: 64:
65: public function addAction($action)
66: {
67: $this->actions[] = $action;
68: return $this;
69: }
70:
71:
72: 73: 74: 75: 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: 93: 94: 95: 96:
97: public function renderToFile($exception, $file)
98: {
99: if ($handle = @fopen($file, 'x')) {
100: ob_start();
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: 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) {
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: 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: 243: 244: 245: 246: 247:
248: public static function highlightFile($file, $line, $lines = 15, array $vars = null)
249: {
250: $source = @file_get_contents($file);
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: 263: 264: 265: 266: 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];
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: 301: 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) {
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: 345: 346: 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: }
360: