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.5';
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: public static function isEnabled()
228: {
229: return self::$enabled;
230: }
231:
232:
233: 234: 235: 236: 237:
238: public static function shutdownHandler()
239: {
240: if (!self::$reserved) {
241: return;
242: }
243: self::$reserved = NULL;
244:
245: $error = error_get_last();
246: if (in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], TRUE)) {
247: self::exceptionHandler(
248: Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])),
249: FALSE
250: );
251:
252: } elseif (self::$showBar && !self::$productionMode) {
253: self::removeOutputBuffers(FALSE);
254: self::getBar()->render();
255: }
256: }
257:
258:
259: 260: 261: 262: 263: 264:
265: public static function exceptionHandler($exception, $exit = TRUE)
266: {
267: if (!self::$reserved && $exit) {
268: return;
269: }
270: self::$reserved = NULL;
271:
272: if (!headers_sent()) {
273: http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE ? 503 : 500);
274: if (Helpers::isHtmlMode()) {
275: header('Content-Type: text/html; charset=UTF-8');
276: }
277: }
278:
279: Helpers::improveException($exception);
280: self::removeOutputBuffers(TRUE);
281:
282: if (self::$productionMode) {
283: try {
284: self::log($exception, self::EXCEPTION);
285: } catch (\Throwable $e) {
286: } catch (\Exception $e) {
287: }
288:
289: if (Helpers::isHtmlMode()) {
290: $logged = empty($e);
291: require self::$errorTemplate ?: __DIR__ . '/assets/Debugger/error.500.phtml';
292: } elseif (PHP_SAPI === 'cli') {
293: fwrite(STDERR, 'ERROR: application encountered an error and can not continue. '
294: . (isset($e) ? "Unable to log error.\n" : "Error was logged.\n"));
295: }
296:
297: } elseif (!connection_aborted() && (Helpers::isHtmlMode() || Helpers::isAjax())) {
298: self::getBlueScreen()->render($exception);
299: if (self::$showBar) {
300: self::getBar()->render();
301: }
302:
303: } else {
304: self::fireLog($exception);
305: $s = get_class($exception) . ($exception->getMessage() === '' ? '' : ': ' . $exception->getMessage())
306: . ' in ' . $exception->getFile() . ':' . $exception->getLine()
307: . "\nStack trace:\n" . $exception->getTraceAsString();
308: try {
309: $file = self::log($exception, self::EXCEPTION);
310: if ($file && !headers_sent()) {
311: header("X-Tracy-Error-Log: $file");
312: }
313: echo "$s\n" . ($file ? "(stored in $file)\n" : '');
314: if ($file && self::$browser) {
315: exec(self::$browser . ' ' . escapeshellarg($file));
316: }
317: } catch (\Throwable $e) {
318: echo "$s\nUnable to log error: {$e->getMessage()}\n";
319: } catch (\Exception $e) {
320: echo "$s\nUnable to log error: {$e->getMessage()}\n";
321: }
322: }
323:
324: try {
325: $e = NULL;
326: foreach (self::$onFatalError as $handler) {
327: call_user_func($handler, $exception);
328: }
329: } catch (\Throwable $e) {
330: } catch (\Exception $e) {
331: }
332: if ($e) {
333: try {
334: self::log($e, self::EXCEPTION);
335: } catch (\Throwable $e) {
336: } catch (\Exception $e) {
337: }
338: }
339:
340: if ($exit) {
341: exit(255);
342: }
343: }
344:
345:
346: 347: 348: 349: 350: 351:
352: public static function errorHandler($severity, $message, $file, $line, $context)
353: {
354: if (self::$scream) {
355: error_reporting(E_ALL);
356: }
357:
358: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
359: if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) {
360: $previous = isset($context['e']) && ($context['e'] instanceof \Exception || $context['e'] instanceof \Throwable) ? $context['e'] : NULL;
361: $e = new ErrorException($message, 0, $severity, $file, $line, $previous);
362: $e->context = $context;
363: self::exceptionHandler($e);
364: }
365:
366: $e = new ErrorException($message, 0, $severity, $file, $line);
367: $e->context = $context;
368: throw $e;
369:
370: } elseif (($severity & error_reporting()) !== $severity) {
371: return FALSE;
372:
373: } elseif (self::$productionMode && ($severity & self::$logSeverity) === $severity) {
374: $e = new ErrorException($message, 0, $severity, $file, $line);
375: $e->context = $context;
376: Helpers::improveException($e);
377: try {
378: self::log($e, self::ERROR);
379: } catch (\Throwable $e) {
380: } catch (\Exception $foo) {
381: }
382: return NULL;
383:
384: } elseif (!self::$productionMode && !isset($_GET['_tracy_skip_error'])
385: && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))
386: ) {
387: $e = new ErrorException($message, 0, $severity, $file, $line);
388: $e->context = $context;
389: $e->skippable = TRUE;
390: self::exceptionHandler($e);
391: }
392:
393: $message = 'PHP ' . Helpers::errorTypeToString($severity) . ": $message";
394: $count = & self::getBar()->getPanel('Tracy:errors')->data["$file|$line|$message"];
395:
396: if ($count++) {
397: return NULL;
398:
399: } elseif (self::$productionMode) {
400: try {
401: self::log("$message in $file:$line", self::ERROR);
402: } catch (\Throwable $e) {
403: } catch (\Exception $foo) {
404: }
405: return NULL;
406:
407: } else {
408: self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
409: return Helpers::isHtmlMode() || Helpers::isAjax() ? NULL : FALSE;
410: }
411: }
412:
413:
414: private static function removeOutputBuffers($errorOccurred)
415: {
416: while (ob_get_level() > self::$obLevel) {
417: $status = ob_get_status();
418: if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'])) {
419: break;
420: }
421: $fnc = $status['chunk_size'] || !$errorOccurred ? 'ob_end_flush' : 'ob_end_clean';
422: if (!@$fnc()) {
423: break;
424: }
425: }
426: }
427:
428:
429:
430:
431:
432: 433: 434:
435: public static function getBlueScreen()
436: {
437: if (!self::$blueScreen) {
438: self::$blueScreen = new BlueScreen;
439: self::$blueScreen->info = [
440: 'PHP ' . PHP_VERSION,
441: isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : NULL,
442: 'Tracy ' . self::VERSION,
443: ];
444: }
445: return self::$blueScreen;
446: }
447:
448:
449: 450: 451:
452: public static function getBar()
453: {
454: if (!self::$bar) {
455: self::$bar = new Bar;
456: self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
457: $info->cpuUsage = self::$cpuUsage;
458: self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors');
459: }
460: return self::$bar;
461: }
462:
463:
464: 465: 466:
467: public static function setLogger(ILogger $logger)
468: {
469: self::$logger = $logger;
470: }
471:
472:
473: 474: 475:
476: public static function getLogger()
477: {
478: if (!self::$logger) {
479: self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
480: self::$logger->directory = & self::$logDirectory;
481: self::$logger->email = & self::$email;
482: }
483: return self::$logger;
484: }
485:
486:
487: 488: 489:
490: public static function getFireLogger()
491: {
492: if (!self::$fireLogger) {
493: self::$fireLogger = new FireLogger;
494: }
495: return self::$fireLogger;
496: }
497:
498:
499:
500:
501:
502: 503: 504: 505: 506: 507: 508:
509: public static function dump($var, $return = FALSE)
510: {
511: if ($return) {
512: ob_start(function () {});
513: Dumper::dump($var, [
514: Dumper::DEPTH => self::$maxDepth,
515: Dumper::TRUNCATE => self::$maxLength,
516: ]);
517: return ob_get_clean();
518:
519: } elseif (!self::$productionMode) {
520: Dumper::dump($var, [
521: Dumper::DEPTH => self::$maxDepth,
522: Dumper::TRUNCATE => self::$maxLength,
523: Dumper::LOCATION => self::$showLocation,
524: ]);
525: }
526:
527: return $var;
528: }
529:
530:
531: 532: 533: 534: 535:
536: public static function timer($name = NULL)
537: {
538: static $time = [];
539: $now = microtime(TRUE);
540: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
541: $time[$name] = $now;
542: return $delta;
543: }
544:
545:
546: 547: 548: 549: 550: 551: 552: 553:
554: public static function barDump($var, $title = NULL, array $options = NULL)
555: {
556: if (!self::$productionMode) {
557: static $panel;
558: if (!$panel) {
559: self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps');
560: }
561: $panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, (array) $options + [
562: Dumper::DEPTH => self::$maxDepth,
563: Dumper::TRUNCATE => self::$maxLength,
564: Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
565: ])];
566: }
567: return $var;
568: }
569:
570:
571: 572: 573: 574: 575:
576: public static function log($message, $priority = ILogger::INFO)
577: {
578: return self::getLogger()->log($message, $priority);
579: }
580:
581:
582: 583: 584: 585: 586:
587: public static function fireLog($message)
588: {
589: if (!self::$productionMode) {
590: return self::getFireLogger()->log($message);
591: }
592: }
593:
594:
595: 596: 597: 598: 599:
600: public static function detectDebugMode($list = NULL)
601: {
602: $addr = isset($_SERVER['REMOTE_ADDR'])
603: ? $_SERVER['REMOTE_ADDR']
604: : php_uname('n');
605: $secret = isset($_COOKIE[self::COOKIE_SECRET]) && is_string($_COOKIE[self::COOKIE_SECRET])
606: ? $_COOKIE[self::COOKIE_SECRET]
607: : NULL;
608: $list = is_string($list)
609: ? preg_split('#[,\s]+#', $list)
610: : (array) $list;
611: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
612: $list[] = '127.0.0.1';
613: $list[] = '::1';
614: }
615: return in_array($addr, $list, TRUE) || in_array("$secret@$addr", $list, TRUE);
616: }
617:
618: }
619: