1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
25: final class Debugger
26: {
27:
28: public static $productionMode;
29:
30:
31: public static $consoleMode;
32:
33:
34: public static $time;
35:
36:
37: private static $ajaxDetected;
38:
39:
40: public static $source;
41:
42:
43: public static $editor = 'editor://open/?file=%file&line=%line';
44:
45:
46: public static $browser;
47:
48:
49:
50:
51: public static $maxDepth = 3;
52:
53:
54: public static $maxLen = 150;
55:
56:
57: public static $showLocation = FALSE;
58:
59:
60: public static $consoleColors = array(
61: 'bool' => '1;33',
62: 'null' => '1;33',
63: 'int' => '1;36',
64: 'float' => '1;36',
65: 'string' => '1;32',
66: 'array' => '1;31',
67: 'key' => '1;37',
68: 'object' => '1;31',
69: 'visibility' => '1;30',
70: 'resource' => '1;37',
71: );
72:
73:
74:
75:
76: const DEVELOPMENT = FALSE,
77: PRODUCTION = TRUE,
78: DETECT = NULL;
79:
80:
81: public static $blueScreen;
82:
83:
84: public static $strictMode = FALSE;
85:
86:
87: public static $scream = FALSE;
88:
89:
90: public static $onFatalError = array();
91:
92:
93: private static $enabled = FALSE;
94:
95:
96: private static $lastError = FALSE;
97:
98:
99:
100:
101: public static $logger;
102:
103:
104: public static $fireLogger;
105:
106:
107: public static $logDirectory;
108:
109:
110: public static $email;
111:
112:
113: public static $mailer;
114:
115:
116: public static $emailSnooze;
117:
118:
119:
120:
121: public static $bar;
122:
123:
124: private static $errorPanel;
125:
126:
127: private static $dumpPanel;
128:
129:
130:
131:
132: const DEBUG = 'debug',
133: INFO = 'info',
134: WARNING = 'warning',
135: ERROR = 'error',
136: CRITICAL = 'critical';
137:
138:
139:
140: 141: 142:
143: final public function __construct()
144: {
145: throw new StaticClassException;
146: }
147:
148:
149:
150: 151: 152: 153:
154: public static function _init()
155: {
156: self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
157: self::$consoleMode = PHP_SAPI === 'cli';
158: self::$productionMode = self::DETECT;
159: if (self::$consoleMode) {
160: self::$source = empty($_SERVER['argv']) ? 'cli' : 'cli: ' . implode(' ', $_SERVER['argv']);
161: } else {
162: self::$ajaxDetected = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
163: if (isset($_SERVER['REQUEST_URI'])) {
164: self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
165: . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
166: . $_SERVER['REQUEST_URI'];
167: }
168: }
169:
170: self::$logger = new Logger;
171: self::$logDirectory = & self::$logger->directory;
172: self::$email = & self::$logger->email;
173: self::$mailer = & self::$logger->mailer;
174: self::$emailSnooze = & Logger::$emailSnooze;
175:
176: self::$fireLogger = new FireLogger;
177:
178: self::$blueScreen = new DebugBlueScreen;
179: self::$blueScreen->addPanel(create_function('$e', '
180: if ($e instanceof TemplateException) {
181: return array(
182: \'tab\' => \'Template\',
183: \'panel\' => \'<p><b>File:</b> \' . DebugHelpers::editorLink($e->sourceFile, $e->sourceLine)
184: . \' <b>Line:</b> \' . ($e->sourceLine ? $e->sourceLine : \'n/a\') . \'</p>\'
185: . ($e->sourceLine ? DebugBlueScreen::highlightFile($e->sourceFile, $e->sourceLine) : \'\')
186: );
187: } elseif ($e instanceof NeonException && preg_match(\'#line (\\d+)#\', $e->getMessage(), $m)) {
188: if ($item = DebugHelpers::findTrace($e->getTrace(), \'ConfigNeonAdapter::load\')) {
189: return array(
190: \'tab\' => \'NEON\',
191: \'panel\' => \'<p><b>File:</b> \' . DebugHelpers::editorLink($item[\'args\'][0], $m[1]) . \' <b>Line:</b> \' . $m[1] . \'</p>\'
192: . DebugBlueScreen::highlightFile($item[\'args\'][0], $m[1])
193: );
194: } elseif ($item = DebugHelpers::findTrace($e->getTrace(), \'Neon::decode\')) {
195: return array(
196: \'tab\' => \'NEON\',
197: \'panel\' => DebugBlueScreen::highlightPhp($item[\'args\'][0], $m[1])
198: );
199: }
200: }
201: '));
202:
203: self::$bar = new DebugBar;
204: self::$bar->addPanel(new DefaultBarPanel('time'));
205: self::$bar->addPanel(new DefaultBarPanel('memory'));
206: self::$bar->addPanel(self::$errorPanel = new DefaultBarPanel('errors'));
207: self::$bar->addPanel(self::$dumpPanel = new DefaultBarPanel('dumps'));
208: }
209:
210:
211:
212:
213:
214:
215:
216: 217: 218: 219: 220: 221: 222:
223: public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
224: {
225: error_reporting(E_ALL | E_STRICT);
226:
227:
228: if (is_bool($mode)) {
229: self::$productionMode = $mode;
230:
231: } elseif ($mode !== self::DETECT || self::$productionMode === NULL) {
232: $list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
233: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
234: $list[] = '127.0.0.1';
235: $list[] = '::1';
236: }
237: self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
238: }
239:
240:
241: if (is_string($logDirectory)) {
242: self::$logDirectory = realpath($logDirectory);
243: if (self::$logDirectory === FALSE) {
244: echo __METHOD__ . "() error: Log directory is not found or is not directory.\n";
245: exit(254);
246: }
247: } elseif ($logDirectory === FALSE) {
248: self::$logDirectory = FALSE;
249:
250: } elseif (self::$logDirectory === NULL) {
251: self::$logDirectory = defined('APP_DIR') ? APP_DIR . '/../log' : getcwd() . '/log';
252: }
253: if (self::$logDirectory) {
254: ini_set('error_log', self::$logDirectory . '/php_error.log');
255: }
256:
257:
258: if (function_exists('ini_set')) {
259: ini_set('display_errors', !self::$productionMode);
260: ini_set('html_errors', FALSE);
261: ini_set('log_errors', FALSE);
262:
263: } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) {
264: echo __METHOD__ . "() error: Unable to set 'display_errors' because function ini_set() is disabled.\n";
265: exit(254);
266: }
267:
268: if ($email) {
269: if (!is_string($email)) {
270: echo __METHOD__ . "() error: Email address must be a string.\n";
271: exit(254);
272: }
273: self::$email = $email;
274: }
275:
276: if (!defined('E_DEPRECATED')) {
277: define('E_DEPRECATED', 8192);
278: }
279:
280: if (!defined('E_USER_WARNING')) {
281: define('E_USER_WARNING', 16384);
282: }
283:
284: if (!self::$enabled) {
285: register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
286: set_exception_handler(array(__CLASS__, '_exceptionHandler'));
287: set_error_handler(array(__CLASS__, '_errorHandler'));
288: self::$enabled = TRUE;
289: }
290: }
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:
311: public static function log($message, $priority = self::INFO)
312: {
313: if (self::$logDirectory === FALSE) {
314: return;
315:
316: } elseif (!self::$logDirectory) {
317: throw new InvalidStateException('Logging directory is not specified in Debugger::$logDirectory.');
318: }
319:
320: if ($message instanceof Exception) {
321: $exception = $message;
322: $message = ($message instanceof FatalErrorException
323: ? 'Fatal error: ' . $exception->getMessage()
324: : get_class($exception) . ": " . $exception->getMessage())
325: . " in " . $exception->getFile() . ":" . $exception->getLine();
326:
327: $hash = md5($exception . (method_exists($exception, 'getPrevious') ? $exception->getPrevious() : (isset($exception->previous) ? $exception->previous : '')));
328: $exceptionFilename = "exception-" . @date('Y-m-d-H-i-s') . "-$hash.html";
329: foreach (new DirectoryIterator(self::$logDirectory) as $entry) {
330: if (strpos($entry, $hash)) {
331: $exceptionFilename = $entry;
332: $saved = TRUE;
333: break;
334: }
335: }
336: }
337:
338: self::$logger->log(array(
339: @date('[Y-m-d H-i-s]'),
340: trim($message),
341: self::$source ? ' @ ' . self::$source : NULL,
342: !empty($exceptionFilename) ? ' @@ ' . $exceptionFilename : NULL
343: ), $priority);
344:
345: if (!empty($exceptionFilename)) {
346: $exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
347: if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
348: ob_start();
349: ob_start(create_function('$buffer', 'extract($GLOBALS[0]['.array_push($GLOBALS[0], array('logHandle'=>$logHandle)).'-1], EXTR_REFS); fwrite($logHandle, $buffer); '), 4096);
350: self::$blueScreen->render($exception);
351: ob_end_flush();
352: ob_end_clean();
353: fclose($logHandle);
354: }
355: return strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR);
356: }
357: }
358:
359:
360:
361: 362: 363: 364: 365:
366: public static function _shutdownHandler()
367: {
368: if (!self::$enabled) {
369: return;
370: }
371:
372:
373: static $types = array(
374: E_ERROR => 1,
375: E_CORE_ERROR => 1,
376: E_COMPILE_ERROR => 1,
377: E_PARSE => 1,
378: );
379: $error = error_get_last();
380: if (isset($types[$error['type']])) {
381: self::_exceptionHandler(new FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL));
382: }
383:
384:
385: if (self::$bar && !self::$productionMode && self::isHtmlMode()) {
386: self::$bar->render();
387: }
388: }
389:
390:
391:
392: 393: 394: 395: 396: 397:
398: public static function _exceptionHandler(Exception $exception)
399: {
400: if (!headers_sent()) {
401: $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
402: header($protocol . ' 500', TRUE, 500);
403: }
404:
405: try {
406: if (self::$productionMode) {
407: try {
408: self::log($exception, self::ERROR);
409: } catch (Exception $e) {
410: echo 'FATAL ERROR: unable to log error';
411: }
412:
413: if (self::$consoleMode) {
414: echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
415:
416: } elseif (self::isHtmlMode()) {
417: require dirname(__FILE__) . '/templates/error.phtml';
418: }
419:
420: } else {
421: if (self::$consoleMode) {
422: echo "$exception\n";
423: if ($file = self::log($exception)) {
424: echo "(stored in $file)\n";
425: if (self::$browser) {
426: exec(self::$browser . ' ' . escapeshellarg($file));
427: }
428: }
429:
430: } elseif (self::isHtmlMode()) {
431: self::$blueScreen->render($exception);
432: if (self::$bar) {
433: self::$bar->render();
434: }
435:
436: } elseif (!self::fireLog($exception)) {
437: $file = self::log($exception, self::ERROR);
438: if (!headers_sent()) {
439: header("X-Nette-Error-Log: $file");
440: }
441: }
442: }
443:
444: foreach (self::$onFatalError as $handler) {
445: call_user_func($handler, $exception);
446: }
447:
448: } catch (Exception $e) {
449: if (self::$productionMode) {
450: echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
451: } else {
452: echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
453: "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
454: }
455: }
456:
457: self::$enabled = FALSE;
458: exit(254);
459: }
460:
461:
462:
463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473:
474: public static function _errorHandler($severity, $message, $file, $line, $context)
475: {
476: if (self::$scream) {
477: error_reporting(E_ALL | E_STRICT);
478: }
479:
480: if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) {
481: self::$lastError = new ErrorException($message, 0, $severity, $file, $line);
482: return NULL;
483: }
484:
485: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
486: if (DebugHelpers::findTrace(PHP_VERSION_ID < 50205 ? debug_backtrace() :PHP_VERSION_ID < 50205 ? debug_backtrace() : debug_backtrace(FALSE), '*::__toString')) {
487: $previous = isset($context['e']) && $context['e'] instanceof Exception ? $context['e'] : NULL;
488: self::_exceptionHandler(new FatalErrorException($message, 0, $severity, $file, $line, $context, $previous));
489: }
490: throw new FatalErrorException($message, 0, $severity, $file, $line, $context);
491:
492: } elseif (($severity & error_reporting()) !== $severity) {
493: return FALSE;
494:
495: } elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
496: self::_exceptionHandler(new FatalErrorException($message, 0, $severity, $file, $line, $context));
497: }
498:
499: static $types = array(
500: E_WARNING => 'Warning',
501: E_COMPILE_WARNING => 'Warning',
502: E_USER_WARNING => 'Warning',
503: E_NOTICE => 'Notice',
504: E_USER_NOTICE => 'Notice',
505: E_STRICT => 'Strict standards',
506: E_DEPRECATED => 'Deprecated',
507: E_USER_WARNING => 'Deprecated',
508: );
509:
510: $message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
511: $count = & self::$errorPanel->data["$message|$file|$line"];
512:
513: if ($count++) {
514: return NULL;
515:
516: } elseif (self::$productionMode) {
517: self::log("$message in $file:$line", self::ERROR);
518: return NULL;
519:
520: } else {
521: $ok = self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
522: return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
523: }
524:
525: return FALSE;
526: }
527:
528:
529:
530: 531: 532: 533: 534:
535: public static function toStringException(Exception $exception)
536: {
537: if (self::$enabled) {
538: self::_exceptionHandler($exception);
539: } else {
540: trigger_error($exception->getMessage(), E_USER_ERROR);
541: }
542: }
543:
544:
545:
546: 547: 548: 549:
550: public static function tryError()
551: {
552: if (!self::$enabled && self::$lastError === FALSE) {
553: set_error_handler(array(__CLASS__, '_errorHandler'));
554: }
555: self::$lastError = NULL;
556: }
557:
558:
559:
560: 561: 562: 563: 564:
565: public static function catchError(& $error)
566: {
567: if (!self::$enabled && self::$lastError !== FALSE) {
568: restore_error_handler();
569: }
570: $error = self::$lastError;
571: self::$lastError = FALSE;
572: return (bool) $error;
573: }
574:
575:
576:
577:
578:
579:
580:
581: 582: 583: 584: 585: 586:
587: public static function dump($var, $return = FALSE)
588: {
589: if (!$return && self::$productionMode) {
590: return $var;
591: }
592:
593: $output = "<pre class=\"nette-dump\">" . DebugHelpers::htmlDump($var) . "</pre>\n";
594:
595: if (!$return) {
596: $trace = PHP_VERSION_ID < 50205 ? debug_backtrace() :PHP_VERSION_ID < 50205 ? debug_backtrace() : debug_backtrace(FALSE);
597: $item = ($tmp=DebugHelpers::findTrace($trace, 'dump')) ? $tmp : DebugHelpers::findTrace($trace, __CLASS__ . '::dump');
598: if (isset($item['file'], $item['line']) && is_file($item['file'])) {
599: $lines = file($item['file']);
600: preg_match('#dump\((.*)\)#', $lines[$item['line'] - 1], $m);
601: $output = substr_replace(
602: $output,
603: ' title="' . htmlspecialchars((isset($m[0]) ? "$m[0] \n" : '') . "in file {$item['file']} on line {$item['line']}") . '"',
604: 4, 0);
605:
606: if (self::$showLocation) {
607: $output = substr_replace(
608: $output,
609: ' <small>in ' . DebugHelpers::editorLink($item['file'], $item['line']) . ":{$item['line']}</small>",
610: -8, 0);
611: }
612: }
613: }
614:
615: if (self::$consoleMode) {
616: if (self::$consoleColors && substr(getenv('TERM'), 0, 5) === 'xterm') {
617: $output = preg_replace_callback('#<span class="php-(\w+)">|</span>#', create_function('$m', '
618: return "\\033[" . (isset($m[1], Debugger::$consoleColors[$m[1]]) ? Debugger::$consoleColors[$m[1]] : \'0\') . "m";
619: '), $output);
620: }
621: $output = htmlspecialchars_decode(strip_tags($output), ENT_QUOTES);
622: }
623:
624: if ($return) {
625: return $output;
626:
627: } else {
628: echo $output;
629: return $var;
630: }
631: }
632:
633:
634:
635: 636: 637: 638: 639:
640: public static function timer($name = NULL)
641: {
642: static $time = array();
643: $now = microtime(TRUE);
644: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
645: $time[$name] = $now;
646: return $delta;
647: }
648:
649:
650:
651: 652: 653: 654: 655: 656:
657: public static function barDump($var, $title = NULL)
658: {
659: if (!self::$productionMode) {
660: $dump = array();
661: foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
662: $dump[$key] = DebugHelpers::clickableDump($val);
663: }
664: self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
665: }
666: return $var;
667: }
668:
669:
670:
671: 672: 673: 674: 675:
676: public static function fireLog($message)
677: {
678: if (!self::$productionMode) {
679: return self::$fireLogger->log($message);
680: }
681: }
682:
683:
684:
685: private static function isHtmlMode()
686: {
687: return !self::$ajaxDetected && !self::$consoleMode
688: && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
689: }
690:
691:
692:
693:
694: public static function addPanel(IBarPanel $panel, $id = NULL)
695: {
696: return self::$bar->addPanel($panel, $id);
697: }
698:
699: }
700: