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