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