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