1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tracy;
9:
10: use Tracy;
11: use ErrorException;
12:
13:
14: 15: 16: 17: 18: 19: 20: 21: 22:
23: class Debugger
24: {
25: const VERSION = '2.2.9';
26:
27:
28: public static $version = self::VERSION;
29:
30:
31: public static $productionMode = self::DETECT;
32:
33:
34: public static $time;
35:
36:
37: public static $source;
38:
39:
40: public static $editor = 'editor://open/?file=%file&line=%line';
41:
42:
43: public static $browser;
44:
45:
46:
47:
48: public static $maxDepth = 3;
49:
50:
51: public static $maxLen = 150;
52:
53:
54: public static $showLocation = FALSE;
55:
56:
57:
58:
59: const DEVELOPMENT = FALSE,
60: PRODUCTION = TRUE,
61: DETECT = NULL;
62:
63:
64: private static $blueScreen;
65:
66:
67: public static $strictMode = FALSE;
68:
69:
70: public static $scream = FALSE;
71:
72:
73: public static $onFatalError = array();
74:
75:
76: private static $enabled = FALSE;
77:
78:
79: private static $done;
80:
81:
82: public static $errorTypes = array(
83: E_ERROR => 'Fatal Error',
84: E_USER_ERROR => 'User Error',
85: E_RECOVERABLE_ERROR => 'Recoverable Error',
86: E_CORE_ERROR => 'Core Error',
87: E_COMPILE_ERROR => 'Compile Error',
88: E_PARSE => 'Parse Error',
89: E_WARNING => 'Warning',
90: E_CORE_WARNING => 'Core Warning',
91: E_COMPILE_WARNING => 'Compile Warning',
92: E_USER_WARNING => 'User Warning',
93: E_NOTICE => 'Notice',
94: E_USER_NOTICE => 'User Notice',
95: E_STRICT => 'Strict standards',
96: E_DEPRECATED => 'Deprecated',
97: E_USER_DEPRECATED => 'User Deprecated',
98: );
99:
100:
101:
102:
103: private static $logger;
104:
105:
106: private static $fireLogger;
107:
108:
109: public static $logDirectory;
110:
111:
112: public static $logSeverity = 0;
113:
114:
115: public static $email;
116:
117:
118: public static $mailer = array('Tracy\Logger', 'defaultMailer');
119:
120:
121: public static $emailSnooze = 172800;
122:
123:
124: const DEBUG = 'debug',
125: INFO = 'info',
126: WARNING = 'warning',
127: ERROR = 'error',
128: EXCEPTION = 'exception',
129: CRITICAL = 'critical';
130:
131:
132:
133:
134: private static $bar;
135:
136:
137: 138: 139:
140: final public function __construct()
141: {
142: throw new \LogicException;
143: }
144:
145:
146: 147: 148: 149: 150: 151: 152:
153: public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
154: {
155: self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
156: if (isset($_SERVER['REQUEST_URI'])) {
157: self::$source = (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
158: . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '')
159: . $_SERVER['REQUEST_URI'];
160: } else {
161: self::$source = empty($_SERVER['argv']) ? 'CLI' : 'CLI: ' . implode(' ', $_SERVER['argv']);
162: }
163: error_reporting(E_ALL | E_STRICT);
164:
165:
166: if (is_bool($mode)) {
167: self::$productionMode = $mode;
168:
169: } elseif ($mode !== self::DETECT || self::$productionMode === NULL) {
170: $list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
171: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
172: $list[] = '127.0.0.1';
173: $list[] = '::1';
174: }
175: self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
176: }
177:
178:
179: if ($email !== NULL) {
180: self::$email = $email;
181: }
182: if (is_string($logDirectory)) {
183: self::$logDirectory = realpath($logDirectory);
184: if (self::$logDirectory === FALSE) {
185: self::_exceptionHandler(new \RuntimeException("Log directory is not found or is not directory."));
186: }
187: } elseif ($logDirectory === FALSE) {
188: self::$logDirectory = NULL;
189: }
190:
191:
192: if (function_exists('ini_set')) {
193: ini_set('display_errors', !self::$productionMode);
194: ini_set('html_errors', FALSE);
195: ini_set('log_errors', FALSE);
196:
197: } elseif (ini_get('display_errors') != !self::$productionMode
198: && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')
199: ) {
200: self::_exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
201: }
202:
203: if (!self::$enabled) {
204: register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
205: set_exception_handler(array(__CLASS__, '_exceptionHandler'));
206: set_error_handler(array(__CLASS__, '_errorHandler'));
207:
208: foreach (array('Tracy\Bar', 'Tracy\BlueScreen', 'Tracy\DefaultBarPanel', 'Tracy\Dumper',
209: 'Tracy\FireLogger', 'Tracy\Helpers', 'Tracy\Logger') as $class) {
210: class_exists($class);
211: }
212:
213: self::$enabled = TRUE;
214: }
215: }
216:
217:
218: 219: 220:
221: public static function getBlueScreen()
222: {
223: if (!self::$blueScreen) {
224: self::$blueScreen = new BlueScreen;
225: self::$blueScreen->info = array(
226: 'PHP ' . PHP_VERSION,
227: isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : NULL,
228: 'Tracy ' . self::VERSION,
229: );
230: }
231: return self::$blueScreen;
232: }
233:
234:
235: 236: 237:
238: public static function getBar()
239: {
240: if (!self::$bar) {
241: self::$bar = new Bar;
242: self::$bar->addPanel(new DefaultBarPanel('time'));
243: self::$bar->addPanel(new DefaultBarPanel('memory'));
244: self::$bar->addPanel(new DefaultBarPanel('errors'), __CLASS__ . ':errors');
245: self::$bar->addPanel(new DefaultBarPanel('dumps'), __CLASS__ . ':dumps');
246: self::$bar->info = array(
247: 'PHP ' . PHP_VERSION,
248: isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : NULL,
249: 'Tracy ' . self::VERSION,
250: );
251: }
252: return self::$bar;
253: }
254:
255:
256: 257: 258:
259: public static function setLogger($logger)
260: {
261: self::$logger = $logger;
262: }
263:
264:
265: 266: 267:
268: public static function getLogger()
269: {
270: if (!self::$logger) {
271: self::$logger = new Logger;
272: self::$logger->directory = & self::$logDirectory;
273: self::$logger->email = & self::$email;
274: self::$logger->mailer = & self::$mailer;
275: self::$logger->emailSnooze = & self::$emailSnooze;
276: }
277: return self::$logger;
278: }
279:
280:
281: 282: 283:
284: public static function getFireLogger()
285: {
286: if (!self::$fireLogger) {
287: self::$fireLogger = new FireLogger;
288: }
289: return self::$fireLogger;
290: }
291:
292:
293: 294: 295: 296:
297: public static function isEnabled()
298: {
299: return self::$enabled;
300: }
301:
302:
303: 304: 305: 306: 307: 308:
309: public static function log($message, $priority = self::INFO)
310: {
311: if (!self::$logDirectory) {
312: return;
313: }
314:
315: $exceptionFilename = NULL;
316: if ($message instanceof \Exception || $message instanceof \Throwable) {
317: $exception = $message;
318: while ($exception) {
319: $tmp[] = ($exception instanceof ErrorException
320: ? 'Fatal error: ' . $exception->getMessage()
321: : Helpers::getClass($exception) . ': ' . $exception->getMessage())
322: . ' in ' . $exception->getFile() . ':' . $exception->getLine();
323: $exception = $exception->getPrevious();
324: }
325: $exception = $message;
326: $message = implode($tmp, "\ncaused by ");
327:
328: $hash = md5(preg_replace('~(Resource id #)\d+~', '$1', $exception));
329: $exceptionFilename = 'exception-' . @date('Y-m-d-H-i-s') . "-$hash.html";
330: foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
331: if (strpos($entry, $hash)) {
332: $exceptionFilename = $entry;
333: $saved = TRUE;
334: break;
335: }
336: }
337: } elseif (!is_string($message)) {
338: $message = Dumper::toText($message);
339: }
340:
341: if ($exceptionFilename) {
342: $exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
343: if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
344: ob_start();
345: ob_start(function ($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 4096);
346: self::getBlueScreen()->render($exception);
347: ob_end_flush();
348: ob_end_clean();
349: fclose($logHandle);
350: }
351: }
352:
353: self::getLogger()->log(array(
354: @date('[Y-m-d H-i-s]'),
355: trim($message),
356: self::$source ? ' @ ' . self::$source : NULL,
357: $exceptionFilename ? ' @@ ' . basename($exceptionFilename) : NULL
358: ), $priority);
359:
360: return $exceptionFilename ? strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR) : NULL;
361: }
362:
363:
364: 365: 366: 367: 368:
369: public static function _shutdownHandler()
370: {
371: if (self::$done) {
372: return;
373: }
374:
375: $error = error_get_last();
376: if (in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE), TRUE)) {
377: self::_exceptionHandler(Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])), FALSE);
378:
379: } elseif (!connection_aborted() && !self::$productionMode && self::isHtmlMode()) {
380: self::getBar()->render();
381: }
382: }
383:
384:
385: 386: 387: 388: 389: 390:
391: public static function _exceptionHandler($exception, $exit = TRUE)
392: {
393: if (self::$done) {
394: return;
395: }
396: self::$done = TRUE;
397:
398: if (!headers_sent()) {
399: $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
400: $code = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE ? 503 : 500;
401: header("$protocol $code", TRUE, $code);
402: if (self::isHtmlMode()) {
403: header('Content-Type: text/html; charset=UTF-8');
404: }
405: }
406:
407: if (self::$productionMode) {
408: try {
409: self::log($exception, self::EXCEPTION);
410: } catch (\Throwable $e) {
411: } catch (\Exception $e) {
412: }
413:
414: if (self::isHtmlMode()) {
415: $logged = empty($e);
416: require __DIR__ . '/templates/error.phtml';
417: } elseif (PHP_SAPI === 'cli') {
418: fwrite(STDERR, 'ERROR: application encountered an error and can not continue. '
419: . (isset($e) ? "Unable to log error.\n" : "Error was logged.\n"));
420: }
421:
422: } elseif (!connection_aborted() && self::isHtmlMode()) {
423: self::getBlueScreen()->render($exception);
424: self::getBar()->render();
425:
426: } elseif (connection_aborted() || !self::fireLog($exception)) {
427: try {
428: $file = self::log($exception, self::EXCEPTION);
429: if ($file && !headers_sent()) {
430: header("X-Tracy-Error-Log: $file");
431: }
432: echo "$exception\n" . ($file ? "(stored in $file)\n" : '');
433: if ($file && self::$browser) {
434: exec(self::$browser . ' ' . escapeshellarg($file));
435: }
436: } catch (\Throwable $e) {
437: echo "$s\nUnable to log error: {$e->getMessage()}\n";
438: } catch (\Exception $e) {
439: echo "$exception\nUnable to log error: {$e->getMessage()}\n";
440: }
441: }
442:
443: try {
444: $e = NULL;
445: foreach (self::$onFatalError as $handler) {
446: call_user_func($handler, $exception);
447: }
448: } catch (\Throwable $e) {
449: } catch (\Exception $e) {
450: }
451: if ($e) {
452: try {
453: self::log($e, self::EXCEPTION);
454: } catch (\Throwable $e) {
455: } catch (\Exception $e) {
456: }
457: }
458:
459: if ($exit) {
460: exit(254);
461: }
462: }
463:
464:
465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475:
476: public static function _errorHandler($severity, $message, $file, $line, $context)
477: {
478: if (self::$scream) {
479: error_reporting(E_ALL | E_STRICT);
480: }
481:
482: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
483: if (Helpers::findTrace(debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE), '*::__toString')) {
484: $previous = isset($context['e']) && ($context['e'] instanceof \Exception || $context['e'] instanceof \Throwable) ? $context['e'] : NULL;
485: $e = new ErrorException($message, 0, $severity, $file, $line, $previous);
486: $e->context = $context;
487: self::_exceptionHandler($e);
488: }
489:
490: $e = new ErrorException($message, 0, $severity, $file, $line);
491: $e->context = $context;
492: throw $e;
493:
494: } elseif (($severity & error_reporting()) !== $severity) {
495: return FALSE;
496:
497: } elseif (self::$productionMode && ($severity & self::$logSeverity) === $severity) {
498: $e = new ErrorException($message, 0, $severity, $file, $line);
499: $e->context = $context;
500: self::log($e, self::ERROR);
501: return NULL;
502:
503: } elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
504: $e = new ErrorException($message, 0, $severity, $file, $line);
505: $e->context = $context;
506: self::_exceptionHandler($e);
507: }
508:
509: $message = 'PHP ' . (isset(self::$errorTypes[$severity]) ? self::$errorTypes[$severity] : 'Unknown error') . ": $message";
510: $count = & self::getBar()->getPanel(__CLASS__ . ':errors')->data["$file|$line|$message"];
511:
512: if ($count++) {
513: return NULL;
514:
515: } elseif (self::$productionMode) {
516: self::log("$message in $file:$line", self::ERROR);
517: return NULL;
518:
519: } else {
520: self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
521: return self::isHtmlMode() ? NULL : FALSE;
522: }
523: }
524:
525:
526:
527:
528:
529: 530: 531: 532: 533: 534: 535:
536: public static function dump($var, $return = FALSE)
537: {
538: if ($return) {
539: ob_start();
540: Dumper::dump($var, array(
541: Dumper::DEPTH => self::$maxDepth,
542: Dumper::TRUNCATE => self::$maxLen,
543: ));
544: return ob_get_clean();
545:
546: } elseif (!self::$productionMode) {
547: Dumper::dump($var, array(
548: Dumper::DEPTH => self::$maxDepth,
549: Dumper::TRUNCATE => self::$maxLen,
550: Dumper::LOCATION => self::$showLocation,
551: ));
552: }
553:
554: return $var;
555: }
556:
557:
558: 559: 560: 561: 562:
563: public static function timer($name = NULL)
564: {
565: static $time = array();
566: $now = microtime(TRUE);
567: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
568: $time[$name] = $now;
569: return $delta;
570: }
571:
572:
573: 574: 575: 576: 577: 578: 579: 580:
581: public static function barDump($var, $title = NULL, array $options = NULL)
582: {
583: if (!self::$productionMode) {
584: self::getBar()->getPanel(__CLASS__ . ':dumps')->data[] = array('title' => $title, 'dump' => Dumper::toHtml($var, (array) $options + array(
585: Dumper::DEPTH => self::$maxDepth,
586: Dumper::TRUNCATE => self::$maxLen,
587: Dumper::LOCATION => self::$showLocation,
588: )));
589: }
590: return $var;
591: }
592:
593:
594: 595: 596: 597: 598:
599: public static function fireLog($message)
600: {
601: if (!self::$productionMode) {
602: return self::getFireLogger()->log($message);
603: }
604: }
605:
606:
607: private static function isHtmlMode()
608: {
609: return empty($_SERVER['HTTP_X_REQUESTED_WITH'])
610: && PHP_SAPI !== 'cli'
611: && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
612: }
613:
614: }
615: