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