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