PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/Nette/Diagnostics/Debugger.php

https://github.com/lm/nette
PHP | 660 lines | 412 code | 115 blank | 133 comment | 71 complexity | b12686f2c639a2ff7afaa42aa01d13c6 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (http://nette.org)
  4. *
  5. * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. */
  10. namespace Nette\Diagnostics;
  11. use Nette;
  12. /**
  13. * Debugger: displays and logs errors.
  14. *
  15. * Behavior is determined by two factors: mode & output
  16. * - modes: production / development
  17. * - output: HTML / AJAX / CLI / other (e.g. XML)
  18. *
  19. * @author David Grudl
  20. */
  21. final class Debugger
  22. {
  23. /** @var bool in production mode is suppressed any debugging output */
  24. public static $productionMode = self::DETECT;
  25. /** @deprecated */
  26. public static $consoleMode;
  27. /** @var int timestamp with microseconds of the start of the request */
  28. public static $time;
  29. /** @var string requested URI or command line */
  30. public static $source;
  31. /** @var string URL pattern mask to open editor */
  32. public static $editor = 'editor://open/?file=%file&line=%line';
  33. /** @var string command to open browser (use 'start ""' in Windows) */
  34. public static $browser;
  35. /********************* Debugger::dump() ****************d*g**/
  36. /** @var int how many nested levels of array/object properties display {@link Debugger::dump()} */
  37. public static $maxDepth = 3;
  38. /** @var int how long strings display {@link Debugger::dump()} */
  39. public static $maxLen = 150;
  40. /** @var bool display location? {@link Debugger::dump()} */
  41. public static $showLocation = FALSE;
  42. /** @deprecated */
  43. public static $consoleColors;
  44. /********************* errors and exceptions reporting ****************d*g**/
  45. /** server modes {@link Debugger::enable()} */
  46. const DEVELOPMENT = FALSE,
  47. PRODUCTION = TRUE,
  48. DETECT = NULL;
  49. /** @deprecated @var BlueScreen*/
  50. public static $blueScreen;
  51. /** @var bool|int determines whether any error will cause immediate death; if integer that it's matched against error severity */
  52. public static $strictMode = FALSE; // $immediateDeath
  53. /** @var bool disables the @ (shut-up) operator so that notices and warnings are no longer hidden */
  54. public static $scream = FALSE;
  55. /** @var array of callables specifies the functions that are automatically called after fatal error */
  56. public static $onFatalError = array();
  57. /** @var bool {@link Debugger::enable()} */
  58. private static $enabled = FALSE;
  59. /** @var mixed {@link Debugger::tryError()} FALSE means catching is disabled */
  60. private static $lastError = FALSE;
  61. /** @internal */
  62. public static $errorTypes = array(
  63. E_ERROR => 'Fatal Error',
  64. E_USER_ERROR => 'User Error',
  65. E_RECOVERABLE_ERROR => 'Recoverable Error',
  66. E_CORE_ERROR => 'Core Error',
  67. E_COMPILE_ERROR => 'Compile Error',
  68. E_PARSE => 'Parse Error',
  69. E_WARNING => 'Warning',
  70. E_CORE_WARNING => 'Core Warning',
  71. E_COMPILE_WARNING => 'Compile Warning',
  72. E_USER_WARNING => 'User Warning',
  73. E_NOTICE => 'Notice',
  74. E_USER_NOTICE => 'User Notice',
  75. E_STRICT => 'Strict standards',
  76. E_DEPRECATED => 'Deprecated',
  77. E_USER_DEPRECATED => 'User Deprecated',
  78. );
  79. /********************* logging ****************d*g**/
  80. /** @deprecated @var Logger */
  81. public static $logger;
  82. /** @deprecated @var FireLogger */
  83. public static $fireLogger;
  84. /** @var string name of the directory where errors should be logged; FALSE means that logging is disabled */
  85. public static $logDirectory;
  86. /** @var string|array email(s) to which send error notifications */
  87. public static $email;
  88. /** @deprecated */
  89. public static $mailer = array('Nette\Diagnostics\Logger', 'defaultMailer');
  90. /** @deprecated */
  91. public static $emailSnooze = 172800;
  92. /** {@link Debugger::log()} and {@link Debugger::fireLog()} */
  93. const DEBUG = 'debug',
  94. INFO = 'info',
  95. WARNING = 'warning',
  96. ERROR = 'error',
  97. CRITICAL = 'critical';
  98. /********************* debug bar ****************d*g**/
  99. /** @deprecated @var Bar */
  100. public static $bar;
  101. /**
  102. * Static class - cannot be instantiated.
  103. */
  104. final public function __construct()
  105. {
  106. throw new Nette\StaticClassException;
  107. }
  108. /**
  109. * Enables displaying or logging errors and exceptions.
  110. * @param mixed production, development mode, autodetection or IP address(es) whitelist.
  111. * @param string error log directory; enables logging in production mode, FALSE means that logging is disabled
  112. * @param string administrator email; enables email sending in production mode
  113. * @return void
  114. */
  115. public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
  116. {
  117. self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
  118. if (isset($_SERVER['REQUEST_URI'])) {
  119. self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
  120. . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '')
  121. . $_SERVER['REQUEST_URI'];
  122. } else {
  123. self::$source = empty($_SERVER['argv']) ? 'CLI' : 'CLI: ' . implode(' ', $_SERVER['argv']);
  124. }
  125. self::$consoleColors = & Dumper::$terminalColors;
  126. error_reporting(E_ALL | E_STRICT);
  127. // production/development mode detection
  128. if (is_bool($mode)) {
  129. self::$productionMode = $mode;
  130. } elseif ($mode !== self::DETECT || self::$productionMode === NULL) { // IP addresses or computer names whitelist detection
  131. $list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
  132. if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  133. $list[] = '127.0.0.1';
  134. $list[] = '::1';
  135. }
  136. self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
  137. }
  138. // logging configuration
  139. if (is_string($logDirectory)) {
  140. self::$logDirectory = realpath($logDirectory);
  141. if (self::$logDirectory === FALSE) {
  142. echo __METHOD__ . "() error: Log directory is not found or is not directory.\n";
  143. exit(254);
  144. }
  145. } elseif ($logDirectory === FALSE || self::$logDirectory === NULL) {
  146. self::$logDirectory = FALSE;
  147. }
  148. if (self::$logDirectory) {
  149. ini_set('error_log', self::$logDirectory . '/php_error.log');
  150. }
  151. // php configuration
  152. if (function_exists('ini_set')) {
  153. ini_set('display_errors', !self::$productionMode); // or 'stderr'
  154. ini_set('html_errors', FALSE);
  155. ini_set('log_errors', FALSE);
  156. } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { // intentionally ==
  157. echo __METHOD__ . "() error: Unable to set 'display_errors' because function ini_set() is disabled.\n";
  158. exit(254);
  159. }
  160. if ($email) {
  161. if (!is_string($email) && !is_array($email)) {
  162. echo __METHOD__ . "() error: Email address must be a string.\n";
  163. exit(254);
  164. }
  165. self::$email = $email;
  166. }
  167. if (!self::$enabled) {
  168. register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
  169. set_exception_handler(array(__CLASS__, '_exceptionHandler'));
  170. set_error_handler(array(__CLASS__, '_errorHandler'));
  171. foreach (array('Nette\Diagnostics\Bar', 'Nette\Diagnostics\BlueScreen', 'Nette\Diagnostics\DefaultBarPanel', 'Nette\Diagnostics\Dumper', 'Nette\Diagnostics\FireLogger',
  172. 'Nette\Diagnostics\Helpers', 'Nette\Diagnostics\Logger', 'Nette\FatalErrorException', 'Nette\Utils\Html', 'Nette\Utils\Strings') as $class) {
  173. class_exists($class);
  174. }
  175. self::$enabled = TRUE;
  176. }
  177. }
  178. /**
  179. * @return BlueScreen
  180. */
  181. public static function getBlueScreen()
  182. {
  183. if (!self::$blueScreen) {
  184. self::$blueScreen = new BlueScreen;
  185. self::$blueScreen->collapsePaths[] = dirname(__DIR__);
  186. self::$blueScreen->addPanel(function($e) {
  187. if ($e instanceof Nette\Templating\FilterException) {
  188. return array(
  189. 'tab' => 'Template',
  190. 'panel' => '<p><b>File:</b> ' . Helpers::editorLink($e->sourceFile, $e->sourceLine) . '</p>'
  191. . ($e->sourceLine ? BlueScreen::highlightFile($e->sourceFile, $e->sourceLine) : '')
  192. );
  193. } elseif ($e instanceof Nette\Utils\NeonException && preg_match('#line (\d+)#', $e->getMessage(), $m)) {
  194. if ($item = Helpers::findTrace($e->getTrace(), 'Nette\Config\Adapters\NeonAdapter::load')) {
  195. return array(
  196. 'tab' => 'NEON',
  197. 'panel' => '<p><b>File:</b> ' . Helpers::editorLink($item['args'][0], $m[1]) . '</p>'
  198. . BlueScreen::highlightFile($item['args'][0], $m[1])
  199. );
  200. } elseif ($item = Helpers::findTrace($e->getTrace(), 'Nette\Utils\Neon::decode')) {
  201. return array(
  202. 'tab' => 'NEON',
  203. 'panel' => BlueScreen::highlightPhp($item['args'][0], $m[1])
  204. );
  205. }
  206. }
  207. });
  208. }
  209. return self::$blueScreen;
  210. }
  211. /**
  212. * @return Bar
  213. */
  214. public static function getBar()
  215. {
  216. if (!self::$bar) {
  217. self::$bar = new Bar;
  218. self::$bar->addPanel(new DefaultBarPanel('time'));
  219. self::$bar->addPanel(new DefaultBarPanel('memory'));
  220. self::$bar->addPanel(new DefaultBarPanel('errors'), __CLASS__ . ':errors'); // filled by _errorHandler()
  221. self::$bar->addPanel(new DefaultBarPanel('dumps'), __CLASS__ . ':dumps'); // filled by barDump()
  222. }
  223. return self::$bar;
  224. }
  225. /**
  226. * @return void
  227. */
  228. public static function setLogger($logger)
  229. {
  230. self::$logger = $logger;
  231. }
  232. /**
  233. * @return Logger
  234. */
  235. public static function getLogger()
  236. {
  237. if (!self::$logger) {
  238. self::$logger = new Logger;
  239. self::$logger->directory = & self::$logDirectory;
  240. self::$logger->email = & self::$email;
  241. self::$logger->mailer = & self::$mailer;
  242. self::$logger->emailSnooze = & self::$emailSnooze;
  243. }
  244. return self::$logger;
  245. }
  246. /**
  247. * @return FireLogger
  248. */
  249. public static function getFireLogger()
  250. {
  251. if (!self::$fireLogger) {
  252. self::$fireLogger = new FireLogger;
  253. }
  254. return self::$fireLogger;
  255. }
  256. /**
  257. * Is Debug enabled?
  258. * @return bool
  259. */
  260. public static function isEnabled()
  261. {
  262. return self::$enabled;
  263. }
  264. /**
  265. * Logs message or exception to file (if not disabled) and sends email notification (if enabled).
  266. * @param string|Exception
  267. * @param int one of constant Debugger::INFO, WARNING, ERROR (sends email), CRITICAL (sends email)
  268. * @return string logged error filename
  269. */
  270. public static function log($message, $priority = self::INFO)
  271. {
  272. if (self::$logDirectory === FALSE) {
  273. return;
  274. } elseif (!self::$logDirectory) {
  275. throw new Nette\InvalidStateException('Logging directory is not specified in Nette\Diagnostics\Debugger::$logDirectory.');
  276. }
  277. $exceptionFilename = NULL;
  278. if ($message instanceof \Exception) {
  279. $exception = $message;
  280. while ($exception) {
  281. $tmp[] = ($exception instanceof Nette\FatalErrorException
  282. ? 'Fatal error: ' . $exception->getMessage()
  283. : get_class($exception) . ": " . $exception->getMessage())
  284. . " in " . $exception->getFile() . ":" . $exception->getLine();
  285. $exception = $exception->getPrevious();
  286. }
  287. $exception = $message;
  288. $message = implode($tmp, "\ncaused by ");
  289. $hash = md5(preg_replace('~(Resource id #)\d+~', '$1', $exception));
  290. $exceptionFilename = "exception-" . @date('Y-m-d-H-i-s') . "-$hash.html";
  291. foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
  292. if (strpos($entry, $hash)) {
  293. $exceptionFilename = $entry;
  294. $saved = TRUE;
  295. break;
  296. }
  297. }
  298. } elseif (!is_string($message)) {
  299. $message = Dumper::toText($message);
  300. }
  301. if ($exceptionFilename) {
  302. $exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
  303. if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
  304. ob_start(); // double buffer prevents sending HTTP headers in some PHP
  305. ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 4096);
  306. self::getBlueScreen()->render($exception);
  307. ob_end_flush();
  308. ob_end_clean();
  309. fclose($logHandle);
  310. }
  311. }
  312. self::getLogger()->log(array(
  313. @date('[Y-m-d H-i-s]'),
  314. trim($message),
  315. self::$source ? ' @ ' . self::$source : NULL,
  316. $exceptionFilename ? ' @@ ' . basename($exceptionFilename) : NULL
  317. ), $priority);
  318. return $exceptionFilename ? strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR) : NULL;
  319. }
  320. /**
  321. * Shutdown handler to catch fatal errors and execute of the planned activities.
  322. * @return void
  323. * @internal
  324. */
  325. public static function _shutdownHandler()
  326. {
  327. if (!self::$enabled) {
  328. return;
  329. }
  330. $error = error_get_last();
  331. if (in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
  332. self::_exceptionHandler(Helpers::fixStack(new Nette\FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL)), TRUE);
  333. } elseif (!connection_aborted() && !self::$productionMode && self::isHtmlMode()) {
  334. self::getBar()->render();
  335. }
  336. }
  337. /**
  338. * Handler to catch uncaught exception.
  339. * @param \Exception
  340. * @return void
  341. * @internal
  342. */
  343. public static function _exceptionHandler(\Exception $exception, $shutdown = FALSE)
  344. {
  345. if (!self::$enabled) {
  346. return;
  347. }
  348. self::$enabled = FALSE; // prevent double rendering
  349. if (!headers_sent()) {
  350. $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
  351. $code = isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== FALSE ? 503 : 500;
  352. header("$protocol $code", TRUE, $code);
  353. }
  354. try {
  355. if (self::$productionMode) {
  356. try {
  357. self::log($exception, self::ERROR);
  358. } catch (\Exception $e) {
  359. echo 'FATAL ERROR: unable to log error';
  360. }
  361. if (self::isHtmlMode()) {
  362. require __DIR__ . '/templates/error.phtml';
  363. } else {
  364. echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
  365. }
  366. } else {
  367. if (!connection_aborted() && self::isHtmlMode()) {
  368. self::getBlueScreen()->render($exception);
  369. self::getBar()->render();
  370. } elseif (connection_aborted() || !self::fireLog($exception)) {
  371. $file = self::log($exception, self::ERROR);
  372. if (!headers_sent()) {
  373. header("X-Nette-Error-Log: $file");
  374. }
  375. echo "$exception\n" . ($file ? "(stored in $file)\n" : '');
  376. if (self::$browser) {
  377. exec(self::$browser . ' ' . escapeshellarg($file));
  378. }
  379. }
  380. }
  381. foreach (self::$onFatalError as $handler) {
  382. call_user_func($handler, $exception);
  383. }
  384. } catch (\Exception $e) {
  385. if (self::$productionMode) {
  386. echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
  387. } else {
  388. echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
  389. "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
  390. }
  391. }
  392. if (!$shutdown) {
  393. exit(254);
  394. }
  395. }
  396. /**
  397. * Handler to catch warnings and notices.
  398. * @param int level of the error raised
  399. * @param string error message
  400. * @param string file that the error was raised in
  401. * @param int line number the error was raised at
  402. * @param array an array of variables that existed in the scope the error was triggered in
  403. * @return bool FALSE to call normal error handler, NULL otherwise
  404. * @throws Nette\FatalErrorException
  405. * @internal
  406. */
  407. public static function _errorHandler($severity, $message, $file, $line, $context)
  408. {
  409. if (self::$scream) {
  410. error_reporting(E_ALL | E_STRICT);
  411. }
  412. if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { // tryError mode
  413. self::$lastError = new \ErrorException($message, 0, $severity, $file, $line);
  414. return NULL;
  415. }
  416. if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
  417. if (Helpers::findTrace(debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE), '*::__toString')) {
  418. $previous = isset($context['e']) && $context['e'] instanceof \Exception ? $context['e'] : NULL;
  419. self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context, $previous));
  420. }
  421. throw new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context);
  422. } elseif (($severity & error_reporting()) !== $severity) {
  423. return FALSE; // calls normal error handler to fill-in error_get_last()
  424. } elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
  425. self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context));
  426. }
  427. $message = 'PHP ' . (isset(self::$errorTypes[$severity]) ? self::$errorTypes[$severity] : 'Unknown error') . ": $message";
  428. $count = & self::getBar()->getPanel(__CLASS__ . ':errors')->data["$file|$line|$message"];
  429. if ($count++) { // repeated error
  430. return NULL;
  431. } elseif (self::$productionMode) {
  432. self::log("$message in $file:$line", self::ERROR);
  433. return NULL;
  434. } else {
  435. self::fireLog(new \ErrorException($message, 0, $severity, $file, $line));
  436. return self::isHtmlMode() ? NULL : FALSE;
  437. }
  438. return FALSE; // call normal error handler
  439. }
  440. /** @deprecated */
  441. public static function toStringException(\Exception $exception)
  442. {
  443. if (self::$enabled) {
  444. self::_exceptionHandler($exception);
  445. } else {
  446. trigger_error($exception->getMessage(), E_USER_ERROR);
  447. }
  448. }
  449. /** @deprecated */
  450. public static function tryError()
  451. {
  452. trigger_error(__METHOD__ . '() is deprecated; use own error handler instead.', E_USER_DEPRECATED);
  453. if (!self::$enabled && self::$lastError === FALSE) {
  454. set_error_handler(array(__CLASS__, '_errorHandler'));
  455. }
  456. self::$lastError = NULL;
  457. }
  458. /** @deprecated */
  459. public static function catchError(& $error)
  460. {
  461. trigger_error(__METHOD__ . '() is deprecated; use own error handler instead.', E_USER_DEPRECATED);
  462. if (!self::$enabled && self::$lastError !== FALSE) {
  463. restore_error_handler();
  464. }
  465. $error = self::$lastError;
  466. self::$lastError = FALSE;
  467. return (bool) $error;
  468. }
  469. /********************* useful tools ****************d*g**/
  470. /**
  471. * Dumps information about a variable in readable format.
  472. * @param mixed variable to dump
  473. * @param bool return output instead of printing it? (bypasses $productionMode)
  474. * @return mixed variable itself or dump
  475. */
  476. public static function dump($var, $return = FALSE)
  477. {
  478. if ($return) {
  479. ob_start();
  480. Dumper::dump($var, array(
  481. Dumper::DEPTH => self::$maxDepth,
  482. Dumper::TRUNCATE => self::$maxLen,
  483. ));
  484. return ob_get_clean();
  485. } elseif (!self::$productionMode) {
  486. Dumper::dump($var, array(
  487. Dumper::DEPTH => self::$maxDepth,
  488. Dumper::TRUNCATE => self::$maxLen,
  489. Dumper::LOCATION => self::$showLocation,
  490. ));
  491. }
  492. return $var;
  493. }
  494. /**
  495. * Starts/stops stopwatch.
  496. * @param string name
  497. * @return float elapsed seconds
  498. */
  499. public static function timer($name = NULL)
  500. {
  501. static $time = array();
  502. $now = microtime(TRUE);
  503. $delta = isset($time[$name]) ? $now - $time[$name] : 0;
  504. $time[$name] = $now;
  505. return $delta;
  506. }
  507. /**
  508. * Dumps information about a variable in Nette Debug Bar.
  509. * @param mixed variable to dump
  510. * @param string optional title
  511. * @return mixed variable itself
  512. */
  513. public static function barDump($var, $title = NULL)
  514. {
  515. if (!self::$productionMode) {
  516. $dump = array();
  517. foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
  518. $dump[$key] = Dumper::toHtml($val);
  519. }
  520. self::getBar()->getPanel(__CLASS__ . ':dumps')->data[] = array('title' => $title, 'dump' => $dump);
  521. }
  522. return $var;
  523. }
  524. /**
  525. * Sends message to FireLogger console.
  526. * @param mixed message to log
  527. * @return bool was successful?
  528. */
  529. public static function fireLog($message)
  530. {
  531. if (!self::$productionMode) {
  532. return self::getFireLogger()->log($message);
  533. }
  534. }
  535. private static function isHtmlMode()
  536. {
  537. return empty($_SERVER['HTTP_X_REQUESTED_WITH'])
  538. && PHP_SAPI !== 'cli'
  539. && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
  540. }
  541. public static function addPanel(IBarPanel $panel, $id = NULL)
  542. {
  543. return self::getBar()->addPanel($panel, $id);
  544. }
  545. }