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.3.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 $version = self::VERSION;
31:
32:
33: public static $productionMode = self::DETECT;
34:
35:
36: public static $showBar = TRUE;
37:
38:
39: private static $enabled = FALSE;
40:
41:
42: private static $reserved;
43:
44:
45: private static $obLevel;
46:
47:
48:
49:
50: public static $strictMode = FALSE;
51:
52:
53: public static $scream = FALSE;
54:
55:
56: public static $onFatalError = array();
57:
58:
59:
60:
61: public static $maxDepth = 3;
62:
63:
64: public static $maxLen = 150;
65:
66:
67: public static $showLocation = FALSE;
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 $source;
96:
97:
98: public static $editor = 'editor://open/?file=%file&line=%line';
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::$reserved = str_repeat('t', 3e5);
147: self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
148: self::$obLevel = ob_get_level();
149: self::$cpuUsage = !self::$productionMode && function_exists('getrusage') ? getrusage() : NULL;
150:
151:
152: if ($email !== NULL) {
153: self::$email = $email;
154: }
155: if ($logDirectory !== NULL) {
156: self::$logDirectory = $logDirectory;
157: }
158: if (self::$logDirectory) {
159: if (!is_dir(self::$logDirectory) || !preg_match('#([a-z]+:)?[/\\\\]#Ai', self::$logDirectory)) {
160: self::$logDirectory = NULL;
161: self::exceptionHandler(new \RuntimeException('Logging directory not found or is not absolute path.'));
162: }
163: }
164:
165:
166: if (function_exists('ini_set')) {
167: ini_set('display_errors', !self::$productionMode);
168: ini_set('html_errors', FALSE);
169: ini_set('log_errors', FALSE);
170:
171: } elseif (ini_get('display_errors') != !self::$productionMode
172: && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')
173: ) {
174: self::exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
175: }
176: error_reporting(E_ALL | E_STRICT);
177:
178: if (!self::$enabled) {
179: register_shutdown_function(array(__CLASS__, 'shutdownHandler'));
180: set_exception_handler(array(__CLASS__, 'exceptionHandler'));
181: set_error_handler(array(__CLASS__, 'errorHandler'));
182:
183: array_map('class_exists', array('Tracy\Bar', 'Tracy\BlueScreen', 'Tracy\DefaultBarPanel', 'Tracy\Dumper',
184: 'Tracy\FireLogger', 'Tracy\Helpers', 'Tracy\Logger'));
185:
186: self::$enabled = TRUE;
187: }
188: }
189:
190:
191: 192: 193:
194: public static function isEnabled()
195: {
196: return self::$enabled;
197: }
198:
199:
200: 201: 202: 203: 204:
205: public static function shutdownHandler()
206: {
207: if (!self::$reserved) {
208: return;
209: }
210:
211: $error = error_get_last();
212: if (in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR), TRUE)) {
213: self::exceptionHandler(
214: Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])),
215: FALSE
216: );
217:
218: } elseif (self::$showBar && !connection_aborted() && !self::$productionMode && self::isHtmlMode()) {
219: self::$reserved = NULL;
220: self::removeOutputBuffers(FALSE);
221: self::getBar()->render();
222: }
223: }
224:
225:
226: 227: 228: 229: 230: 231:
232: public static function exceptionHandler($exception, $exit = TRUE)
233: {
234: if (!self::$reserved) {
235: return;
236: }
237: self::$reserved = NULL;
238:
239: if (!headers_sent()) {
240: $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
241: $code = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE ? 503 : 500;
242: header("$protocol $code", TRUE, $code);
243: if (self::isHtmlMode()) {
244: header('Content-Type: text/html; charset=UTF-8');
245: }
246: }
247:
248: Helpers::improveException($exception);
249: self::removeOutputBuffers(TRUE);
250:
251: if (self::$productionMode) {
252: try {
253: self::log($exception, self::EXCEPTION);
254: } catch (\Throwable $e) {
255: } catch (\Exception $e) {
256: }
257:
258: if (self::isHtmlMode()) {
259: $logged = empty($e);
260: require self::$errorTemplate ?: __DIR__ . '/assets/Debugger/error.500.phtml';
261: } elseif (PHP_SAPI === 'cli') {
262: fwrite(STDERR, 'ERROR: application encountered an error and can not continue. '
263: . (isset($e) ? "Unable to log error.\n" : "Error was logged.\n"));
264: }
265:
266: } elseif (!connection_aborted() && self::isHtmlMode()) {
267: self::getBlueScreen()->render($exception);
268: if (self::$showBar) {
269: self::getBar()->render();
270: }
271:
272: } else {
273: self::fireLog($exception);
274: $s = get_class($exception) . ($exception->getMessage() === '' ? '' : ': ' . $exception->getMessage())
275: . ' in ' . $exception->getFile() . ':' . $exception->getLine()
276: . "\nStack trace:\n" . $exception->getTraceAsString();
277: try {
278: $file = self::log($exception, self::EXCEPTION);
279: if ($file && !headers_sent()) {
280: header("X-Tracy-Error-Log: $file");
281: }
282: echo "$s\n" . ($file ? "(stored in $file)\n" : '');
283: if ($file && self::$browser) {
284: exec(self::$browser . ' ' . escapeshellarg($file));
285: }
286: } catch (\Throwable $e) {
287: echo "$s\nUnable to log error: {$e->getMessage()}\n";
288: } catch (\Exception $e) {
289: echo "$s\nUnable to log error: {$e->getMessage()}\n";
290: }
291: }
292:
293: try {
294: $e = NULL;
295: foreach (self::$onFatalError as $handler) {
296: call_user_func($handler, $exception);
297: }
298: } catch (\Throwable $e) {
299: } catch (\Exception $e) {
300: }
301: if ($e) {
302: try {
303: self::log($e, self::EXCEPTION);
304: } catch (\Throwable $e) {
305: } catch (\Exception $e) {
306: }
307: }
308:
309: if ($exit) {
310: exit($exception instanceof \Error ? 255 : 254);
311: }
312: }
313:
314:
315: 316: 317: 318: 319: 320:
321: public static function errorHandler($severity, $message, $file, $line, $context)
322: {
323: if (self::$scream) {
324: error_reporting(E_ALL | E_STRICT);
325: }
326:
327: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
328: if (Helpers::findTrace(debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE), '*::__toString')) {
329: $previous = isset($context['e']) && ($context['e'] instanceof \Exception || $context['e'] instanceof \Throwable) ? $context['e'] : NULL;
330: $e = new ErrorException($message, 0, $severity, $file, $line, $previous);
331: $e->context = $context;
332: self::exceptionHandler($e);
333: }
334:
335: $e = new ErrorException($message, 0, $severity, $file, $line);
336: $e->context = $context;
337: throw $e;
338:
339: } elseif (($severity & error_reporting()) !== $severity) {
340: return FALSE;
341:
342: } elseif (self::$productionMode && ($severity & self::$logSeverity) === $severity) {
343: $e = new ErrorException($message, 0, $severity, $file, $line);
344: $e->context = $context;
345: try {
346: self::log($e, self::ERROR);
347: } catch (\Throwable $e) {
348: } catch (\Exception $foo) {
349: }
350: return NULL;
351:
352: } elseif (!self::$productionMode && !isset($_GET['_tracy_skip_error'])
353: && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))
354: ) {
355: $e = new ErrorException($message, 0, $severity, $file, $line);
356: $e->context = $context;
357: $e->skippable = TRUE;
358: self::exceptionHandler($e);
359: }
360:
361: $message = 'PHP ' . Helpers::errorTypeToString($severity) . ": $message";
362: $count = & self::getBar()->getPanel('Tracy:errors')->data["$file|$line|$message"];
363:
364: if ($count++) {
365: return NULL;
366:
367: } elseif (self::$productionMode) {
368: try {
369: self::log("$message in $file:$line", self::ERROR);
370: } catch (\Throwable $e) {
371: } catch (\Exception $foo) {
372: }
373: return NULL;
374:
375: } else {
376: self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
377: return self::isHtmlMode() ? NULL : FALSE;
378: }
379: }
380:
381:
382: private static function isHtmlMode()
383: {
384: return empty($_SERVER['HTTP_X_REQUESTED_WITH'])
385: && PHP_SAPI !== 'cli'
386: && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
387: }
388:
389:
390: private static function removeOutputBuffers($errorOccurred)
391: {
392: while (ob_get_level() > self::$obLevel) {
393: $tmp = ob_get_status(TRUE);
394: $status = end($tmp);
395: if (in_array($status['name'], array('ob_gzhandler', 'zlib output compression'))) {
396: break;
397: }
398: $fnc = $status['chunk_size'] || !$errorOccurred ? 'ob_end_flush' : 'ob_end_clean';
399: if (!@$fnc()) {
400: break;
401: }
402: }
403: }
404:
405:
406:
407:
408:
409: 410: 411:
412: public static function getBlueScreen()
413: {
414: if (!self::$blueScreen) {
415: self::$blueScreen = new BlueScreen;
416: self::$blueScreen->info = array(
417: 'PHP ' . PHP_VERSION,
418: isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : NULL,
419: 'Tracy ' . self::VERSION,
420: );
421: }
422: return self::$blueScreen;
423: }
424:
425:
426: 427: 428:
429: public static function getBar()
430: {
431: if (!self::$bar) {
432: self::$bar = new Bar;
433: self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
434: $info->cpuUsage = self::$cpuUsage;
435: self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors');
436: }
437: return self::$bar;
438: }
439:
440:
441: 442: 443:
444: public static function setLogger(ILogger $logger)
445: {
446: self::$logger = $logger;
447: }
448:
449:
450: 451: 452:
453: public static function getLogger()
454: {
455: if (!self::$logger) {
456: self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
457: self::$logger->directory = & self::$logDirectory;
458: self::$logger->email = & self::$email;
459: }
460: return self::$logger;
461: }
462:
463:
464: 465: 466:
467: public static function getFireLogger()
468: {
469: if (!self::$fireLogger) {
470: self::$fireLogger = new FireLogger;
471: }
472: return self::$fireLogger;
473: }
474:
475:
476:
477:
478:
479: 480: 481: 482: 483: 484: 485:
486: public static function dump($var, $return = FALSE)
487: {
488: if ($return) {
489: ob_start(function () {});
490: Dumper::dump($var, array(
491: Dumper::DEPTH => self::$maxDepth,
492: Dumper::TRUNCATE => self::$maxLen,
493: ));
494: return ob_get_clean();
495:
496: } elseif (!self::$productionMode) {
497: Dumper::dump($var, array(
498: Dumper::DEPTH => self::$maxDepth,
499: Dumper::TRUNCATE => self::$maxLen,
500: Dumper::LOCATION => self::$showLocation,
501: ));
502: }
503:
504: return $var;
505: }
506:
507:
508: 509: 510: 511: 512:
513: public static function timer($name = NULL)
514: {
515: static $time = array();
516: $now = microtime(TRUE);
517: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
518: $time[$name] = $now;
519: return $delta;
520: }
521:
522:
523: 524: 525: 526: 527: 528: 529: 530:
531: public static function barDump($var, $title = NULL, array $options = NULL)
532: {
533: if (!self::$productionMode) {
534: static $panel;
535: if (!$panel) {
536: self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'));
537: }
538: $panel->data[] = array('title' => $title, 'dump' => Dumper::toHtml($var, (array) $options + array(
539: Dumper::DEPTH => self::$maxDepth,
540: Dumper::TRUNCATE => self::$maxLen,
541: Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
542: )));
543: }
544: return $var;
545: }
546:
547:
548: 549: 550: 551: 552:
553: public static function log($message, $priority = ILogger::INFO)
554: {
555: return self::getLogger()->log($message, $priority);
556: }
557:
558:
559: 560: 561: 562: 563:
564: public static function fireLog($message)
565: {
566: if (!self::$productionMode) {
567: return self::getFireLogger()->log($message);
568: }
569: }
570:
571:
572: 573: 574: 575: 576:
577: public static function detectDebugMode($list = NULL)
578: {
579: $addr = isset($_SERVER['REMOTE_ADDR'])
580: ? $_SERVER['REMOTE_ADDR']
581: : php_uname('n');
582: $secret = isset($_COOKIE[self::COOKIE_SECRET]) && is_string($_COOKIE[self::COOKIE_SECRET])
583: ? $_COOKIE[self::COOKIE_SECRET]
584: : NULL;
585: $list = is_string($list)
586: ? preg_split('#[,\s]+#', $list)
587: : (array) $list;
588: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
589: $list[] = '127.0.0.1';
590: $list[] = '::1';
591: }
592: return in_array($addr, $list, TRUE) || in_array("$secret@$addr", $list, TRUE);
593: }
594:
595: }
596: