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: private $panels = [];
30:
31:
32: public function __construct()
33: {
34: $this->collapsePaths[] = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy$#', strtr(__DIR__, '\\', '/'), $m)
35: ? $m[1]
36: : __DIR__;
37: }
38:
39:
40: 41: 42: 43: 44:
45: public function addPanel($panel)
46: {
47: if (!in_array($panel, $this->panels, true)) {
48: $this->panels[] = $panel;
49: }
50: return $this;
51: }
52:
53:
54: 55: 56: 57: 58:
59: public function render($exception)
60: {
61: if (Helpers::isAjax() && session_status() === PHP_SESSION_ACTIVE) {
62: ob_start(function () {});
63: $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/content.phtml');
64: $contentId = $_SERVER['HTTP_X_TRACY_AJAX'];
65: $_SESSION['_tracy']['bluescreen'][$contentId] = ['content' => ob_get_clean(), 'dumps' => Dumper::fetchLiveData(), 'time' => time()];
66:
67: } else {
68: $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/page.phtml');
69: }
70: }
71:
72:
73: 74: 75: 76: 77: 78:
79: public function renderToFile($exception, $file)
80: {
81: if ($handle = @fopen($file, 'x')) {
82: ob_start();
83: ob_start(function ($buffer) use ($handle) { fwrite($handle, $buffer); }, 4096);
84: $this->renderTemplate($exception, __DIR__ . '/assets/BlueScreen/page.phtml');
85: ob_end_flush();
86: ob_end_clean();
87: fclose($handle);
88: }
89: }
90:
91:
92: private function renderTemplate($exception, $template)
93: {
94: $info = array_filter($this->info);
95: $source = Helpers::getSource();
96: $sourceIsUrl = preg_match('#^https?://#', $source);
97: $title = $exception instanceof \ErrorException
98: ? Helpers::errorTypeToString($exception->getSeverity())
99: : Helpers::getClass($exception);
100: $skipError = $sourceIsUrl && $exception instanceof \ErrorException && !empty($exception->skippable)
101: ? $source . (strpos($source, '?') ? '&' : '?') . '_tracy_skip_error'
102: : null;
103: $lastError = $exception instanceof \ErrorException || $exception instanceof \Error ? null : error_get_last();
104: $dump = function ($v) {
105: return Dumper::toHtml($v, [
106: Dumper::DEPTH => $this->maxDepth,
107: Dumper::TRUNCATE => $this->maxLength,
108: Dumper::LIVE => true,
109: Dumper::LOCATION => Dumper::LOCATION_CLASS,
110: ]);
111: };
112: $nonce = Helpers::getNonce();
113: $css = array_map('file_get_contents', array_merge([
114: __DIR__ . '/assets/BlueScreen/bluescreen.css',
115: ], Debugger::$customCssFiles));
116: $css = preg_replace('#\s+#u', ' ', implode($css));
117:
118: require $template;
119: }
120:
121:
122: 123: 124:
125: private function renderPanels($ex)
126: {
127: $obLevel = ob_get_level();
128: $res = [];
129: foreach ($this->panels as $callback) {
130: try {
131: $panel = call_user_func($callback, $ex);
132: if (empty($panel['tab']) || empty($panel['panel'])) {
133: continue;
134: }
135: $res[] = (object) $panel;
136: continue;
137: } catch (\Exception $e) {
138: } catch (\Throwable $e) {
139: }
140: while (ob_get_level() > $obLevel) {
141: ob_end_clean();
142: }
143: is_callable($callback, true, $name);
144: $res[] = (object) [
145: 'tab' => "Error in panel $name",
146: 'panel' => nl2br(Helpers::escapeHtml($e)),
147: ];
148: }
149: return $res;
150: }
151:
152:
153: 154: 155: 156: 157: 158: 159:
160: public static function highlightFile($file, $line, $lines = 15, array $vars = null)
161: {
162: $source = @file_get_contents($file);
163: if ($source) {
164: $source = static::highlightPhp($source, $line, $lines, $vars);
165: if ($editor = Helpers::editorUri($file, $line)) {
166: $source = substr_replace($source, ' data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0);
167: }
168: return $source;
169: }
170: }
171:
172:
173: 174: 175: 176: 177: 178: 179:
180: public static function highlightPhp($source, $line, $lines = 15, array $vars = null)
181: {
182: if (function_exists('ini_set')) {
183: ini_set('highlight.comment', '#998; font-style: italic');
184: ini_set('highlight.default', '#000');
185: ini_set('highlight.html', '#06B');
186: ini_set('highlight.keyword', '#D24; font-weight: bold');
187: ini_set('highlight.string', '#080');
188: }
189:
190: $source = str_replace(["\r\n", "\r"], "\n", $source);
191: $source = explode("\n", highlight_string($source, true));
192: $out = $source[0];
193: $source = str_replace('<br />', "\n", $source[1]);
194: $out .= static::highlightLine($source, $line, $lines);
195:
196: if ($vars) {
197: $out = preg_replace_callback('#">\$(\w+)( )?</span>#', function ($m) use ($vars) {
198: return array_key_exists($m[1], $vars)
199: ? '" title="'
200: . str_replace('"', '"', trim(strip_tags(Dumper::toHtml($vars[$m[1]], [Dumper::DEPTH => 1]))))
201: . $m[0]
202: : $m[0];
203: }, $out);
204: }
205:
206: $out = str_replace(' ', ' ', $out);
207: return "<pre class='code'><div>$out</div></pre>";
208: }
209:
210:
211: 212: 213: 214:
215: public static function highlightLine($html, $line, $lines = 15)
216: {
217: $source = explode("\n", "\n" . str_replace("\r\n", "\n", $html));
218: $out = '';
219: $spans = 1;
220: $start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3));
221: while (--$i >= 1) {
222: if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) {
223: if ($m[1] !== '</span>') {
224: $spans++;
225: $out .= $m[1];
226: }
227: break;
228: }
229: }
230:
231: $source = array_slice($source, $start, $lines, true);
232: end($source);
233: $numWidth = strlen((string) key($source));
234:
235: foreach ($source as $n => $s) {
236: $spans += substr_count($s, '<span') - substr_count($s, '</span');
237: $s = str_replace(["\r", "\n"], ['', ''], $s);
238: preg_match_all('#<[^>]+>#', $s, $tags);
239: if ($n == $line) {
240: $out .= sprintf(
241: "<span class='highlight'>%{$numWidth}s: %s\n</span>%s",
242: $n,
243: strip_tags($s),
244: implode('', $tags[0])
245: );
246: } else {
247: $out .= sprintf("<span class='line'>%{$numWidth}s:</span> %s\n", $n, $s);
248: }
249: }
250: $out .= str_repeat('</span>', $spans) . '</code>';
251: return $out;
252: }
253:
254:
255: 256: 257: 258: 259:
260: public function isCollapsed($file)
261: {
262: $file = strtr($file, '\\', '/') . '/';
263: foreach ($this->collapsePaths as $path) {
264: $path = strtr($path, '\\', '/') . '/';
265: if (strncmp($file, $path, strlen($path)) === 0) {
266: return true;
267: }
268: }
269: return false;
270: }
271: }
272: