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