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