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 NDebugger
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: final public function __construct()
143: {
144: throw new NStaticClassException;
145: }
146:
147:
148: 149: 150: 151:
152: public static function _init()
153: {
154: self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
155: self::$consoleMode = PHP_SAPI === 'cli';
156: self::$productionMode = self::DETECT;
157: if (self::$consoleMode) {
158: self::$source = empty($_SERVER['argv']) ? 'cli' : 'cli: ' . implode(' ', $_SERVER['argv']);
159: } else {
160: self::$ajaxDetected = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
161: if (isset($_SERVER['REQUEST_URI'])) {
162: self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
163: . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
164: . $_SERVER['REQUEST_URI'];
165: }
166: }
167:
168: self::$logger = new NLogger;
169: self::$logDirectory = & self::$logger->directory;
170: self::$email = & self::$logger->email;
171: self::$mailer = & self::$logger->mailer;
172: self::$emailSnooze = & NLogger::$emailSnooze;
173:
174: self::$fireLogger = new NFireLogger;
175:
176: self::$blueScreen = new NDebugBlueScreen;
177: self::$blueScreen->addPanel(create_function('$e', '
178: if ($e instanceof NTemplateException) {
179: return array(
180: \'tab\' => \'Template\',
181: \'panel\' => \'<p><b>File:</b> \' . NDebugHelpers::editorLink($e->sourceFile, $e->sourceLine)
182: . \' <b>Line:</b> \' . ($e->sourceLine ? $e->sourceLine : \'n/a\') . \'</p>\'
183: . ($e->sourceLine ? NDebugBlueScreen::highlightFile($e->sourceFile, $e->sourceLine) : \'\')
184: );
185: } elseif ($e instanceof NNeonException && preg_match(\'#line (\\d+)#\', $e->getMessage(), $m)) {
186: if ($item = NDebugHelpers::findTrace($e->getTrace(), \'NConfigNeonAdapter::load\')) {
187: return array(
188: \'tab\' => \'NEON\',
189: \'panel\' => \'<p><b>File:</b> \' . NDebugHelpers::editorLink($item[\'args\'][0], $m[1]) . \' <b>Line:</b> \' . $m[1] . \'</p>\'
190: . NDebugBlueScreen::highlightFile($item[\'args\'][0], $m[1])
191: );
192: } elseif ($item = NDebugHelpers::findTrace($e->getTrace(), \'NNeon::decode\')) {
193: return array(
194: \'tab\' => \'NEON\',
195: \'panel\' => NDebugBlueScreen::highlightPhp($item[\'args\'][0], $m[1])
196: );
197: }
198: }
199: '));
200:
201: self::$bar = new NDebugBar;
202: self::$bar->addPanel(new NDefaultBarPanel('time'));
203: self::$bar->addPanel(new NDefaultBarPanel('memory'));
204: self::$bar->addPanel(self::$errorPanel = new NDefaultBarPanel('errors'));
205: self::$bar->addPanel(self::$dumpPanel = new NDefaultBarPanel('dumps'));
206: }
207:
208:
209:
210:
211:
212: 213: 214: 215: 216: 217: 218:
219: public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
220: {
221: error_reporting(E_ALL | E_STRICT);
222:
223:
224: if (is_bool($mode)) {
225: self::$productionMode = $mode;
226:
227: } elseif ($mode !== self::DETECT || self::$productionMode === NULL) {
228: $list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
229: if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
230: $list[] = '127.0.0.1';
231: $list[] = '::1';
232: }
233: self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
234: }
235:
236:
237: if (is_string($logDirectory)) {
238: self::$logDirectory = realpath($logDirectory);
239: if (self::$logDirectory === FALSE) {
240: echo __METHOD__ . "() error: Log directory is not found or is not directory.\n";
241: exit(254);
242: }
243: } elseif ($logDirectory === FALSE) {
244: self::$logDirectory = FALSE;
245:
246: } elseif (self::$logDirectory === NULL) {
247: self::$logDirectory = defined('APP_DIR') ? APP_DIR . '/../log' : getcwd() . '/log';
248: }
249: if (self::$logDirectory) {
250: ini_set('error_log', self::$logDirectory . '/php_error.log');
251: }
252:
253:
254: if (function_exists('ini_set')) {
255: ini_set('display_errors', !self::$productionMode);
256: ini_set('html_errors', FALSE);
257: ini_set('log_errors', FALSE);
258:
259: } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) {
260: echo __METHOD__ . "() error: Unable to set 'display_errors' because function ini_set() is disabled.\n";
261: exit(254);
262: }
263:
264: if ($email) {
265: if (!is_string($email)) {
266: echo __METHOD__ . "() error: Email address must be a string.\n";
267: exit(254);
268: }
269: self::$email = $email;
270: }
271:
272: if (!defined('E_DEPRECATED')) {
273: define('E_DEPRECATED', 8192);
274: }
275:
276: if (!defined('E_USER_WARNING')) {
277: define('E_USER_WARNING', 16384);
278: }
279:
280: if (!self::$enabled) {
281: register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
282: set_exception_handler(array(__CLASS__, '_exceptionHandler'));
283: set_error_handler(array(__CLASS__, '_errorHandler'));
284: self::$enabled = TRUE;
285: }
286: }
287:
288:
289: 290: 291: 292:
293: public static function isEnabled()
294: {
295: return self::$enabled;
296: }
297:
298:
299: 300: 301: 302: 303: 304:
305: public static function log($message, $priority = self::INFO)
306: {
307: if (self::$logDirectory === FALSE) {
308: return;
309:
310: } elseif (!self::$logDirectory) {
311: throw new InvalidStateException('Logging directory is not specified in NDebugger::$logDirectory.');
312: }
313:
314: if ($message instanceof Exception) {
315: $exception = $message;
316: $message = ($message instanceof FatalErrorException
317: ? 'Fatal error: ' . $exception->getMessage()
318: : get_class($exception) . ": " . $exception->getMessage())
319: . " in " . $exception->getFile() . ":" . $exception->getLine();
320:
321: $hash = md5(preg_replace('~(Resource id #)\d+~', '$1', $exception . (method_exists($exception, 'getPrevious') ? $exception->getPrevious() : (isset($exception->previous) ? $exception->previous : ''))));
322: $exceptionFilename = "exception-" . @date('Y-m-d-H-i-s') . "-$hash.html";
323: foreach (new DirectoryIterator(self::$logDirectory) as $entry) {
324: if (strpos($entry, $hash)) {
325: $exceptionFilename = $entry;
326: $saved = TRUE;
327: break;
328: }
329: }
330: }
331:
332: self::$logger->log(array(
333: @date('[Y-m-d H-i-s]'),
334: trim($message),
335: self::$source ? ' @ ' . self::$source : NULL,
336: !empty($exceptionFilename) ? ' @@ ' . $exceptionFilename : NULL
337: ), $priority);
338:
339: if (!empty($exceptionFilename)) {
340: $exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
341: if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
342: ob_start();
343: ob_start(create_function('$buffer', 'extract($GLOBALS[0]['.array_push($GLOBALS[0], array('logHandle'=>$logHandle)).'-1], EXTR_REFS); fwrite($logHandle, $buffer); '), 4096);
344: self::$blueScreen->render($exception);
345: ob_end_flush();
346: ob_end_clean();
347: fclose($logHandle);
348: }
349: return strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR);
350: }
351: }
352:
353:
354: 355: 356: 357: 358:
359: public static function _shutdownHandler()
360: {
361: if (!self::$enabled) {
362: return;
363: }
364:
365:
366: static $types = array(
367: E_ERROR => 1,
368: E_CORE_ERROR => 1,
369: E_COMPILE_ERROR => 1,
370: E_PARSE => 1,
371: );
372: $error = error_get_last();
373: if (isset($types[$error['type']])) {
374: self::_exceptionHandler(new FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL));
375: }
376:
377:
378: if (self::$bar && !self::$productionMode && self::isHtmlMode()) {
379: self::$bar->render();
380: }
381: }
382:
383:
384: 385: 386: 387: 388: 389:
390: public static function _exceptionHandler(Exception $exception)
391: {
392: if (!headers_sent()) {
393: $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
394: $code = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE ? 503 : 500;
395: header("$protocol $code", TRUE, $code);
396: }
397:
398: try {
399: if (self::$productionMode) {
400: try {
401: self::log($exception, self::ERROR);
402: } catch (Exception $e) {
403: echo 'FATAL ERROR: unable to log error';
404: }
405:
406: if (self::$consoleMode) {
407: echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
408:
409: } elseif (self::isHtmlMode()) {
410: require dirname(__FILE__) . '/templates/error.phtml';
411: }
412:
413: } else {
414: if (self::$consoleMode) {
415: echo "$exception\n";
416: if ($file = self::log($exception)) {
417: echo "(stored in $file)\n";
418: if (self::$browser) {
419: exec(self::$browser . ' ' . escapeshellarg($file));
420: }
421: }
422:
423: } elseif (self::isHtmlMode()) {
424: self::$blueScreen->render($exception);
425: if (self::$bar) {
426: self::$bar->render();
427: }
428:
429: } elseif (!self::fireLog($exception)) {
430: $file = self::log($exception, self::ERROR);
431: if (!headers_sent()) {
432: header("X-Nette-Error-Log: $file");
433: }
434: }
435: }
436:
437: foreach (self::$onFatalError as $handler) {
438: call_user_func($handler, $exception);
439: }
440:
441: } catch (Exception $e) {
442: if (self::$productionMode) {
443: echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
444: } else {
445: echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
446: "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
447: }
448: }
449:
450: self::$enabled = FALSE;
451: exit(254);
452: }
453:
454:
455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465:
466: public static function _errorHandler($severity, $message, $file, $line, $context)
467: {
468: if (self::$scream) {
469: error_reporting(E_ALL | E_STRICT);
470: }
471:
472: if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) {
473: self::$lastError = new ErrorException($message, 0, $severity, $file, $line);
474: return NULL;
475: }
476:
477: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
478: if (NDebugHelpers::findTrace(PHP_VERSION_ID < 50205 ? debug_backtrace() : debug_backtrace(FALSE), '*::__toString')) {
479: $previous = isset($context['e']) && $context['e'] instanceof Exception ? $context['e'] : NULL;
480: self::_exceptionHandler(new FatalErrorException($message, 0, $severity, $file, $line, $context, $previous));
481: }
482: throw new FatalErrorException($message, 0, $severity, $file, $line, $context);
483:
484: } elseif (($severity & error_reporting()) !== $severity) {
485: return FALSE;
486:
487: } elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
488: self::_exceptionHandler(new FatalErrorException($message, 0, $severity, $file, $line, $context));
489: }
490:
491: static $types = array(
492: E_WARNING => 'Warning',
493: E_COMPILE_WARNING => 'Warning',
494: E_USER_WARNING => 'Warning',
495: E_NOTICE => 'Notice',
496: E_USER_NOTICE => 'Notice',
497: E_STRICT => 'Strict standards',
498: E_DEPRECATED => 'Deprecated',
499: E_USER_WARNING => 'Deprecated',
500: );
501:
502: $message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
503: $count = & self::$errorPanel->data["$file|$line|$message"];
504:
505: if ($count++) {
506: return NULL;
507:
508: } elseif (self::$productionMode) {
509: self::log("$message in $file:$line", self::ERROR);
510: return NULL;
511:
512: } else {
513: $ok = self::fireLog(new ErrorException($message, 0, $severity, $file, $line));
514: return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
515: }
516:
517: return FALSE;
518: }
519:
520:
521: 522: 523: 524: 525:
526: public static function toStringException(Exception $exception)
527: {
528: if (self::$enabled) {
529: self::_exceptionHandler($exception);
530: } else {
531: trigger_error($exception->getMessage(), E_USER_ERROR);
532: }
533: }
534:
535:
536: 537: 538: 539:
540: public static function tryError()
541: {
542: if (!self::$enabled && self::$lastError === FALSE) {
543: set_error_handler(array(__CLASS__, '_errorHandler'));
544: }
545: self::$lastError = NULL;
546: }
547:
548:
549: 550: 551: 552: 553:
554: public static function catchError(& $error)
555: {
556: if (!self::$enabled && self::$lastError !== FALSE) {
557: restore_error_handler();
558: }
559: $error = self::$lastError;
560: self::$lastError = FALSE;
561: return (bool) $error;
562: }
563:
564:
565:
566:
567:
568: 569: 570: 571: 572: 573:
574: public static function dump($var, $return = FALSE)
575: {
576: if (!$return && self::$productionMode) {
577: return $var;
578: }
579:
580: $output = "<pre class=\"nette-dump\">" . NDebugHelpers::htmlDump($var) . "</pre>\n";
581:
582: if (!$return) {
583: $trace = PHP_VERSION_ID < 50205 ? debug_backtrace() : debug_backtrace(FALSE);
584: $item = ($tmp=NDebugHelpers::findTrace($trace, 'dump')) ? $tmp : NDebugHelpers::findTrace($trace, __CLASS__ . '::dump');
585: if (isset($item['file'], $item['line']) && is_file($item['file'])) {
586: $lines = file($item['file']);
587: preg_match('#dump\((.*)\)#', $lines[$item['line'] - 1], $m);
588: $output = substr_replace(
589: $output,
590: ' title="' . htmlspecialchars((isset($m[0]) ? "$m[0] \n" : '') . "in file {$item['file']} on line {$item['line']}") . '"',
591: 4, 0);
592:
593: if (self::$showLocation) {
594: $output = substr_replace(
595: $output,
596: ' <small>in ' . NDebugHelpers::editorLink($item['file'], $item['line']) . ":{$item['line']}</small>",
597: -8, 0);
598: }
599: }
600: }
601:
602: if (self::$consoleMode) {
603: if (self::$consoleColors && substr(getenv('TERM'), 0, 5) === 'xterm') {
604: $output = preg_replace_callback('#<span class="php-(\w+)">|</span>#', create_function('$m', '
605: return "\\033[" . (isset($m[1], NDebugger::$consoleColors[$m[1]]) ? NDebugger::$consoleColors[$m[1]] : \'0\') . "m";
606: '), $output);
607: }
608: $output = htmlspecialchars_decode(strip_tags($output), ENT_QUOTES);
609: }
610:
611: if ($return) {
612: return $output;
613:
614: } else {
615: echo $output;
616: return $var;
617: }
618: }
619:
620:
621: 622: 623: 624: 625:
626: public static function timer($name = NULL)
627: {
628: static $time = array();
629: $now = microtime(TRUE);
630: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
631: $time[$name] = $now;
632: return $delta;
633: }
634:
635:
636: 637: 638: 639: 640: 641:
642: public static function barDump($var, $title = NULL)
643: {
644: if (!self::$productionMode) {
645: $dump = array();
646: foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
647: $dump[$key] = NDebugHelpers::clickableDump($val);
648: }
649: self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
650: }
651: return $var;
652: }
653:
654:
655: 656: 657: 658: 659:
660: public static function fireLog($message)
661: {
662: if (!self::$productionMode) {
663: return self::$fireLogger->log($message);
664: }
665: }
666:
667:
668: private static function isHtmlMode()
669: {
670: return !self::$ajaxDetected && !self::$consoleMode
671: && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
672: }
673:
674:
675:
676: public static function addPanel(IBarPanel $panel, $id = NULL)
677: {
678: return self::$bar->addPanel($panel, $id);
679: }
680:
681: }
682: