PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Tracy/Debugger.php

https://github.com/voda/tracy
PHP | 590 lines | 346 code | 122 blank | 122 comment | 61 complexity | ae2f75c40ef1342856226bc0d9364276 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 Tracy;
  11. use Tracy;
  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 int timestamp with microseconds of the start of the request */
  26. public static $time;
  27. /** @var string requested URI or command line */
  28. public static $source;
  29. /** @var string URL pattern mask to open editor */
  30. public static $editor = 'editor://open/?file=%file&line=%line';
  31. /** @var string command to open browser (use 'start ""' in Windows) */
  32. public static $browser;
  33. /********************* Debugger::dump() ****************d*g**/
  34. /** @var int how many nested levels of array/object properties display {@link Debugger::dump()} */
  35. public static $maxDepth = 3;
  36. /** @var int how long strings display {@link Debugger::dump()} */
  37. public static $maxLen = 150;
  38. /** @var bool display location? {@link Debugger::dump()} */
  39. public static $showLocation = FALSE;
  40. /********************* errors and exceptions reporting ****************d*g**/
  41. /** server modes {@link Debugger::enable()} */
  42. const DEVELOPMENT = FALSE,
  43. PRODUCTION = TRUE,
  44. DETECT = NULL;
  45. /** @var BlueScreen */
  46. public static $blueScreen;
  47. /** @var bool|int determines whether any error will cause immediate death; if integer that it's matched against error severity */
  48. public static $strictMode = FALSE; // $immediateDeath
  49. /** @var bool disables the @ (shut-up) operator so that notices and warnings are no longer hidden */
  50. public static $scream = FALSE;
  51. /** @var array of callables specifies the functions that are automatically called after fatal error */
  52. public static $onFatalError = array();
  53. /** @var bool {@link Debugger::enable()} */
  54. private static $enabled = FALSE;
  55. /** @var mixed {@link Debugger::tryError()} FALSE means catching is disabled */
  56. private static $lastError = FALSE;
  57. /********************* logging ****************d*g**/
  58. /** @var Logger */
  59. public static $logger;
  60. /** @var FireLogger */
  61. public static $fireLogger;
  62. /** @var string name of the directory where errors should be logged; FALSE means that logging is disabled */
  63. public static $logDirectory;
  64. /** @var string email to sent error notifications */
  65. public static $email;
  66. /** @deprecated */
  67. public static $mailer;
  68. /** @deprecated */
  69. public static $emailSnooze;
  70. /********************* debug bar ****************d*g**/
  71. /** @var Bar */
  72. public static $bar;
  73. /** @var DefaultBarPanel */
  74. private static $errorPanel;
  75. /** @var DefaultBarPanel */
  76. private static $dumpPanel;
  77. /********************* Firebug extension ****************d*g**/
  78. /** {@link Debugger::log()} and {@link Debugger::fireLog()} */
  79. const DEBUG = 'debug',
  80. INFO = 'info',
  81. WARNING = 'warning',
  82. ERROR = 'error',
  83. CRITICAL = 'critical';
  84. /**
  85. * Static class - cannot be instantiated.
  86. */
  87. final public function __construct()
  88. {
  89. throw new \LogicException;
  90. }
  91. /**
  92. * Static class constructor.
  93. * @internal
  94. */
  95. public static function _init()
  96. {
  97. self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
  98. self::$productionMode = self::DETECT;
  99. if (isset($_SERVER['REQUEST_URI'])) {
  100. self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
  101. . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
  102. . $_SERVER['REQUEST_URI'];
  103. } else {
  104. self::$source = empty($_SERVER['argv']) ? 'CLI' : 'CLI: ' . implode(' ', $_SERVER['argv']);
  105. }
  106. self::$logger = new Logger;
  107. self::$logDirectory = & self::$logger->directory;
  108. self::$email = & self::$logger->email;
  109. self::$mailer = & self::$logger->mailer;
  110. self::$emailSnooze = & Logger::$emailSnooze;
  111. self::$fireLogger = new FireLogger;
  112. self::$blueScreen = new BlueScreen;
  113. self::$bar = new Bar;
  114. self::$bar->addPanel(new DefaultBarPanel('time'));
  115. self::$bar->addPanel(new DefaultBarPanel('memory'));
  116. self::$bar->addPanel(self::$errorPanel = new DefaultBarPanel('errors')); // filled by _errorHandler()
  117. self::$bar->addPanel(self::$dumpPanel = new DefaultBarPanel('dumps')); // filled by barDump()
  118. }
  119. /********************* errors and exceptions reporting ****************d*g**/
  120. /**
  121. * Enables displaying or logging errors and exceptions.
  122. * @param mixed production, development mode, autodetection or IP address(es) whitelist.
  123. * @param string error log directory; enables logging in production mode, FALSE means that logging is disabled
  124. * @param string administrator email; enables email sending in production mode
  125. * @return void
  126. */
  127. public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
  128. {
  129. error_reporting(E_ALL | E_STRICT);
  130. // production/development mode detection
  131. if (is_bool($mode)) {
  132. self::$productionMode = $mode;
  133. } elseif ($mode !== self::DETECT || self::$productionMode === NULL) { // IP addresses or computer names whitelist detection
  134. $list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
  135. if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  136. $list[] = '127.0.0.1';
  137. $list[] = '::1';
  138. }
  139. self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
  140. }
  141. // logging configuration
  142. if (is_string($logDirectory)) {
  143. self::$logDirectory = realpath($logDirectory);
  144. if (self::$logDirectory === FALSE) {
  145. echo __METHOD__ . "() error: Log directory is not found or is not directory.\n";
  146. exit(254);
  147. }
  148. } elseif ($logDirectory === FALSE || self::$logDirectory === NULL) {
  149. self::$logDirectory = FALSE;
  150. }
  151. if (self::$logDirectory) {
  152. ini_set('error_log', self::$logDirectory . '/php_error.log');
  153. }
  154. // php configuration
  155. if (function_exists('ini_set')) {
  156. ini_set('display_errors', !self::$productionMode); // or 'stderr'
  157. ini_set('html_errors', FALSE);
  158. ini_set('log_errors', FALSE);
  159. } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { // intentionally ==
  160. echo __METHOD__ . "() error: Unable to set 'display_errors' because function ini_set() is disabled.\n";
  161. exit(254);
  162. }
  163. if ($email) {
  164. if (!is_string($email)) {
  165. echo __METHOD__ . "() error: Email address must be a string.\n";
  166. exit(254);
  167. }
  168. self::$email = $email;
  169. }
  170. if (!self::$enabled) {
  171. register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
  172. set_exception_handler(array(__CLASS__, '_exceptionHandler'));
  173. set_error_handler(array(__CLASS__, '_errorHandler'));
  174. self::$enabled = TRUE;
  175. }
  176. }
  177. /**
  178. * Is Debug enabled?
  179. * @return bool
  180. */
  181. public static function isEnabled()
  182. {
  183. return self::$enabled;
  184. }
  185. /**
  186. * Logs message or exception to file (if not disabled) and sends email notification (if enabled).
  187. * @param string|Exception
  188. * @param int one of constant Debugger::INFO, WARNING, ERROR (sends email), CRITICAL (sends email)
  189. * @return string logged error filename
  190. */
  191. public static function log($message, $priority = self::INFO)
  192. {
  193. if (self::$logDirectory === FALSE) {
  194. return;
  195. } elseif (!self::$logDirectory) {
  196. throw new \RuntimeException('Logging directory is not specified in Tracy\Debugger::$logDirectory.');
  197. }
  198. $exceptionFilename = NULL;
  199. if ($message instanceof \Exception) {
  200. $exception = $message;
  201. while ($exception) {
  202. $tmp[] = ($exception instanceof \ErrorException
  203. ? 'Fatal error: ' . $exception->getMessage()
  204. : get_class($exception) . ": " . $exception->getMessage())
  205. . " in " . $exception->getFile() . ":" . $exception->getLine();
  206. $exception = $exception->getPrevious();
  207. }
  208. $exception = $message;
  209. $message = implode($tmp, "\ncaused by ");
  210. $hash = md5(preg_replace('~(Resource id #)\d+~', '$1', $exception));
  211. $exceptionFilename = "exception-" . @date('Y-m-d-H-i-s') . "-$hash.html";
  212. foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
  213. if (strpos($entry, $hash)) {
  214. $exceptionFilename = $entry;
  215. $saved = TRUE;
  216. break;
  217. }
  218. }
  219. } elseif (!is_string($message)) {
  220. $message = Dumper::toText($message);
  221. }
  222. if ($exceptionFilename) {
  223. $exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
  224. if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
  225. ob_start(); // double buffer prevents sending HTTP headers in some PHP
  226. ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 4096);
  227. self::$blueScreen->render($exception);
  228. ob_end_flush();
  229. ob_end_clean();
  230. fclose($logHandle);
  231. }
  232. }
  233. self::$logger->log(array(
  234. @date('[Y-m-d H-i-s]'),
  235. trim($message),
  236. self::$source ? ' @ ' . self::$source : NULL,
  237. $exceptionFilename ? ' @@ ' . basename($exceptionFilename) : NULL
  238. ), $priority);
  239. return $exceptionFilename ? strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR) : NULL;
  240. }
  241. /**
  242. * Shutdown handler to catch fatal errors and execute of the planned activities.
  243. * @return void
  244. * @internal
  245. */
  246. public static function _shutdownHandler()
  247. {
  248. if (!self::$enabled) {
  249. return;
  250. }
  251. // fatal error handler
  252. static $types = array(
  253. E_ERROR => 1,
  254. E_CORE_ERROR => 1,
  255. E_COMPILE_ERROR => 1,
  256. E_PARSE => 1,
  257. );
  258. $error = error_get_last();
  259. if (isset($types[$error['type']])) {
  260. $exception = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']);
  261. if (function_exists('xdebug_get_function_stack')) {
  262. $stack = array();
  263. foreach (array_slice(array_reverse(xdebug_get_function_stack()), 1, -1) as $row) {
  264. $frame = array(
  265. 'file' => $row['file'],
  266. 'line' => $row['line'],
  267. 'function' => isset($row['function']) ? $row['function'] : '*unknown*',
  268. 'args' => array(),
  269. );
  270. if (!empty($row['class'])) {
  271. $frame['type'] = isset($row['type']) && $row['type'] === 'dynamic' ? '->' : '::';
  272. $frame['class'] = $row['class'];
  273. }
  274. $stack[] = $frame;
  275. }
  276. $ref = new \ReflectionProperty('Exception', 'trace');
  277. $ref->setAccessible(TRUE);
  278. $ref->setValue($exception, $stack);
  279. }
  280. self::_exceptionHandler($exception);
  281. }
  282. // debug bar (require HTML & development mode)
  283. if (!connection_aborted() && self::$bar && !self::$productionMode && self::isHtmlMode()) {
  284. self::$bar->render();
  285. }
  286. }
  287. /**
  288. * Handler to catch uncaught exception.
  289. * @param \Exception
  290. * @return void
  291. * @internal
  292. */
  293. public static function _exceptionHandler(\Exception $exception)
  294. {
  295. if (!headers_sent()) {
  296. $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
  297. header($protocol . ' 500', TRUE, 500);
  298. }
  299. try {
  300. if (self::$productionMode) {
  301. try {
  302. self::log($exception, self::ERROR);
  303. } catch (\Exception $e) {
  304. echo 'FATAL ERROR: unable to log error';
  305. }
  306. if (self::isHtmlMode()) {
  307. require __DIR__ . '/templates/error.phtml';
  308. } else {
  309. echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
  310. }
  311. } else {
  312. if (!connection_aborted() && self::isHtmlMode()) {
  313. self::$blueScreen->render($exception);
  314. if (self::$bar) {
  315. self::$bar->render();
  316. }
  317. } elseif (connection_aborted() || !self::fireLog($exception)) {
  318. $file = self::log($exception, self::ERROR);
  319. if (!headers_sent()) {
  320. header("X-Nette-Error-Log: $file");
  321. }
  322. echo "$exception\n" . ($file ? "(stored in $file)\n" : '');
  323. if (self::$browser) {
  324. exec(self::$browser . ' ' . escapeshellarg($file));
  325. }
  326. }
  327. }
  328. foreach (self::$onFatalError as $handler) {
  329. call_user_func($handler, $exception);
  330. }
  331. } catch (\Exception $e) {
  332. if (self::$productionMode) {
  333. echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
  334. } else {
  335. echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
  336. "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
  337. }
  338. }
  339. self::$enabled = FALSE; // un-register shutdown function
  340. exit(254);
  341. }
  342. /**
  343. * Handler to catch warnings and notices.
  344. * @param int level of the error raised
  345. * @param string error message
  346. * @param string file that the error was raised in
  347. * @param int line number the error was raised at
  348. * @param array an array of variables that existed in the scope the error was triggered in
  349. * @return bool FALSE to call normal error handler, NULL otherwise
  350. * @throws ErrorException
  351. * @internal
  352. */
  353. public static function _errorHandler($severity, $message, $file, $line, $context)
  354. {
  355. if (self::$scream) {
  356. error_reporting(E_ALL | E_STRICT);
  357. }
  358. if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { // tryError mode
  359. self::$lastError = new ErrorException($message, 0, $severity, $file, $line);
  360. return NULL;
  361. }
  362. if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
  363. if (Helpers::findTrace(debug_backtrace(FALSE), '*::__toString')) {
  364. $previous = isset($context['e']) && $context['e'] instanceof \Exception ? $context['e'] : NULL;
  365. self::_exceptionHandler(new ErrorException($message, 0, $severity, $file, $line, $previous, $context));
  366. }
  367. throw new ErrorException($message, 0, $severity, $file, $line, NULL, $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 ErrorException($message, 0, $severity, $file, $line, NULL, $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));
  392. return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
  393. }
  394. return FALSE; // call normal error handler
  395. }
  396. /********************* useful tools ****************d*g**/
  397. /**
  398. * Dumps information about a variable in readable format.
  399. * @param mixed variable to dump
  400. * @param bool return output instead of printing it? (bypasses $productionMode)
  401. * @return mixed variable itself or dump
  402. */
  403. public static function dump($var, $return = FALSE)
  404. {
  405. if ($return) {
  406. ob_start();
  407. Dumper::dump($var, array(
  408. Dumper::DEPTH => self::$maxDepth,
  409. Dumper::TRUNCATE => self::$maxLen,
  410. ));
  411. return ob_get_clean();
  412. } elseif (!self::$productionMode) {
  413. Dumper::dump($var, array(
  414. Dumper::DEPTH => self::$maxDepth,
  415. Dumper::TRUNCATE => self::$maxLen,
  416. Dumper::LOCATION => self::$showLocation,
  417. ));
  418. }
  419. return $var;
  420. }
  421. /**
  422. * Starts/stops stopwatch.
  423. * @param string name
  424. * @return float elapsed seconds
  425. */
  426. public static function timer($name = NULL)
  427. {
  428. static $time = array();
  429. $now = microtime(TRUE);
  430. $delta = isset($time[$name]) ? $now - $time[$name] : 0;
  431. $time[$name] = $now;
  432. return $delta;
  433. }
  434. /**
  435. * Dumps information about a variable in Nette Debug Bar.
  436. * @param mixed variable to dump
  437. * @param string optional title
  438. * @return mixed variable itself
  439. */
  440. public static function barDump($var, $title = NULL)
  441. {
  442. if (!self::$productionMode) {
  443. $dump = array();
  444. foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
  445. $dump[$key] = Dumper::toHtml($val);
  446. }
  447. self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
  448. }
  449. return $var;
  450. }
  451. /**
  452. * Sends message to FireLogger console.
  453. * @param mixed message to log
  454. * @return bool was successful?
  455. */
  456. public static function fireLog($message)
  457. {
  458. if (!self::$productionMode) {
  459. return self::$fireLogger->log($message);
  460. }
  461. }
  462. private static function isHtmlMode()
  463. {
  464. return empty($_SERVER['HTTP_X_REQUESTED_WITH'])
  465. && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
  466. }
  467. }
  468. Debugger::_init();