1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tracy;
9:
10: use ErrorException;
11: use Tracy;
12:
13:
14: 15: 16:
17: class Debugger
18: {
19: const VERSION = '2.4.9';
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', 30000);
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 ? '0' : '1');
169: ini_set('html_errors', '0');
170: ini_set('log_errors', '0');
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:
183: register_shutdown_function([__CLASS__, 'shutdownHandler']);
184: set_exception_handler([__CLASS__, 'exceptionHandler']);
185: set_error_handler([__CLASS__, 'errorHandler']);
186:
187: array_map('class_exists', ['Tracy\Bar', 'Tracy\BlueScreen', 'Tracy\DefaultBarPanel', 'Tracy\Dumper',
188: 'Tracy\FireLogger', 'Tracy\Helpers', 'Tracy\Logger', ]);
189:
190: self::dispatch();
191: self::$enabled = true;
192: }
193:
194:
195: 196: 197:
198: public static function dispatch()
199: {
200: if (self::$productionMode) {
201: return;
202:
203: } elseif (headers_sent($file, $line) || ob_get_length()) {
204: throw new \LogicException(
205: __METHOD__ . '() called after some output has been sent. '
206: . ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.')
207: );
208:
209: } elseif (self::$enabled && session_status() !== PHP_SESSION_ACTIVE) {
210: ini_set('session.use_cookies', '1');
211: ini_set('session.use_only_cookies', '1');
212: ini_set('session.use_trans_sid', '0');
213: ini_set('session.cookie_path', '/');
214: ini_set('session.cookie_httponly', '1');
215: session_start();
216: }
217:
218: if (self::getBar()->dispatchAssets()) {
219: exit;
220: }
221: }
222:
223:
224: 225: 226: 227:
228: public static function renderLoader()
229: {
230: if (!self::$productionMode) {
231: self::getBar()->renderLoader();
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: self::$reserved = null;
256:
257: $error = error_get_last();
258: if (in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
259: self::exceptionHandler(
260: Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])),
261: false
262: );
263:
264: } elseif (self::$showBar && !self::$productionMode) {
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 && $exit) {
280: return;
281: }
282: self::$reserved = null;
283:
284: if (!headers_sent()) {
285: http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== false ? 503 : 500);
286: if (Helpers::isHtmlMode()) {
287: header('Content-Type: text/html; charset=UTF-8');
288: }
289: }
290:
291: Helpers::improveException($exception);
292: self::removeOutputBuffers(true);
293:
294: if (self::$productionMode) {
295: try {
296: self::log($exception, self::EXCEPTION);
297: } catch (\Exception $e) {
298: } catch (\Throwable $e) {
299: }
300:
301: if (Helpers::isHtmlMode()) {
302: $logged = empty($e);
303: require self::$errorTemplate ?: __DIR__ . '/assets/Debugger/error.500.phtml';
304: } elseif (PHP_SAPI === 'cli') {
305: fwrite(STDERR, 'ERROR: application encountered an error and can not continue. '
306: . (isset($e) ? "Unable to log error.\n" : "Error was logged.\n"));
307: }
308:
309: } elseif (!connection_aborted() && (Helpers::isHtmlMode() || Helpers::isAjax())) {
310: self::getBlueScreen()->render($exception);
311: if (self::$showBar) {
312: self::getBar()->render();
313: }
314:
315: } else {
316: self::fireLog($exception);
317: $s = get_class($exception) . ($exception->getMessage() === '' ? '' : ': ' . $exception->getMessage())
318: . ' in ' . $exception->getFile() . ':' . $exception->getLine()
319: . "\nStack trace:\n" . $exception->getTraceAsString();
320: try {
321: $file = self::log($exception, self::EXCEPTION);
322: if ($file && !headers_sent()) {
323: header("X-Tracy-Error-Log: $file");
324: }
325: echo "$s\n" . ($file ? "(stored in $file)\n" : '');
326: if ($file && self::$browser) {
327: exec(self::$browser . ' ' . escapeshellarg($file));
328: }
329: } catch (\Exception $e) {
330: echo "$s\nUnable to log error: {$e->getMessage()}\n";
331: } catch (\Throwable $e) {
332: echo "$s\nUnable to log error: {$e->getMessage()}\n";
333: }
334: }
335:
336: try {
337: $e = null;
338: foreach (self::$onFatalError as $handler) {
339: call_user_func($handler, $exception);
340: }
341: } catch (\Exception $e) {
342: } catch (\Throwable $e) {
343: }
344: if ($e) {
345: try {
346: self::log($e, self::EXCEPTION);
347: } catch (\Exception $e) {
348: } catch (\Throwable $e) {
349: }
350: }
351:
352: if ($exit) {
353: exit(255);
354: }
355: }
356:
357:
358: 359: 360: 361: 362: 363:
364: public static function errorHandler($severity, $message, $file, $line, $context)
365: {
366: if (self::$scream) {
367: error_reporting(E_ALL);
368: }
369:
370: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
371: if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) {
372: $previous = isset($context['e']) && ($context['e'] instanceof \Exception || $context['e'] instanceof \Throwable) ? $context['e'] : null;
373: $e = new ErrorException($message, 0, $severity, $file, $line, $previous);
374: $e->context = $context;
375: self::exceptionHandler($e);
376: }
377:
378: $e = new ErrorException($message, 0, $severity, $file, $line);
379: $e->context = $context;
380: throw $e;
381:
382: } elseif (($severity & error_reporting()) !== $severity) {
383: return false;
384:
385: } elseif (self::$productionMode && ($severity & self::$logSeverity) === $severity) {
386: $e = new ErrorException($message, 0, $severity, $file, $line);
387: $e->context = $context;
388: Helpers::improveException($e);
389: try {
390: self::log($e, self::ERROR);
391: } catch (\Exception $foo) {
392: } catch (\Throwable $foo) {
393: }
394: return null;
395:
396: } elseif (!self::$productionMode && !isset($_GET['_tracy_skip_error'])
397: && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))
398: ) {
399: $e = new ErrorException($message, 0, $severity, $file, $line);
400: $e->context = $context;
401: $e->skippable = true;
402: self::exceptionHandler($e);
403: }
404:
405: $message = 'PHP ' . Helpers::errorTypeToString($severity) . ": $message";
406: $count = &self::getBar()->getPanel('Tracy:errors')->data["$file|$line|$message"];
407:
408: if ($count++) {
409: return null;
410:
411: } elseif (self::$productionMode) {
412: try {
413: self::log("$message in $file:$line", self::ERROR);
414: } catch (\Exception $foo) {
415: } catch (\Throwable $foo) {
416: }
417: return null;
418:
419: } else {
420: self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
421: return Helpers::isHtmlMode() || Helpers::isAjax() ? null : false;
422: }
423: }
424:
425:
426: private static function removeOutputBuffers($errorOccurred)
427: {
428: while (ob_get_level() > self::$obLevel) {
429: $status = ob_get_status();
430: if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], true)) {
431: break;
432: }
433: $fnc = $status['chunk_size'] || !$errorOccurred ? 'ob_end_flush' : 'ob_end_clean';
434: if (!@$fnc()) {
435: break;
436: }
437: }
438: }
439:
440:
441:
442:
443:
444: 445: 446:
447: public static function getBlueScreen()
448: {
449: if (!self::$blueScreen) {
450: self::$blueScreen = new BlueScreen;
451: self::$blueScreen->info = [
452: 'PHP ' . PHP_VERSION,
453: isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : null,
454: 'Tracy ' . self::VERSION,
455: ];
456: }
457: return self::$blueScreen;
458: }
459:
460:
461: 462: 463:
464: public static function getBar()
465: {
466: if (!self::$bar) {
467: self::$bar = new Bar;
468: self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
469: $info->cpuUsage = self::$cpuUsage;
470: self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors');
471: }
472: return self::$bar;
473: }
474:
475:
476: 477: 478:
479: public static function setLogger(ILogger $logger)
480: {
481: self::$logger = $logger;
482: }
483:
484:
485: 486: 487:
488: public static function getLogger()
489: {
490: if (!self::$logger) {
491: self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
492: self::$logger->directory = &self::$logDirectory;
493: self::$logger->email = &self::$email;
494: }
495: return self::$logger;
496: }
497:
498:
499: 500: 501:
502: public static function getFireLogger()
503: {
504: if (!self::$fireLogger) {
505: self::$fireLogger = new FireLogger;
506: }
507: return self::$fireLogger;
508: }
509:
510:
511:
512:
513:
514: 515: 516: 517: 518: 519: 520:
521: public static function dump($var, $return = false)
522: {
523: if ($return) {
524: ob_start(function () {});
525: Dumper::dump($var, [
526: Dumper::DEPTH => self::$maxDepth,
527: Dumper::TRUNCATE => self::$maxLength,
528: ]);
529: return ob_get_clean();
530:
531: } elseif (!self::$productionMode) {
532: Dumper::dump($var, [
533: Dumper::DEPTH => self::$maxDepth,
534: Dumper::TRUNCATE => self::$maxLength,
535: Dumper::LOCATION => self::$showLocation,
536: ]);
537: }
538:
539: return $var;
540: }
541:
542:
543: 544: 545: 546: 547:
548: public static function timer($name = null)
549: {
550: static $time = [];
551: $now = microtime(true);
552: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
553: $time[$name] = $now;
554: return $delta;
555: }
556:
557:
558: 559: 560: 561: 562: 563: 564: 565:
566: public static function barDump($var, $title = null, array $options = null)
567: {
568: if (!self::$productionMode) {
569: static $panel;
570: if (!$panel) {
571: self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps');
572: }
573: $panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, (array) $options + [
574: Dumper::DEPTH => self::$maxDepth,
575: Dumper::TRUNCATE => self::$maxLength,
576: Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
577: ])];
578: }
579: return $var;
580: }
581:
582:
583: 584: 585: 586: 587:
588: public static function log($message, $priority = ILogger::INFO)
589: {
590: return self::getLogger()->log($message, $priority);
591: }
592:
593:
594: 595: 596: 597: 598:
599: public static function fireLog($message)
600: {
601: if (!self::$productionMode) {
602: return self::getFireLogger()->log($message);
603: }
604: }
605:
606:
607: 608: 609: 610: 611:
612: public static function detectDebugMode($list = null)
613: {
614: $addr = isset($_SERVER['REMOTE_ADDR'])
615: ? $_SERVER['REMOTE_ADDR']
616: : php_uname('n');
617: $secret = isset($_COOKIE[self::COOKIE_SECRET]) && is_string($_COOKIE[self::COOKIE_SECRET])
618: ? $_COOKIE[self::COOKIE_SECRET]
619: : null;
620: $list = is_string($list)
621: ? preg_split('#[,\s]+#', $list)
622: : (array) $list;
623: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
624: $list[] = '127.0.0.1';
625: $list[] = '::1';
626: }
627: return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true);
628: }
629: }
630: