PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/Nette/Diagnostics/Debugger.php

https://github.com/stekycz/nette
PHP | 667 lines | 405 code | 134 blank | 128 comment | 72 complexity | 1f4f5ed52957c268e0e6ad9038f484e5 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;
  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. /** @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. /********************* logging ****************d*g**/
  62. /** @var Logger */
  63. public static $logger;
  64. /** @var FireLogger */
  65. public static $fireLogger;
  66. /** @var string name of the directory where errors should be logged; FALSE means that logging is disabled */
  67. public static $logDirectory;
  68. /** @var string email to sent error notifications */
  69. public static $email;
  70. /** @deprecated */
  71. public static $mailer;
  72. /** @deprecated */
  73. public static $emailSnooze;
  74. /********************* debug bar ****************d*g**/
  75. /** @var Bar */
  76. public static $bar;
  77. /** @var DefaultBarPanel */
  78. private static $errorPanel;
  79. /** @var DefaultBarPanel */
  80. private static $dumpPanel;
  81. /********************* Firebug extension ****************d*g**/
  82. /** {@link Debugger::log()} and {@link Debugger::fireLog()} */
  83. const DEBUG = 'debug',
  84. INFO = 'info',
  85. WARNING = 'warning',
  86. ERROR = 'error',
  87. CRITICAL = 'critical';
  88. /**
  89. * Static class - cannot be instantiated.
  90. */
  91. final public function __construct()
  92. {
  93. throw new Nette\StaticClassException;
  94. }
  95. /**
  96. * Static class constructor.
  97. * @internal
  98. */
  99. public static function _init()
  100. {
  101. self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
  102. self::$productionMode = self::DETECT;
  103. if (isset($_SERVER['REQUEST_URI'])) {
  104. self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
  105. . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
  106. . $_SERVER['REQUEST_URI'];
  107. } else {
  108. self::$source = empty($_SERVER['argv']) ? 'CLI' : 'CLI: ' . implode(' ', $_SERVER['argv']);
  109. }
  110. self::$consoleColors = & Dumper::$terminalColors;
  111. self::$logger = new Logger;
  112. self::$logDirectory = & self::$logger->directory;
  113. self::$email = & self::$logger->email;
  114. self::$mailer = & self::$logger->mailer;
  115. self::$emailSnooze = & Logger::$emailSnooze;
  116. self::$fireLogger = new FireLogger;
  117. self::$blueScreen = new BlueScreen;
  118. self::$blueScreen->collapsePaths[] = NETTE_DIR;
  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) . '</p>'
  124. . ($e->sourceLine ? BlueScreen::highlightFile($e->sourceFile, $e->sourceLine) : '')
  125. );
  126. } elseif ($e instanceof Nette\Utils\NeonException && preg_match('#line (\d+)#', $e->getMessage(), $m)) {
  127. if ($item = Helpers::findTrace($e->getTrace(), 'Nette\Config\Adapters\NeonAdapter::load')) {
  128. return array(
  129. 'tab' => 'NEON',
  130. 'panel' => '<p><b>File:</b> ' . Helpers::editorLink($item['args'][0], $m[1]) . '</p>'
  131. . BlueScreen::highlightFile($item['args'][0], $m[1])
  132. );
  133. } elseif ($item = Helpers::findTrace($e->getTrace(), 'Nette\Utils\Neon::decode')) {
  134. return array(
  135. 'tab' => 'NEON',
  136. 'panel' => BlueScreen::highlightPhp($item['args'][0], $m[1])
  137. );
  138. }
  139. }
  140. });
  141. self::$bar = new Bar;
  142. self::$bar->addPanel(new DefaultBarPanel('time'));
  143. self::$bar->addPanel(new DefaultBarPanel('memory'));
  144. self::$bar->addPanel(self::$errorPanel = new DefaultBarPanel('errors')); // filled by _errorHandler()
  145. self::$bar->addPanel(self::$dumpPanel = new DefaultBarPanel('dumps')); // filled by barDump()
  146. }
  147. /********************* errors and exceptions reporting ****************d*g**/
  148. /**
  149. * Enables displaying or logging errors and exceptions.
  150. * @param mixed production, development mode, autodetection or IP address(es) whitelist.
  151. * @param string error log directory; enables logging in production mode, FALSE means that logging is disabled
  152. * @param string administrator email; enables email sending in production mode
  153. * @return void
  154. */
  155. public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
  156. {
  157. error_reporting(E_ALL | E_STRICT);
  158. // production/development mode detection
  159. if (is_bool($mode)) {
  160. self::$productionMode = $mode;
  161. } elseif ($mode !== self::DETECT || self::$productionMode === NULL) { // IP addresses or computer names whitelist detection
  162. $list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
  163. if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  164. $list[] = '127.0.0.1';
  165. $list[] = '::1';
  166. }
  167. self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
  168. }
  169. // logging configuration
  170. if (is_string($logDirectory)) {
  171. self::$logDirectory = realpath($logDirectory);
  172. if (self::$logDirectory === FALSE) {
  173. echo __METHOD__ . "() error: Log directory is not found or is not directory.\n";
  174. exit(254);
  175. }
  176. } elseif ($logDirectory === FALSE || self::$logDirectory === NULL) {
  177. self::$logDirectory = FALSE;
  178. }
  179. if (self::$logDirectory) {
  180. ini_set('error_log', self::$logDirectory . '/php_error.log');
  181. }
  182. // php configuration
  183. if (function_exists('ini_set')) {
  184. ini_set('display_errors', !self::$productionMode); // or 'stderr'
  185. ini_set('html_errors', FALSE);
  186. ini_set('log_errors', FALSE);
  187. } elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { // intentionally ==
  188. echo __METHOD__ . "() error: Unable to set 'display_errors' because function ini_set() is disabled.\n";
  189. exit(254);
  190. }
  191. if ($email) {
  192. if (!is_string($email)) {
  193. echo __METHOD__ . "() error: Email address must be a string.\n";
  194. exit(254);
  195. }
  196. self::$email = $email;
  197. }
  198. if (!self::$enabled) {
  199. register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
  200. set_exception_handler(array(__CLASS__, '_exceptionHandler'));
  201. set_error_handler(array(__CLASS__, '_errorHandler'));
  202. self::$enabled = TRUE;
  203. }
  204. }
  205. /**
  206. * Is Debug enabled?
  207. * @return bool
  208. */
  209. public static function isEnabled()
  210. {
  211. return self::$enabled;
  212. }
  213. /**
  214. * Logs message or exception to file (if not disabled) and sends email notification (if enabled).
  215. * @param string|Exception
  216. * @param int one of constant Debugger::INFO, WARNING, ERROR (sends email), CRITICAL (sends email)
  217. * @return string logged error filename
  218. */
  219. public static function log($message, $priority = self::INFO)
  220. {
  221. if (self::$logDirectory === FALSE) {
  222. return;
  223. } elseif (!self::$logDirectory) {
  224. throw new Nette\InvalidStateException('Logging directory is not specified in Nette\Diagnostics\Debugger::$logDirectory.');
  225. }
  226. $exceptionFilename = NULL;
  227. if ($message instanceof \Exception) {
  228. $exception = $message;
  229. while ($exception) {
  230. $tmp[] = ($exception instanceof Nette\FatalErrorException
  231. ? 'Fatal error: ' . $exception->getMessage()
  232. : get_class($exception) . ": " . $exception->getMessage())
  233. . " in " . $exception->getFile() . ":" . $exception->getLine();
  234. $exception = /*5.2*(method_exists($exception, 'getPrevious') ? */$exception->getPrevious()/*5.2* : (isset($exception->previous) ? $exception->previous : NULL))*/;
  235. }
  236. $exception = $message;
  237. $message = implode($tmp, "\ncaused by ");
  238. $hash = md5($exception /*5.2*. (method_exists($exception, 'getPrevious') ? $exception->getPrevious() : (isset($exception->previous) ? $exception->previous : ''))*/);
  239. $exceptionFilename = "exception-" . @date('Y-m-d-H-i-s') . "-$hash.html";
  240. foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
  241. if (strpos($entry, $hash)) {
  242. $exceptionFilename = $entry;
  243. $saved = TRUE;
  244. break;
  245. }
  246. }
  247. } elseif (!is_string($message)) {
  248. $message = Dumper::toText($message);
  249. }
  250. if ($exceptionFilename) {
  251. $exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
  252. if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
  253. ob_start(); // double buffer prevents sending HTTP headers in some PHP
  254. ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 4096);
  255. self::$blueScreen->render($exception);
  256. ob_end_flush();
  257. ob_end_clean();
  258. fclose($logHandle);
  259. }
  260. }
  261. self::$logger->log(array(
  262. @date('[Y-m-d H-i-s]'),
  263. trim($message),
  264. self::$source ? ' @ ' . self::$source : NULL,
  265. $exceptionFilename ? ' @@ ' . basename($exceptionFilename) : NULL
  266. ), $priority);
  267. return $exceptionFilename ? strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR) : NULL;
  268. }
  269. /**
  270. * Shutdown handler to catch fatal errors and execute of the planned activities.
  271. * @return void
  272. * @internal
  273. */
  274. public static function _shutdownHandler()
  275. {
  276. if (!self::$enabled) {
  277. return;
  278. }
  279. // fatal error handler
  280. static $types = array(
  281. E_ERROR => 1,
  282. E_CORE_ERROR => 1,
  283. E_COMPILE_ERROR => 1,
  284. E_PARSE => 1,
  285. );
  286. $error = error_get_last();
  287. if (isset($types[$error['type']])) {
  288. $exception = new Nette\FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL);
  289. if (PHP_VERSION_ID >= 50300 && function_exists('xdebug_get_function_stack')) {
  290. $stack = array();
  291. foreach (array_slice(array_reverse(xdebug_get_function_stack()), 1, -1) as $row) {
  292. $frame = array(
  293. 'file' => $row['file'],
  294. 'line' => $row['line'],
  295. 'function' => isset($row['function']) ? $row['function'] : '*unknown*',
  296. 'args' => array(),
  297. );
  298. if (!empty($row['class'])) {
  299. $frame['type'] = isset($row['type']) && $row['type'] === 'dynamic' ? '->' : '::';
  300. $frame['class'] = $row['class'];
  301. }
  302. $stack[] = $frame;
  303. }
  304. $ref = new \ReflectionProperty('Exception', 'trace');
  305. $ref->setAccessible(TRUE);
  306. $ref->setValue($exception, $stack);
  307. }
  308. self::_exceptionHandler($exception);
  309. }
  310. // debug bar (require HTML & development mode)
  311. if (!connection_aborted() && self::$bar && !self::$productionMode && self::isHtmlMode()) {
  312. self::$bar->render();
  313. }
  314. }
  315. /**
  316. * Handler to catch uncaught exception.
  317. * @param \Exception
  318. * @return void
  319. * @internal
  320. */
  321. public static function _exceptionHandler(\Exception $exception)
  322. {
  323. if (!headers_sent()) { // for PHP < 5.2.4
  324. $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
  325. header($protocol . ' 500', TRUE, 500);
  326. }
  327. try {
  328. if (self::$productionMode) {
  329. try {
  330. self::log($exception, self::ERROR);
  331. } catch (\Exception $e) {
  332. echo 'FATAL ERROR: unable to log error';
  333. }
  334. if (self::isHtmlMode()) {
  335. require __DIR__ . '/templates/error.phtml';
  336. } else {
  337. echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
  338. }
  339. } else {
  340. if (!connection_aborted() && self::isHtmlMode()) {
  341. self::$blueScreen->render($exception);
  342. if (self::$bar) {
  343. self::$bar->render();
  344. }
  345. } elseif (connection_aborted() || !self::fireLog($exception)) {
  346. $file = self::log($exception, self::ERROR);
  347. if (!headers_sent()) {
  348. header("X-Nette-Error-Log: $file");
  349. }
  350. echo "$exception\n" . ($file ? "(stored in $file)\n" : '');
  351. if (self::$browser) {
  352. exec(self::$browser . ' ' . escapeshellarg($file));
  353. }
  354. }
  355. }
  356. foreach (self::$onFatalError as $handler) {
  357. call_user_func($handler, $exception);
  358. }
  359. } catch (\Exception $e) {
  360. if (self::$productionMode) {
  361. echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
  362. } else {
  363. echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
  364. "\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
  365. }
  366. }
  367. self::$enabled = FALSE; // un-register shutdown function
  368. exit(254);
  369. }
  370. /**
  371. * Handler to catch warnings and notices.
  372. * @param int level of the error raised
  373. * @param string error message
  374. * @param string file that the error was raised in
  375. * @param int line number the error was raised at
  376. * @param array an array of variables that existed in the scope the error was triggered in
  377. * @return bool FALSE to call normal error handler, NULL otherwise
  378. * @throws Nette\FatalErrorException
  379. * @internal
  380. */
  381. public static function _errorHandler($severity, $message, $file, $line, $context)
  382. {
  383. if (self::$scream) {
  384. error_reporting(E_ALL | E_STRICT);
  385. }
  386. if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { // tryError mode
  387. self::$lastError = new \ErrorException($message, 0, $severity, $file, $line);
  388. return NULL;
  389. }
  390. if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
  391. if (Helpers::findTrace(/*5.2*PHP_VERSION_ID < 50205 ? debug_backtrace() : */debug_backtrace(FALSE), '*::__toString')) {
  392. $previous = isset($context['e']) && $context['e'] instanceof \Exception ? $context['e'] : NULL;
  393. self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context, $previous));
  394. }
  395. throw new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context);
  396. } elseif (($severity & error_reporting()) !== $severity) {
  397. return FALSE; // calls normal error handler to fill-in error_get_last()
  398. } elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
  399. self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context));
  400. }
  401. static $types = array(
  402. E_WARNING => 'Warning',
  403. E_COMPILE_WARNING => 'Warning', // currently unable to handle
  404. E_USER_WARNING => 'Warning',
  405. E_NOTICE => 'Notice',
  406. E_USER_NOTICE => 'Notice',
  407. E_STRICT => 'Strict standards',
  408. );
  409. if (PHP_VERSION_ID >= 50300) {
  410. $types += array(
  411. E_DEPRECATED => 'Deprecated',
  412. E_USER_DEPRECATED => 'Deprecated',
  413. );
  414. }
  415. $message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
  416. $count = & self::$errorPanel->data["$message|$file|$line"];
  417. if ($count++) { // repeated error
  418. return NULL;
  419. } elseif (self::$productionMode) {
  420. self::log("$message in $file:$line", self::ERROR);
  421. return NULL;
  422. } else {
  423. $ok = self::fireLog(new \ErrorException($message, 0, $severity, $file, $line));
  424. return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
  425. }
  426. return FALSE; // call normal error handler
  427. }
  428. /** @deprecated */
  429. public static function toStringException(\Exception $exception)
  430. {
  431. if (self::$enabled) {
  432. self::_exceptionHandler($exception);
  433. } else {
  434. trigger_error($exception->getMessage(), E_USER_ERROR);
  435. }
  436. }
  437. /** @deprecated */
  438. public static function tryError()
  439. {
  440. trigger_error(__METHOD__ . '() is deprecated; use own error handler instead.', E_USER_DEPRECATED);
  441. if (!self::$enabled && self::$lastError === FALSE) {
  442. set_error_handler(array(__CLASS__, '_errorHandler'));
  443. }
  444. self::$lastError = NULL;
  445. }
  446. /** @deprecated */
  447. public static function catchError(& $error)
  448. {
  449. trigger_error(__METHOD__ . '() is deprecated; use own error handler instead.', E_USER_DEPRECATED);
  450. if (!self::$enabled && self::$lastError !== FALSE) {
  451. restore_error_handler();
  452. }
  453. $error = self::$lastError;
  454. self::$lastError = FALSE;
  455. return (bool) $error;
  456. }
  457. /********************* useful tools ****************d*g**/
  458. /**
  459. * Dumps information about a variable in readable format.
  460. * @param mixed variable to dump
  461. * @param bool return output instead of printing it? (bypasses $productionMode)
  462. * @return mixed variable itself or dump
  463. */
  464. public static function dump($var, $return = FALSE)
  465. {
  466. if ($return) {
  467. ob_start();
  468. Dumper::dump($var, array(
  469. Dumper::DEPTH => self::$maxDepth,
  470. Dumper::TRUNCATE => self::$maxLen,
  471. ));
  472. return ob_get_clean();
  473. } elseif (!self::$productionMode) {
  474. Dumper::dump($var, array(
  475. Dumper::DEPTH => self::$maxDepth,
  476. Dumper::TRUNCATE => self::$maxLen,
  477. Dumper::LOCATION => self::$showLocation,
  478. ));
  479. }
  480. return $var;
  481. }
  482. /**
  483. * Starts/stops stopwatch.
  484. * @param string name
  485. * @return float elapsed seconds
  486. */
  487. public static function timer($name = NULL)
  488. {
  489. static $time = array();
  490. $now = microtime(TRUE);
  491. $delta = isset($time[$name]) ? $now - $time[$name] : 0;
  492. $time[$name] = $now;
  493. return $delta;
  494. }
  495. /**
  496. * Dumps information about a variable in Nette Debug Bar.
  497. * @param mixed variable to dump
  498. * @param string optional title
  499. * @return mixed variable itself
  500. */
  501. public static function barDump($var, $title = NULL)
  502. {
  503. if (!self::$productionMode) {
  504. $dump = array();
  505. foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
  506. $dump[$key] = Dumper::toHtml($val);
  507. }
  508. self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
  509. }
  510. return $var;
  511. }
  512. /**
  513. * Sends message to FireLogger console.
  514. * @param mixed message to log
  515. * @return bool was successful?
  516. */
  517. public static function fireLog($message)
  518. {
  519. if (!self::$productionMode) {
  520. return self::$fireLogger->log($message);
  521. }
  522. }
  523. private static function isHtmlMode()
  524. {
  525. return empty($_SERVER['HTTP_X_REQUESTED_WITH'])
  526. && preg_match('#^Content-Type: text/html#im', implode("\n", headers_list()));
  527. }
  528. /** @deprecated */
  529. public static function addPanel(IBarPanel $panel, $id = NULL)
  530. {
  531. return self::$bar->addPanel($panel, $id);
  532. }
  533. }