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:
114: require $template;
115: }
116:
117:
118: 119: 120:
121: private function renderPanels($ex)
122: {
123: $obLevel = ob_get_level();
124: $res = [];
125: foreach ($this->panels as $callback) {
126: try {
127: $panel = call_user_func($callback, $ex);
128: if (empty($panel['tab']) || empty($panel['panel'])) {
129: continue;
130: }
131: $res[] = (object) $panel;
132: continue;
133: } catch (\Exception $e) {
134: } catch (\Throwable $e) {
135: }
136: while (ob_get_level() > $obLevel) {
137: ob_end_clean();
138: }
139: is_callable($callback, true, $name);
140: $res[] = (object) [
141: 'tab' => "Error in panel $name",
142: 'panel' => nl2br(Helpers::escapeHtml($e)),
143: ];
144: }
145: return $res;
146: }
147:
148:
149: 150: 151: 152: 153: 154: 155:
156: public static function highlightFile($file, $line, $lines = 15, array $vars = null)
157: {
158: $source = @file_get_contents($file);
159: if ($source) {
160: $source = static::highlightPhp($source, $line, $lines, $vars);
161: if ($editor = Helpers::editorUri($file, $line)) {
162: $source = substr_replace($source, ' data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0);
163: }
164: return $source;
165: }
166: }
167:
168:
169: 170: 171: 172: 173: 174: 175:
176: public static function highlightPhp($source, $line, $lines = 15, array $vars = null)
177: {
178: if (function_exists('ini_set')) {
179: ini_set('highlight.comment', '#998; font-style: italic');
180: ini_set('highlight.default', '#000');
181: ini_set('highlight.html', '#06B');
182: ini_set('highlight.keyword', '#D24; font-weight: bold');
183: ini_set('highlight.string', '#080');
184: }
185:
186: $source = str_replace(["\r\n", "\r"], "\n", $source);
187: $source = explode("\n", highlight_string($source, true));
188: $out = $source[0];
189: $source = str_replace('<br />', "\n", $source[1]);
190: $out .= static::highlightLine($source, $line, $lines);
191:
192: if ($vars) {
193: $out = preg_replace_callback('#">\$(\w+)( )?</span>#', function ($m) use ($vars) {
194: return array_key_exists($m[1], $vars)
195: ? '" title="'
196: . str_replace('"', '"', trim(strip_tags(Dumper::toHtml($vars[$m[1]], [Dumper::DEPTH => 1]))))
197: . $m[0]
198: : $m[0];
199: }, $out);
200: }
201:
202: $out = str_replace(' ', ' ', $out);
203: return "<pre class='code'><div>$out</div></pre>";
204: }
205:
206:
207: 208: 209: 210:
211: public static function highlightLine($html, $line, $lines = 15)
212: {
213: $source = explode("\n", "\n" . str_replace("\r\n", "\n", $html));
214: $out = '';
215: $spans = 1;
216: $start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3));
217: while (--$i >= 1) {
218: if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) {
219: if ($m[1] !== '</span>') {
220: $spans++;
221: $out .= $m[1];
222: }
223: break;
224: }
225: }
226:
227: $source = array_slice($source, $start, $lines, true);
228: end($source);
229: $numWidth = strlen((string) key($source));
230:
231: foreach ($source as $n => $s) {
232: $spans += substr_count($s, '<span') - substr_count($s, '</span');
233: $s = str_replace(["\r", "\n"], ['', ''], $s);
234: preg_match_all('#<[^>]+>#', $s, $tags);
235: if ($n == $line) {
236: $out .= sprintf(
237: "<span class='highlight'>%{$numWidth}s: %s\n</span>%s",
238: $n,
239: strip_tags($s),
240: implode('', $tags[0])
241: );
242: } else {
243: $out .= sprintf("<span class='line'>%{$numWidth}s:</span> %s\n", $n, $s);
244: }
245: }
246: $out .= str_repeat('</span>', $spans) . '</code>';
247: return $out;
248: }
249:
250:
251: 252: 253: 254: 255:
256: public function isCollapsed($file)
257: {
258: $file = strtr($file, '\\', '/') . '/';
259: foreach ($this->collapsePaths as $path) {
260: $path = strtr($path, '\\', '/') . '/';
261: if (strncmp($file, $path, strlen($path)) === 0) {
262: return true;
263: }
264: }
265: return false;
266: }
267: }
268: