1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette;
13:
14: use Nette,
15: Nette\Environment;
16:
17:
18:
19: 20: 21: 22: 23: 24: 25: 26: 27:
28: final class Debug
29: {
30:
31: public static $productionMode;
32:
33:
34: public static $consoleMode;
35:
36:
37: public static $time;
38:
39:
40: private static $firebugDetected;
41:
42:
43: private static $ajaxDetected;
44:
45:
46: public static $source;
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:
61:
62: const DEVELOPMENT = FALSE,
63: PRODUCTION = TRUE,
64: DETECT = NULL;
65:
66:
67: public static $strictMode = FALSE; 68:
69:
70: public static $scream = FALSE;
71:
72:
73: public static $onFatalError = array();
74:
75:
76: public static $logDirectory;
77:
78:
79: public static $email;
80:
81:
82: public static $mailer = array(__CLASS__, 'defaultMailer');
83:
84:
85: public static $emailSnooze = 172800;
86:
87:
88: public static $editor = 'editor://open/?file=%file&line=%line';
89:
90:
91: private static $enabled = FALSE;
92:
93:
94: private static $lastError = FALSE;
95:
96:
97:
98:
99: public static $showBar = TRUE;
100:
101:
102: private static $panels = array();
103:
104:
105: private static $errors;
106:
107:
108:
109:
110: const DEBUG = 'debug',
111: INFO = 'info',
112: WARNING = 'warning',
113: ERROR = 'error',
114: CRITICAL = 'critical';
115:
116:
117:
118: 119: 120:
121: final public function __construct()
122: {
123: throw new \LogicException("Cannot instantiate static class " . get_class($this));
124: }
125:
126:
127:
128: 129: 130: 131:
132: public static function _init()
133: {
134: self::$time = microtime(TRUE);
135: self::$consoleMode = PHP_SAPI === 'cli';
136: self::$productionMode = self::DETECT;
137: if (self::$consoleMode) {
138: self::$source = empty($_SERVER['argv']) ? 'cli' : 'cli: ' . implode(' ', $_SERVER['argv']);
139: } else {
140: self::$firebugDetected = isset($_SERVER['HTTP_X_FIRELOGGER']);
141: self::$ajaxDetected = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
142: if (isset($_SERVER['REQUEST_URI'])) {
143: self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
144: . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
145: . $_SERVER['REQUEST_URI'];
146: }
147: }
148:
149: $tab = array('Nette\DebugHelpers', 'renderTab'); $panel = array('Nette\DebugHelpers', 'renderPanel');
150: self::addPanel(new DebugPanel('time', $tab, $panel));
151: self::addPanel(new DebugPanel('memory', $tab, $panel));
152: self::addPanel($tmp = new DebugPanel('errors', $tab, $panel)); $tmp->data = & self::$errors;
153: self::addPanel(new DebugPanel('dumps', $tab, $panel));
154: }
155:
156:
157:
158:
159:
160:
161:
162: 163: 164: 165: 166: 167: 168:
169: public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
170: {
171: error_reporting(E_ALL | E_STRICT);
172:
173: 174: if (is_bool($mode)) {
175: self::$productionMode = $mode;
176:
177: } elseif (is_string($mode)) { 178: $mode = preg_split('#[,\s]+#', "$mode 127.0.0.1 ::1");
179: }
180:
181: if (is_array($mode)) { 182: self::$productionMode = !isset($_SERVER['REMOTE_ADDR']) || !in_array($_SERVER['REMOTE_ADDR'], $mode, TRUE);
183: }
184:
185: if (self::$productionMode === self::DETECT) {
186: if (class_exists('Nette\Environment')) {
187: self::$productionMode = Environment::isProduction();
188:
189: } elseif (isset($_SERVER['SERVER_ADDR']) || isset($_SERVER['LOCAL_ADDR'])) { 190: $addrs = array();
191: if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { 192: $addrs = preg_split('#,\s*#', $_SERVER['HTTP_X_FORWARDED_FOR']);
193: }
194: if (isset($_SERVER['REMOTE_ADDR'])) {
195: $addrs[] = $_SERVER['REMOTE_ADDR'];
196: }
197: $addrs[] = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : $_SERVER['LOCAL_ADDR'];
198: self::$productionMode = FALSE;
199: foreach ($addrs as $addr) {
200: $oct = explode('.', $addr);
201: if ($addr !== '::1' && (count($oct) !== 4 || ($oct[0] !== '10' && $oct[0] !== '127' && ($oct[0] !== '172' || $oct[1] < 16 || $oct[1] > 31)
202: && ($oct[0] !== '169' || $oct[1] !== '254') && ($oct[0] !== '192' || $oct[1] !== '168')))
203: ) {
204: self::$productionMode = TRUE;
205: break;
206: }
207: }
208:
209: } else {
210: self::$productionMode = !self::$consoleMode;
211: }
212: }
213:
214: 215: if (is_string($logDirectory)) {
216: self::$logDirectory = realpath($logDirectory);
217: if (self::$logDirectory === FALSE) {
218: throw new \DirectoryNotFoundException("Directory '$logDirectory' is not found.");
219: }
220: } elseif ($logDirectory === FALSE) {
221: self::$logDirectory = FALSE;
222:
223: } else {
224: self::$logDirectory = defined('APP_DIR') ? APP_DIR . '/../log' : getcwd() . '/log';
225: }
226: if (self::$logDirectory) {
227: ini_set('error_log', self::$logDirectory . '/php_error.log');
228: }
229:
230: 231: if (function_exists('ini_set')) {
232: ini_set('display_errors', !self::$productionMode); 233: ini_set('html_errors', FALSE);
234: ini_set('log_errors', FALSE);
235:
236: } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { 237: throw new \NotSupportedException('Function ini_set() must be enabled.');
238: }
239:
240: if ($email) {
241: if (!is_string($email)) {
242: throw new \InvalidArgumentException('Email address must be a string.');
243: }
244: self::$email = $email;
245: }
246:
247: if (!defined('E_DEPRECATED')) {
248: define('E_DEPRECATED', 8192);
249: }
250:
251: if (!defined('E_USER_DEPRECATED')) {
252: define('E_USER_DEPRECATED', 16384);
253: }
254:
255: if (!self::$enabled) {
256: register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
257: set_exception_handler(array(__CLASS__, '_exceptionHandler'));
258: set_error_handler(array(__CLASS__, '_errorHandler'));
259: self::$enabled = TRUE;
260: }
261: }
262:
263:
264:
265: 266: 267: 268:
269: public static function isEnabled()
270: {
271: return self::$enabled;
272: }
273:
274:
275:
276: 277: 278: 279: 280: 281:
282: public static function log($message, $priority = self::INFO)
283: {
284: if (self::$logDirectory === FALSE) {
285: return;
286:
287: } elseif (!self::$logDirectory) {
288: throw new \InvalidStateException('Logging directory is not specified in Nette\Debug::$logDirectory.');
289:
290: } elseif (!is_dir(self::$logDirectory)) {
291: throw new \DirectoryNotFoundException("Directory '" . self::$logDirectory . "' is not found or is not directory.");
292: }
293:
294: if ($message instanceof \Exception) {
295: $exception = $message;
296: $message = "PHP Fatal error: "
297: . ($message instanceof \FatalErrorException
298: ? $exception->getMessage()
299: : "Uncaught exception " . get_class($exception) . " with message '" . $exception->getMessage() . "'")
300: . " in " . $exception->getFile() . ":" . $exception->getLine();
301:
302: $hash = md5($exception );
303: $exceptionFilename = "exception " . @date('Y-m-d H-i-s') . " $hash.html";
304: foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
305: if (strpos($entry, $hash)) {
306: $exceptionFilename = NULL; break;
307: }
308: }
309: }
310:
311: error_log(
312: @date('[Y-m-d H-i-s] ') . trim($message) .
313: (self::$source ? ' @ ' . self::$source : '') .
314: (!empty($exceptionFilename) ? ' @@ ' . $exceptionFilename : '') . PHP_EOL,
315: 3, self::$logDirectory . '/' . strtolower($priority) . '.log'
316: );
317:
318: if (($priority === self::ERROR || $priority === self::CRITICAL) && self::$email
319: && @filemtime(self::$logDirectory . '/email-sent') + self::$emailSnooze < time() 320: && @file_put_contents(self::$logDirectory . '/email-sent', 'sent') 321: ) {
322: call_user_func(self::$mailer, $message);
323: }
324:
325: if (!empty($exceptionFilename) && $logHandle = @fopen(self::$logDirectory . '/'. $exceptionFilename, 'w')) {
326: ob_start(); 327: ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 1);
328: DebugHelpers::renderBlueScreen($exception);
329: ob_end_flush();
330: ob_end_clean();
331: fclose($logHandle);
332: }
333: }
334:
335:
336:
337: 338: 339: 340: 341:
342: public static function _shutdownHandler()
343: {
344: 345: static $types = array(
346: E_ERROR => 1,
347: E_CORE_ERROR => 1,
348: E_COMPILE_ERROR => 1,
349: E_PARSE => 1,
350: );
351: $error = error_get_last();
352: if (isset($types[$error['type']])) {
353: self::_exceptionHandler(new \FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL));
354: return;
355: }
356:
357: 358: if (self::$showBar && !self::$productionMode && !self::$ajaxDetected && !self::$consoleMode
359: && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))
360: ) {
361: DebugHelpers::renderDebugBar(self::$panels);
362: }
363: }
364:
365:
366:
367: 368: 369: 370: 371: 372:
373: public static function _exceptionHandler(\Exception $exception)
374: {
375: if (!headers_sent()) { 376: header('HTTP/1.1 500 Internal Server Error');
377: }
378:
379: $htmlMode = !self::$ajaxDetected && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
380:
381: try {
382: if (self::$productionMode) {
383: self::log($exception, self::ERROR);
384:
385: if (self::$consoleMode) {
386: echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
387:
388: } elseif ($htmlMode) {
389: require __DIR__ . '/templates/error.phtml';
390: }
391:
392: } else {
393: if (self::$consoleMode) { 394: echo "$exception\n";
395:
396: } elseif ($htmlMode) { 397: DebugHelpers::renderBlueScreen($exception);
398:
399: } elseif (!self::fireLog($exception, self::ERROR)) { 400: self::log($exception);
401: }
402: }
403:
404: foreach (self::$onFatalError as $handler) {
405: call_user_func($handler, $exception);
406: }
407: } catch (\Exception $e) {
408: echo "\nNette\\Debug FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
409: "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
410: exit;
411: }
412: }
413:
414:
415:
416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426:
427: public static function _errorHandler($severity, $message, $file, $line, $context)
428: {
429: if (self::$scream) {
430: error_reporting(E_ALL | E_STRICT);
431: }
432:
433: if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { 434: self::$lastError = new \ErrorException($message, 0, $severity, $file, $line);
435: return NULL;
436: }
437:
438: if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
439: throw new \FatalErrorException($message, 0, $severity, $file, $line, $context);
440:
441: } elseif (($severity & error_reporting()) !== $severity) {
442: return FALSE; 443:
444: } elseif (self::$strictMode && !self::$productionMode) {
445: self::_exceptionHandler(new \FatalErrorException($message, 0, $severity, $file, $line, $context));
446: exit;
447: }
448:
449: static $types = array(
450: E_WARNING => 'Warning',
451: E_COMPILE_WARNING => 'Warning', 452: E_USER_WARNING => 'Warning',
453: E_NOTICE => 'Notice',
454: E_USER_NOTICE => 'Notice',
455: E_STRICT => 'Strict standards',
456: E_DEPRECATED => 'Deprecated',
457: E_USER_DEPRECATED => 'Deprecated',
458: );
459:
460: $message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
461: $count = & self::$errors["$message|$file|$line"];
462:
463: if ($count++) { 464: return NULL;
465:
466: } elseif (self::$productionMode) {
467: self::log("$message in $file:$line", self::ERROR);
468: return NULL;
469:
470: } else {
471: $ok = self::fireLog(new \ErrorException($message, 0, $severity, $file, $line), self::WARNING);
472: return self::$consoleMode || (!self::$showBar && !$ok) ? FALSE : NULL;
473: }
474:
475: return FALSE; 476: }
477:
478:
479:
480:
481: public static function processException(\Exception $exception)
482: {
483: trigger_error(__METHOD__ . '() is deprecated; use ' . __CLASS__ . '::log($exception, Debug::ERROR) instead.', E_USER_WARNING);
484: self::log($exception, self::ERROR);
485: }
486:
487:
488:
489: 490: 491: 492: 493:
494: public static function toStringException(\Exception $exception)
495: {
496: if (self::$enabled) {
497: self::_exceptionHandler($exception);
498: } else {
499: trigger_error($exception->getMessage(), E_USER_ERROR);
500: }
501: exit;
502: }
503:
504:
505:
506: 507: 508: 509:
510: public static function tryError()
511: {
512: if (!self::$enabled && self::$lastError === FALSE) {
513: set_error_handler(array(__CLASS__, '_errorHandler'));
514: }
515: self::$lastError = NULL;
516: }
517:
518:
519:
520: 521: 522: 523: 524:
525: public static function catchError(& $error)
526: {
527: if (!self::$enabled && self::$lastError !== FALSE) {
528: restore_error_handler();
529: }
530: $error = self::$lastError;
531: self::$lastError = FALSE;
532: return (bool) $error;
533: }
534:
535:
536:
537: 538: 539: 540: 541:
542: private static function defaultMailer($message)
543: {
544: $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] :
545: (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '');
546:
547: $parts = str_replace(
548: array("\r\n", "\n"),
549: array("\n", PHP_EOL),
550: array(
551: 'headers' => "From: noreply@$host\nX-Mailer: Nette Framework\n",
552: 'subject' => "PHP: An error occurred on the server $host",
553: 'body' => "[" . @date('Y-m-d H:i:s') . "] $message", 554: )
555: );
556:
557: mail(self::$email, $parts['subject'], $parts['body'], $parts['headers']);
558: }
559:
560:
561:
562:
563:
564:
565:
566: 567: 568: 569: 570: 571:
572: public static function dump($var, $return = FALSE)
573: {
574: if (!$return && self::$productionMode) {
575: return $var;
576: }
577:
578: $output = "<pre class=\"nette-dump\">" . DebugHelpers::htmlDump($var) . "</pre>\n";
579:
580: if (!$return && self::$showLocation) {
581: $trace = debug_backtrace();
582: $i = isset($trace[1]['class']) && $trace[1]['class'] === __CLASS__ ? 1 : 0;
583: if (isset($trace[$i]['file'], $trace[$i]['line'])) {
584: $output = substr_replace(
585: $output,
586: ' <small>' . htmlspecialchars("in file {$trace[$i]['file']} on line {$trace[$i]['line']}", ENT_NOQUOTES) . '</small>',
587: -8, 0);
588: }
589: }
590:
591: if (self::$consoleMode) {
592: $output = htmlspecialchars_decode(strip_tags($output), ENT_NOQUOTES);
593: }
594:
595: if ($return) {
596: return $output;
597:
598: } else {
599: echo $output;
600: return $var;
601: }
602: }
603:
604:
605:
606: 607: 608: 609: 610:
611: public static function timer($name = NULL)
612: {
613: static $time = array();
614: $now = microtime(TRUE);
615: $delta = isset($time[$name]) ? $now - $time[$name] : 0;
616: $time[$name] = $now;
617: return $delta;
618: }
619:
620:
621:
622:
623:
624:
625:
626: 627: 628: 629: 630:
631: public static function addPanel(IDebugPanel $panel)
632: {
633: self::$panels[] = $panel;
634: }
635:
636:
637:
638: 639: 640: 641: 642: 643:
644: public static function barDump($var, $title = NULL)
645: {
646: if (!self::$productionMode) {
647: $dump = array();
648: foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
649: $dump[$key] = DebugHelpers::clickableDump($val);
650: }
651: self::$panels[3]->data[] = array('title' => $title, 'dump' => $dump);
652: }
653: return $var;
654: }
655:
656:
657:
658:
659:
660:
661:
662: 663: 664: 665: 666: 667:
668: public static function fireLog($message)
669: {
670: if (self::$productionMode) {
671: return;
672:
673: } elseif (!self::$firebugDetected || headers_sent()) {
674: return FALSE;
675: }
676:
677: static $payload = array('logs' => array());
678:
679: $item = array(
680: 'name' => 'PHP',
681: 'level' => 'debug',
682: 'order' => count($payload['logs']),
683: 'time' => str_pad(number_format((microtime(TRUE) - self::$time) * 1000, 1, '.', ' '), 8, '0', STR_PAD_LEFT) . ' ms',
684: 'template' => '',
685: 'message' => '',
686: 'style' => 'background:#767ab6',
687: );
688:
689: $args = func_get_args();
690: if (isset($args[0]) && is_string($args[0])) {
691: $item['template'] = array_shift($args);
692: }
693:
694: if (isset($args[0]) && $args[0] instanceof \Exception) {
695: $e = array_shift($args);
696: $trace = $e->getTrace();
697: if (isset($trace[0]['class']) && $trace[0]['class'] === __CLASS__
698: && ($trace[0]['function'] === '_shutdownHandler' || $trace[0]['function'] === '_errorHandler')
699: ) {
700: unset($trace[0]);
701: }
702:
703: $item['exc_info'] = array(
704: $e->getMessage(),
705: $e->getFile(),
706: array(),
707: );
708: $item['exc_frames'] = array();
709:
710: foreach ($trace as $frame) {
711: $frame += array('file' => NULL, 'line' => NULL, 'class' => NULL, 'type' => NULL, 'function' => NULL, 'object' => NULL, 'args' => NULL);
712: $item['exc_info'][2][] = array($frame['file'], $frame['line'], "$frame[class]$frame[type]$frame[function]", $frame['object']);
713: $item['exc_frames'][] = $frame['args'];
714: }
715:
716: $file = str_replace(dirname(dirname(dirname($e->getFile()))), "\xE2\x80\xA6", $e->getFile());
717: $item['template'] = ($e instanceof \ErrorException ? '' : get_class($e) . ': ')
718: . $e->getMessage() . ($e->getCode() ? ' #' . $e->getCode() : '') . ' in ' . $file . ':' . $e->getLine();
719: array_unshift($trace, array('file' => $e->getFile(), 'line' => $e->getLine()));
720:
721: } else {
722: $trace = debug_backtrace();
723: if (isset($trace[0]['class']) && $trace[0]['class'] === __CLASS__
724: && ($trace[0]['function'] === '_shutdownHandler' || $trace[0]['function'] === '_errorHandler')
725: ) {
726: unset($trace[0]);
727: }
728: }
729:
730: if (isset($args[0]) && in_array($args[0], array(self::DEBUG, self::INFO, self::WARNING, self::ERROR, self::CRITICAL), TRUE)) {
731: $item['level'] = array_shift($args);
732: }
733:
734: $item['args'] = $args;
735:
736: foreach ($trace as $frame) {
737: if (isset($frame['file']) && is_file($frame['file'])) {
738: $item['pathname'] = $frame['file'];
739: $item['lineno'] = $frame['line'];
740: break;
741: }
742: }
743:
744: $payload['logs'][] = DebugHelpers::jsonDump($item, -1);
745: foreach (str_split(base64_encode(@json_encode($payload)), 4990) as $k => $v) {
746: header("FireLogger-de11e-$k:$v");
747: }
748: return TRUE;
749: }
750:
751: }
752:
753:
754:
755: Debug::_init();
756: