PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Nette/Diagnostics/Debugger.php

https://bitbucket.org/Aurielle/nette
PHP | 669 lines | 388 code | 143 blank | 138 comment | 86 complexity | bd49026d6c7389d015f4bbeabdc3bed9 MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (http://nette.org)
  4. *
  5. * Copyright (c) 2004, 2011 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;
  25. /** @var bool in console mode is omitted HTML output */
  26. public static $consoleMode;
  27. /** @var int timestamp with microseconds of the start of the request */
  28. public static $time;
  29. /** @var bool is AJAX request detected? */
  30. private static $ajaxDetected;
  31. /** @var string requested URI or command line */
  32. public static $source;
  33. /** @var string URL pattern mask to open editor */
  34. public static $editor = 'editor://open/?file=%file&line=%line';
  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. /********************* errors and exceptions reporting ****************d*g**/
  43. /** server modes {@link Debugger::enable()} */
  44. const DEVELOPMENT = FALSE,
  45. PRODUCTION = TRUE,
  46. DETECT = NULL;
  47. /** @var BlueScreen */
  48. public static $blueScreen;
  49. /** @var bool|int determines whether any error will cause immediate death; if integer that it's matched against error severity */
  50. public static $strictMode = FALSE; // $immediateDeath
  51. /** @var bool disables the @ (shut-up) operator so that notices and warnings are no longer hidden */
  52. public static $scream = FALSE;
  53. /** @var array of callbacks specifies the functions that are automatically called after fatal error */
  54. public static $onFatalError = array();
  55. /** @var bool {@link Debugger::enable()} */
  56. private static $enabled = FALSE;
  57. /** @var mixed {@link Debugger::tryError()} FALSE means catching is disabled */
  58. private static $lastError = FALSE;
  59. /********************* logging ****************d*g**/
  60. /** @var Logger */
  61. public static $logger;
  62. /** @var FireLogger */
  63. public static $fireLogger;
  64. /** @var string name of the directory where errors should be logged; FALSE means that logging is disabled */
  65. public static $logDirectory;
  66. /** @var string email to sent error notifications */
  67. public static $email;
  68. /** @deprecated */
  69. public static $mailer;
  70. /** @deprecated */
  71. public static $emailSnooze;
  72. /********************* debug bar ****************d*g**/
  73. /** @var Bar */
  74. public static $bar;
  75. /** @var DefaultBarPanel */
  76. private static $errorPanel;
  77. /** @var DefaultBarPanel */
  78. private static $dumpPanel;
  79. /********************* Firebug extension ****************d*g**/
  80. /** {@link Debugger::log()} and {@link Debugger::fireLog()} */
  81. const DEBUG = 'debug',
  82. INFO = 'info',
  83. WARNING = 'warning',
  84. ERROR = 'error',
  85. CRITICAL = 'critical';
  86. /**
  87. * Static class - cannot be instantiated.
  88. */
  89. final public function __construct()
  90. {
  91. throw new Nette\StaticClassException;
  92. }
  93. /**
  94. * Static class constructor.
  95. * @internal
  96. */
  97. public static function _init()
  98. {
  99. self::$time = microtime(TRUE);
  100. self::$consoleMode = PHP_SAPI === 'cli';
  101. self::$productionMode = self::DETECT;
  102. if (self::$consoleMode) {
  103. self::$source = empty($_SERVER['argv']) ? 'cli' : 'cli: ' . implode(' ', $_SERVER['argv']);
  104. } else {
  105. self::$ajaxDetected = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
  106. if (isset($_SERVER['REQUEST_URI'])) {
  107. self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
  108. . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
  109. . $_SERVER['REQUEST_URI'];
  110. }
  111. }
  112. self::$logger = new Logger;
  113. self::$logDirectory = & self::$logger->directory;
  114. self::$email = & self::$logger->email;
  115. self::$mailer = & self::$logger->mailer;
  116. self::$emailSnooze = & Logger::$emailSnooze;
  117. self::$fireLogger = new FireLogger;
  118. self::$blueScreen = new BlueScreen;
  119. self::$blueScreen->addPanel(function($e) {
  120. if ($e instanceof Nette\Templating\FilterException) {
  121. return array(
  122. 'tab' => 'Template',
  123. 'panel' => '<p><b>File:</b> ' . Helpers::editorLink($e->sourceFile, $e->sourceLine)
  124. . '&nbsp; <b>Line:</b> ' . ($e->sourceLine ? $e->sourceLine : 'n/a') . '</p>'
  125. . ($e->sourceLine ? '<pre>' . BlueScreen::highlightFile($e->sourceFile, $e->sourceLine) . '</pre>' : '')
  126. );
  127. }
  128. });
  129. self::$bar = new Bar;
  130. self::$bar->addPanel(new DefaultBarPanel('time'));
  131. self::$bar->addPanel(new DefaultBarPanel('memory'));
  132. self::$bar->addPanel(self::$errorPanel = new DefaultBarPanel('errors')); // filled by _errorHandler()
  133. self::$bar->addPanel(self::$dumpPanel = new DefaultBarPanel('dumps')); // filled by barDump()
  134. }
  135. /********************* errors and exceptions reporting ****************d*g**/
  136. /**
  137. * Enables displaying or logging errors and exceptions.
  138. * @param mixed production, development mode, autodetection or IP address(es) whitelist.
  139. * @param string error log directory; enables logging in production mode, FALSE means that logging is disabled
  140. * @param string administrator email; enables email sending in production mode
  141. * @return void
  142. */
  143. public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
  144. {
  145. error_reporting(E_ALL | E_STRICT);
  146. // production/development mode detection
  147. if (is_bool($mode)) {
  148. self::$productionMode = $mode;
  149. } elseif (is_string($mode)) { // IP addresses
  150. $mode = preg_split('#[,\s]+#', "$mode 127.0.0.1 ::1");
  151. }
  152. if (is_array($mode)) { // IP addresses whitelist detection
  153. self::$productionMode = !isset($_SERVER['REMOTE_ADDR']) || !in_array($_SERVER['REMOTE_ADDR'], $mode, TRUE);
  154. }
  155. if (self::$productionMode === self::DETECT) {
  156. if (isset($_SERVER['SERVER_ADDR']) || isset($_SERVER['LOCAL_ADDR'])) { // IP address based detection
  157. $addrs = array();
  158. if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { // proxy server detected
  159. $addrs = preg_split('#,\s*#', $_SERVER['HTTP_X_FORWARDED_FOR']);
  160. }
  161. if (isset($_SERVER['REMOTE_ADDR'])) {
  162. $addrs[] = $_SERVER['REMOTE_ADDR'];
  163. }
  164. $addrs[] = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : $_SERVER['LOCAL_ADDR'];
  165. self::$productionMode = FALSE;
  166. foreach ($addrs as $addr) {
  167. $oct = explode('.', $addr);
  168. if ($addr !== '::1' && (count($oct) !== 4 || ($oct[0] !== '10' && $oct[0] !== '127' && ($oct[0] !== '172' || $oct[1] < 16 || $oct[1] > 31)
  169. && ($oct[0] !== '169' || $oct[1] !== '254') && ($oct[0] !== '192' || $oct[1] !== '168')))
  170. ) {
  171. self::$productionMode = TRUE;
  172. break;
  173. }
  174. }
  175. } else {
  176. self::$productionMode = !self::$consoleMode;
  177. }
  178. }
  179. // logging configuration
  180. if (is_string($logDirectory)) {
  181. self::$logDirectory = realpath($logDirectory);
  182. if (self::$logDirectory === FALSE) {
  183. throw new Nette\DirectoryNotFoundException("Directory '$logDirectory' is not found.");
  184. }
  185. } elseif ($logDirectory === FALSE) {
  186. self::$logDirectory = FALSE;
  187. } elseif (self::$logDirectory === NULL) {
  188. self::$logDirectory = defined('APP_DIR') ? APP_DIR . '/../log' : getcwd() . '/log';
  189. }
  190. if (self::$logDirectory) {
  191. ini_set('error_log', self::$logDirectory . '/php_error.log');
  192. }
  193. // php configuration
  194. if (function_exists('ini_set')) {
  195. ini_set('display_errors', !self::$productionMode); // or 'stderr'
  196. ini_set('html_errors', FALSE);
  197. ini_set('log_errors', FALSE);
  198. } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { // intentionally ==
  199. throw new Nette\NotSupportedException('Function ini_set() must be enabled.');
  200. }
  201. if ($email) {
  202. if (!is_string($email)) {
  203. throw new Nette\InvalidArgumentException('Email address must be a string.');
  204. }
  205. self::$email = $email;
  206. }
  207. if (!defined('E_DEPRECATED')) {
  208. define('E_DEPRECATED', 8192);
  209. }
  210. if (!defined('E_USER_DEPRECATED')) {
  211. define('E_USER_DEPRECATED', 16384);
  212. }
  213. if (!self::$enabled) {
  214. register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
  215. set_exception_handler(array(__CLASS__, '_exceptionHandler'));
  216. set_error_handler(array(__CLASS__, '_errorHandler'));
  217. self::$enabled = TRUE;
  218. }
  219. }
  220. /**
  221. * Is Debug enabled?
  222. * @return bool
  223. */
  224. public static function isEnabled()
  225. {
  226. return self::$enabled;
  227. }
  228. /**
  229. * Logs message or exception to file (if not disabled) and sends email notification (if enabled).
  230. * @param string|Exception
  231. * @param int one of constant Debugger::INFO, WARNING, ERROR (sends email), CRITICAL (sends email)
  232. * @return void
  233. */
  234. public static function log($message, $priority = self::INFO)
  235. {
  236. if (self::$logDirectory === FALSE) {
  237. return;
  238. } elseif (!self::$logDirectory) {
  239. throw new Nette\InvalidStateException('Logging directory is not specified in Nette\Diagnostics\Debugger::$logDirectory.');
  240. }
  241. if ($message instanceof \Exception) {
  242. $exception = $message;
  243. $message = "PHP Fatal error: "
  244. . ($message instanceof Nette\FatalErrorException
  245. ? $exception->getMessage()
  246. : "Uncaught exception " . get_class($exception) . " with message '" . $exception->getMessage() . "'")
  247. . " in " . $exception->getFile() . ":" . $exception->getLine();
  248. $hash = md5($exception /*5.2*. (method_exists($exception, 'getPrevious') ? $exception->getPrevious() : (isset($exception->previous) ? $exception->previous : ''))*/);
  249. $exceptionFilename = "exception " . @date('Y-m-d H-i-s') . " $hash.html";
  250. foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
  251. if (strpos($entry, $hash)) {
  252. $exceptionFilename = NULL; break;
  253. }
  254. }
  255. }
  256. self::$logger->log(array(
  257. @date('[Y-m-d H-i-s]'),
  258. $message,
  259. self::$source ? ' @ ' . self::$source : NULL,
  260. !empty($exceptionFilename) ? ' @@ ' . $exceptionFilename : NULL
  261. ), $priority);
  262. if (!empty($exceptionFilename) && $logHandle = @fopen(self::$logDirectory . '/'. $exceptionFilename, 'w')) {
  263. ob_start(); // double buffer prevents sending HTTP headers in some PHP
  264. ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 1);
  265. self::$blueScreen->render($exception);
  266. ob_end_flush();
  267. ob_end_clean();
  268. fclose($logHandle);
  269. }
  270. }
  271. /**
  272. * Shutdown handler to catch fatal errors and execute of the planned activities.
  273. * @return void
  274. * @internal
  275. */
  276. public static function _shutdownHandler()
  277. {
  278. if (!self::$enabled) {
  279. return;
  280. }
  281. // fatal error handler
  282. static $types = array(
  283. E_ERROR => 1,
  284. E_CORE_ERROR => 1,
  285. E_COMPILE_ERROR => 1,
  286. E_PARSE => 1,
  287. );
  288. $error = error_get_last();
  289. if (isset($types[$error['type']])) {
  290. self::_exceptionHandler(new Nette\FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL));
  291. }
  292. // debug bar (require HTML & development mode)
  293. if (self::$bar && !self::$productionMode && self::isHtmlMode()) {
  294. self::$bar->render();
  295. }
  296. }
  297. /**
  298. * Handler to catch uncaught exception.
  299. * @param \Exception
  300. * @return void
  301. * @internal
  302. */
  303. public static function _exceptionHandler(\Exception $exception)
  304. {
  305. if (!headers_sent()) { // for PHP < 5.2.4
  306. header('HTTP/1.1 500 Internal Server Error');
  307. }
  308. try {
  309. if (self::$productionMode) {
  310. try {
  311. self::log($exception, self::ERROR);
  312. } catch (\Exception $e) {
  313. echo 'FATAL ERROR: unable to log error';
  314. }
  315. if (self::$consoleMode) {
  316. echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
  317. } elseif (self::isHtmlMode()) {
  318. require __DIR__ . '/templates/error.phtml';
  319. }
  320. } else {
  321. if (self::$consoleMode) { // dump to console
  322. echo "$exception\n";
  323. } elseif (self::isHtmlMode()) { // dump to browser
  324. self::$blueScreen->render($exception);
  325. if (self::$bar) {
  326. self::$bar->render();
  327. }
  328. } elseif (!self::fireLog($exception, self::ERROR)) { // AJAX or non-HTML mode
  329. self::log($exception);
  330. }
  331. }
  332. foreach (self::$onFatalError as $handler) {
  333. call_user_func($handler, $exception);
  334. }
  335. } catch (\Exception $e) {
  336. if (self::$productionMode) {
  337. echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
  338. } else {
  339. echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
  340. "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
  341. }
  342. }
  343. self::$enabled = FALSE; // un-register shutdown function
  344. exit(255);
  345. }
  346. /**
  347. * Handler to catch warnings and notices.
  348. * @param int level of the error raised
  349. * @param string error message
  350. * @param string file that the error was raised in
  351. * @param int line number the error was raised at
  352. * @param array an array of variables that existed in the scope the error was triggered in
  353. * @return bool FALSE to call normal error handler, NULL otherwise
  354. * @throws Nette\FatalErrorException
  355. * @internal
  356. */
  357. public static function _errorHandler($severity, $message, $file, $line, $context)
  358. {
  359. if (self::$scream) {
  360. error_reporting(E_ALL | E_STRICT);
  361. }
  362. if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { // tryError mode
  363. self::$lastError = new \ErrorException($message, 0, $severity, $file, $line);
  364. return NULL;
  365. }
  366. if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
  367. throw new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context);
  368. } elseif (($severity & error_reporting()) !== $severity) {
  369. return FALSE; // calls normal error handler to fill-in error_get_last()
  370. } elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
  371. self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context));
  372. }
  373. static $types = array(
  374. E_WARNING => 'Warning',
  375. E_COMPILE_WARNING => 'Warning', // currently unable to handle
  376. E_USER_WARNING => 'Warning',
  377. E_NOTICE => 'Notice',
  378. E_USER_NOTICE => 'Notice',
  379. E_STRICT => 'Strict standards',
  380. E_DEPRECATED => 'Deprecated',
  381. E_USER_DEPRECATED => 'Deprecated',
  382. );
  383. $message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
  384. $count = & self::$errorPanel->data["$message|$file|$line"];
  385. if ($count++) { // repeated error
  386. return NULL;
  387. } elseif (self::$productionMode) {
  388. self::log("$message in $file:$line", self::ERROR);
  389. return NULL;
  390. } else {
  391. $ok = self::fireLog(new \ErrorException($message, 0, $severity, $file, $line), self::WARNING);
  392. return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
  393. }
  394. return FALSE; // call normal error handler
  395. }
  396. /**
  397. * Handles exception thrown in __toString().
  398. * @param \Exception
  399. * @return void
  400. */
  401. public static function toStringException(\Exception $exception)
  402. {
  403. if (self::$enabled) {
  404. self::_exceptionHandler($exception);
  405. } else {
  406. trigger_error($exception->getMessage(), E_USER_ERROR);
  407. }
  408. }
  409. /**
  410. * Starts catching potential errors/warnings.
  411. * @return void
  412. */
  413. public static function tryError()
  414. {
  415. if (!self::$enabled && self::$lastError === FALSE) {
  416. set_error_handler(array(__CLASS__, '_errorHandler'));
  417. }
  418. self::$lastError = NULL;
  419. }
  420. /**
  421. * Returns catched error/warning message.
  422. * @param \ErrorException catched error
  423. * @return bool
  424. */
  425. public static function catchError(& $error)
  426. {
  427. if (!self::$enabled && self::$lastError !== FALSE) {
  428. restore_error_handler();
  429. }
  430. $error = self::$lastError;
  431. self::$lastError = FALSE;
  432. return (bool) $error;
  433. }
  434. /********************* useful tools ****************d*g**/
  435. /**
  436. * Dumps information about a variable in readable format.
  437. * @param mixed variable to dump
  438. * @param bool return output instead of printing it? (bypasses $productionMode)
  439. * @return mixed variable itself or dump
  440. */
  441. public static function dump($var, $return = FALSE)
  442. {
  443. if (!$return && self::$productionMode) {
  444. return $var;
  445. }
  446. $output = "<pre class=\"nette-dump\">" . Helpers::htmlDump($var) . "</pre>\n";
  447. if (!$return) {
  448. $trace = debug_backtrace();
  449. $i = !isset($trace[1]['class']) && isset($trace[1]['function']) && $trace[1]['function'] === 'dump' ? 1 : 0;
  450. if (isset($trace[$i]['file'], $trace[$i]['line']) && is_file($trace[$i]['file'])) {
  451. $lines = file($trace[$i]['file']);
  452. preg_match('#dump\((.*)\)#', $lines[$trace[$i]['line'] - 1], $m);
  453. $output = substr_replace(
  454. $output,
  455. ' title="' . htmlspecialchars((isset($m[0]) ? "$m[0] \n" : '') . "in file {$trace[$i]['file']} on line {$trace[$i]['line']}") . '"',
  456. 4, 0);
  457. if (self::$showLocation) {
  458. $output = substr_replace(
  459. $output,
  460. ' <small>in ' . Helpers::editorLink($trace[$i]['file'], $trace[$i]['line']) . ":{$trace[$i]['line']}</small>",
  461. -8, 0);
  462. }
  463. }
  464. }
  465. if (self::$consoleMode) {
  466. $output = htmlspecialchars_decode(strip_tags($output), ENT_NOQUOTES);
  467. }
  468. if ($return) {
  469. return $output;
  470. } else {
  471. echo $output;
  472. return $var;
  473. }
  474. }
  475. /**
  476. * Starts/stops stopwatch.
  477. * @param string name
  478. * @return float elapsed seconds
  479. */
  480. public static function timer($name = NULL)
  481. {
  482. static $time = array();
  483. $now = microtime(TRUE);
  484. $delta = isset($time[$name]) ? $now - $time[$name] : 0;
  485. $time[$name] = $now;
  486. return $delta;
  487. }
  488. /**
  489. * Dumps information about a variable in Nette Debug Bar.
  490. * @param mixed variable to dump
  491. * @param string optional title
  492. * @return mixed variable itself
  493. */
  494. public static function barDump($var, $title = NULL)
  495. {
  496. if (!self::$productionMode) {
  497. $dump = array();
  498. foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
  499. $dump[$key] = Helpers::clickableDump($val);
  500. }
  501. self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
  502. }
  503. return $var;
  504. }
  505. /**
  506. * Sends message to FireLogger console.
  507. * @param mixed message to log
  508. * @return bool was successful?
  509. */
  510. public static function fireLog($message)
  511. {
  512. if (!self::$productionMode) {
  513. return self::$fireLogger->log($message);
  514. }
  515. }
  516. private static function isHtmlMode()
  517. {
  518. return !self::$ajaxDetected && !self::$consoleMode
  519. && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
  520. }
  521. /** @deprecated */
  522. public static function addPanel(IBarPanel $panel, $id = NULL)
  523. {
  524. self::$bar->addPanel($panel, $id);
  525. }
  526. }