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