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