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