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