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