1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tracy;
9:
10: use Tracy;
11: use ErrorException;
12:
13:
14: 15: 16:
17: class Debugger
18: {
19: const VERSION = '2.4.3';
20:
21:
22: const
23: DEVELOPMENT = FALSE,
24: PRODUCTION = TRUE,
25: DETECT = NULL;
26:
27: const COOKIE_SECRET = 'tracy-debug';
28:
29:
30: public static $productionMode = self::DETECT;
31:
32:
33: public static $showBar = TRUE;
34:
35:
36: private static $enabled = FALSE;
37:
38:
39: private static $reserved;
40:
41:
42: private static $obLevel;
43:
44:
45:
46:
47: public static $strictMode = FALSE;
48:
49:
50: public static $scream = FALSE;
51:
52:
53: public static $onFatalError = [];
54:
55:
56:
57:
58: public static $maxDepth = 3;
59:
60:
61: public static $maxLength = 150;
62:
63:
64: public static $showLocation = FALSE;
65:
66:
67: public static $maxLen = 150;
68:
69:
70:
71:
72: public static $logDirectory;
73:
74:
75: public static $logSeverity = 0;
76:
77:
78: public static $email;
79:
80:
81: const
82: DEBUG = ILogger::DEBUG,
83: INFO = ILogger::INFO,
84: WARNING = ILogger::WARNING,
85: ERROR = ILogger::ERROR,
86: EXCEPTION = ILogger::EXCEPTION,
87: CRITICAL = ILogger::CRITICAL;
88:
89:
90:
91:
92: public static $time;
93:
94:
95: public static $editor = 'editor://open/?file=%file&line=%line';
96:
97:
98: public static $editorMapping = [];
99:
100:
101: public static $browser;
102:
103:
104: public static $errorTemplate;
105:
106:
107: private static $cpuUsage;
108:
109:
110:
111:
112: private static $blueScreen;
113:
114:
115: private static $bar;
116:
117:
118: private static $logger;
119:
120:
121: private static $fireLogger;
122:
123:
124: 125: 126:
127: final public function __construct()
128: {
129: throw new \LogicException;
130: }
131:
132:
133: 134: 135: 136: 137: 138: 139:
140: public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
141: {
142: if ($mode !== NULL || self::$productionMode === NULL) {
143: self::$productionMode = is_bool($mode) ? $mode : !self::detectDebugMode($mode);
144: }
145:
146: self::$maxLen = & self::$maxLength;
147: self::$reserved = str_repeat('t', 3e5);
148: self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
149: self::$obLevel = ob_get_level();
150: self::$cpuUsage = !self::$productionMode && function_exists('getrusage') ? getrusage() : NULL;
151:
152:
153: if ($email !== NULL) {
154: self::$email = $email;
155: }
156: if ($logDirectory !== NULL) {
157: self::$logDirectory = $logDirectory;
158: }
159: if (self::$logDirectory) {
160: if (!is_dir(self::$logDirectory) || !preg_match('#([a-z]+:)?[/\\\\]#Ai', self::$logDirectory)) {
161: self::$logDirectory = NULL;
162: self::exceptionHandler(new \RuntimeException('Logging directory not found or is not absolute path.'));
163: }
164: }
165:
166:
167: if (function_exists('ini_set')) {
168: ini_set('display_errors', !self::$productionMode);
169: ini_set('html_errors', FALSE);
170: ini_set('log_errors', FALSE);
171:
172: } elseif (ini_get('display_errors') != !self::$productionMode
173: && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')
174: ) {
175: self::exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
176: }
177: error_reporting(E_ALL);
178:
179: if (self::$enabled) {
180: return;
181: }
182: self::$enabled = TRUE;
183:
184: register_shutdown_function([__CLASS__, 'shutdownHandler']);
185: set_exception_handler([__CLASS__, 'exceptionHandler']);
186: set_error_handler([__CLASS__, 'errorHandler']);
187:
188: array_map('class_exists', ['Tracy\Bar', 'Tracy\BlueScreen', 'Tracy\DefaultBarPanel', 'Tracy\Dumper',
189: 'Tracy\FireLogger', 'Tracy\Helpers', 'Tracy\Logger']);
190:
191: if (self::$productionMode) {
192:
193: } elseif (headers_sent($file, $line) || ob_get_length()) {
194: throw new \LogicException(
195: __METHOD__ . '() called after some output has been sent. '
196: . ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.')
197: );
198:
199: } elseif (self::getBar()->dispatchAssets()) {
200: exit;
201:
202: } elseif (session_status() === PHP_SESSION_ACTIVE) {
203: self::dispatch();
204: }
205: }
206:
207:
208: 209: 210:
211: public static function dispatch()
212: {
213: if (self::$productionMode) {
214: return;
215:
216: } elseif (headers_sent($file, $line) || ob_get_length()) {
217: throw new \LogicException(
218: __METHOD__ . '() called after some output has been sent. '
219: . ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.')
220: );
221:
222: } elseif (session_status() !== PHP_SESSION_ACTIVE) {
223: ini_set('session.use_cookies', '1');
224: ini_set('session.use_only_cookies', '1');
225: ini_set('session.use_trans_sid', '0');
226: ini_set('session.cookie_path', '/');
227: ini_set('session.cookie_httponly', '1');
228: session_start();
229: }
230: if (self::getBar()->dispatchContent()) {
231: exit;
232: }
233: }
234:
235:
236: 237: 238:
239: public static function isEnabled()
240: {
241: return self::$enabled;
242: }
243:
244:
245: 246: 247: 248: 249:
250: public static function shutdownHandler()
251: {
252: if (!self::$reserved) {
253: return;
254: }
255:
256: $error = error_get_last();
257: if (in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], TRUE)) {
258: self::exceptionHandler(
259: Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])),
260: FALSE
261: );
262:
263: } elseif (self::$showBar && !self::$productionMode) {
264: self::$reserved = NULL;
265: self::removeOutputBuffers(FALSE);
266: self::getBar()->render();
267: }
268: }
269:
270:
271: 272: 273: 274: 275: 276:
277: public static function exceptionHandler($exception, $exit = TRUE)
278: {
279: if (!self::$reserved) {
280: return;
281: }
282: self::$reserved = NULL;
283:
284: if (!headers_sent()) {
285: $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
286: $code = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE ? 503 : 500;
287: header("$protocol $code", TRUE, $code);
288: if (Helpers::isHtmlMode()) {
289: header('Content-Type: text/html; charset=UTF-8');
290: }
291: }
292:
293: Helpers::improveException($exception);
294: self::removeOutputBuffers(TRUE);
295:
296: if (self::$productionMode) {
297: try {
298: self::log($exception, self::EXCEPTION);
299: } catch (\Throwable $e) {
300: } catch (\Exception $e) {
301: }
302:
303: if (Helpers::isHtmlMode()) {
304: $logged = empty($e);
305: require self::$errorTemplate ?: __DIR__ . '/assets/Debugger/error.500.phtml';
306: } elseif (PHP_SAPI === 'cli') {
307: fwrite(STDERR, 'ERROR: application encountered an error and can not continue. '
308: . (isset($e) ? "Unable to log error.\n" : "Error was logged.\n"));
309: }
310:
311: } elseif (!connection_aborted() && (Helpers::isHtmlMode() || Helpers::isAjax())) {
312: self::getBlueScreen()->render($exception);
313: if (self::$showBar) {
314: self::getBar()->render();
315: }
316:
317: } else {
318: self::fireLog($exception);
319: $s = get_class($exception) . ($exception->getMessage() === '' ? '' : ': ' . $exception->getMessage())
320: . ' in ' . $exception->getFile() . ':' . $exception->getLine()
321: . "\nStack trace:\n" . $exception->getTraceAsString();
322: try {
323: $file = self::log($exception, self::EXCEPTION);
324: if ($file && !headers_sent()) {
325: header("X-Tracy-Error-Log: $file");
326: }
327: echo "$s\n" . ($file ? "(stored in $file)\n" : '');
328: if ($file && self::$browser) {
329: exec(self::$browser . ' ' . escapeshellarg($file));
330: }
331: } catch (\Throwable $e) {
332: echo "$s\nUnable to log error: {$e->getMessage()}\n";
333: } catch (\Exception $e) {
334: echo "$s\nUnable to log error: {$e->getMessage()}\n";
335: }
336: }
337:
338: try {
339: $e = NULL;
340: foreach (self::$onFatalError as $handler) {
341: call_user_func($handler, $exception);
342: }
343: } catch (\Throwable $e) {
344: } catch (\Exception $e) {
345: }
346: if ($e) {
347: try {
348: self::log($e, self::EXCEPTION);
349: } catch (\Throwable $e) {
350: } catch (\Exception $e) {
351: }
352: }
353:
354: if ($exit) {
355: exit(255);
356: }
357: }
358:
359:
360: 361: 362: 363: 364: 365:
366: public static function errorHandler($severity, $message, $file, $line, $context)
367: {
368: if (self::$scream) {
369: error_reporting(E_ALL);
370: }
371:
372: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
373: if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) {
374: $previous = isset($context['e']) && ($context['e'] instanceof \Exception || $context['e'] instanceof \Throwable) ? $context['e'] : NULL;
375: $e = new ErrorException($message, 0, $severity, $file, $line, $previous);
376: $e->context = $context;
377: self::exceptionHandler($e);
378: }
379:
380: $e = new ErrorException($message, 0, $severity, $file, $line);
381: $e->context = $context;
382: throw $e;
383:
384: } elseif (($severity & error_reporting()) !== $severity) {
385: return FALSE;
386:
387: } elseif (self::$productionMode && ($severity & self::$logSeverity) === $severity) {
388: $e = new ErrorException($message, 0, $severity, $file, $line);
389: $e->context = $context;
390: Helpers::improveException($e);
391: try {
392: self::log($e, self::ERROR);
393: } catch (\Throwable $e) {
394: } catch (\Exception $foo) {
395: }
396: return NULL;
397:
398: } elseif (!self::$productionMode && !isset($_GET['_tracy_skip_error'])
399: && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))
400: ) {
401: $e = new ErrorException($message, 0, $severity, $file, $line);
402: $e->context = $context;
403: $e->skippable = TRUE;
404: self::exceptionHandler($e);
405: }
406:
407: $message = 'PHP ' . Helpers::errorTypeToString($severity) . ": $message";
408: $count = & self::getBar()->getPanel('Tracy:errors')->data["$file|$line|$message"];
409:
410: if ($count++) {
411: return NULL;
412:
413: } elseif (self::$productionMode) {
414: try {
415: self::log("$message in $file:$line", self::ERROR);
416: } catch (\Throwable $e) {
417: } catch (\Exception $foo) {
418: }
419: return NULL;
420:
421: } else {
422: self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
423: return Helpers::isHtmlMode() || Helpers::isAjax() ? NULL : FALSE;
424: }
425: }
426:
427:
428: private static function removeOutputBuffers($errorOccurred)
429: {
430: while (ob_get_level() > self::$obLevel) {
431: $status = ob_get_status();
432: if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'])) {
433: break;
434: }
435: $fnc = $status['chunk_size'] || !$errorOccurred ? 'ob_end_flush' : 'ob_end_clean';
436: if (!@$fnc()) {
437: break;
438: }
439: }
440: }
441:
442:
443:
444:
445:
446: 447: 448:
449: public static function getBlueScreen()
450: {
451: if (!self::$blueScreen) {
452: self::$blueScreen = new BlueScreen;
453: self::$blueScreen->info = [
454: 'PHP ' . PHP_VERSION,
455: isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : NULL,
456: 'Tracy ' . self::VERSION,
457: ];
458: }
459: return self::$blueScreen;
460: }
461:
462:
463: 464: 465:
466: public static function getBar()
467: {
468: if (!self::$bar) {
469: self::$bar = new Bar;
470: self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
471: $info->cpuUsage = self::$cpuUsage;
472: self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors');
473: }
474: return self::$bar;
475: }
476:
477:
478: 479: 480:
481: public static function setLogger(ILogger $logger)
482: {
483: self::$logger = $logger;
484: }
485:
486:
487: 488: 489:
490: public static function getLogger()
491: {
492: if (!self::$logger) {
493: self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
494: self::$logger->directory = & self::$logDirectory;
495: self::$logger->email = & self::$email;
496: }
497: return self::$logger;
498: }
499:
500:
501: 502: 503:
504: public static function getFireLogger()
505: {
506: if (!self::$fireLogger) {
507: self::$fireLogger = new FireLogger;
508: }
509: return self::$fireLogger;
510: }
511:
512:
513:
514:
515:
516: 517: 518: 519: 520: 521: 522:
523: public static function dump($var, $return = FALSE)
524: {
525: if ($return) {
526: ob_start(function () {});
527: Dumper::dump($var, [
528: Dumper::DEPTH => self::$maxDepth,
529: Dumper::TRUNCATE => self::$maxLength,
530: ]);
531: return ob_get_clean();
532:
533: } elseif (!self::$productionMode) {
534: Dumper::dump($var, [
535: Dumper::DEPTH => self::$maxDepth,
536: Dumper::TRUNCATE => self::$maxLength,
537: Dumper::LOCATION => self::$showLocation,
538: ]);
539: }
540:
541: return $var;
542: }
543:
544:
545: 546: 547: 548: 549:
550: public static function timer($name = NULL)
551: {
552: static $time = [];
553: $now = microtime(TRUE);
554: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
555: $time[$name] = $now;
556: return $delta;
557: }
558:
559:
560: 561: 562: 563: 564: 565: 566: 567:
568: public static function barDump($var, $title = NULL, array $options = NULL)
569: {
570: if (!self::$productionMode) {
571: static $panel;
572: if (!$panel) {
573: self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps');
574: }
575: $panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, (array) $options + [
576: Dumper::DEPTH => self::$maxDepth,
577: Dumper::TRUNCATE => self::$maxLength,
578: Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
579: ])];
580: }
581: return $var;
582: }
583:
584:
585: 586: 587: 588: 589:
590: public static function log($message, $priority = ILogger::INFO)
591: {
592: return self::getLogger()->log($message, $priority);
593: }
594:
595:
596: 597: 598: 599: 600:
601: public static function fireLog($message)
602: {
603: if (!self::$productionMode) {
604: return self::getFireLogger()->log($message);
605: }
606: }
607:
608:
609: 610: 611: 612: 613:
614: public static function detectDebugMode($list = NULL)
615: {
616: $addr = isset($_SERVER['REMOTE_ADDR'])
617: ? $_SERVER['REMOTE_ADDR']
618: : php_uname('n');
619: $secret = isset($_COOKIE[self::COOKIE_SECRET]) && is_string($_COOKIE[self::COOKIE_SECRET])
620: ? $_COOKIE[self::COOKIE_SECRET]
621: : NULL;
622: $list = is_string($list)
623: ? preg_split('#[,\s]+#', $list)
624: : (array) $list;
625: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
626: $list[] = '127.0.0.1';
627: $list[] = '::1';
628: }
629: return in_array($addr, $list, TRUE) || in_array("$secret@$addr", $list, TRUE);
630: }
631:
632: }
633: