Debugger.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <?php
  2. /**
  3. * @package Grav.Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use DebugBar\DataCollector\ConfigCollector;
  10. use DebugBar\DataCollector\MessagesCollector;
  11. use DebugBar\JavascriptRenderer;
  12. use DebugBar\StandardDebugBar;
  13. use Grav\Common\Config\Config;
  14. class Debugger
  15. {
  16. /** @var Grav $grav */
  17. protected $grav;
  18. /** @var Config $config */
  19. protected $config;
  20. /** @var JavascriptRenderer $renderer */
  21. protected $renderer;
  22. /** @var StandardDebugBar $debugbar */
  23. protected $debugbar;
  24. protected $enabled;
  25. protected $timers = [];
  26. /** @var string[] $deprecations */
  27. protected $deprecations = [];
  28. protected $errorHandler;
  29. /**
  30. * Debugger constructor.
  31. */
  32. public function __construct()
  33. {
  34. // Enable debugger until $this->init() gets called.
  35. $this->enabled = true;
  36. $this->debugbar = new StandardDebugBar();
  37. $this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true));
  38. // Set deprecation collector.
  39. $this->setErrorHandler();
  40. }
  41. /**
  42. * Initialize the debugger
  43. *
  44. * @return $this
  45. * @throws \DebugBar\DebugBarException
  46. */
  47. public function init()
  48. {
  49. $this->grav = Grav::instance();
  50. $this->config = $this->grav['config'];
  51. // Enable/disable debugger based on configuration.
  52. $this->enabled = $this->config->get('system.debugger.enabled');
  53. if ($this->enabled()) {
  54. $plugins_config = (array)$this->config->get('plugins');
  55. ksort($plugins_config);
  56. $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
  57. $this->debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
  58. $this->addMessage('Grav v' . GRAV_VERSION);
  59. }
  60. return $this;
  61. }
  62. /**
  63. * Set/get the enabled state of the debugger
  64. *
  65. * @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state
  66. *
  67. * @return null
  68. */
  69. public function enabled($state = null)
  70. {
  71. if ($state !== null) {
  72. $this->enabled = $state;
  73. }
  74. return $this->enabled;
  75. }
  76. /**
  77. * Add the debugger assets to the Grav Assets
  78. *
  79. * @return $this
  80. */
  81. public function addAssets()
  82. {
  83. if ($this->enabled()) {
  84. // Only add assets if Page is HTML
  85. $page = $this->grav['page'];
  86. if ($page->templateFormat() !== 'html') {
  87. return $this;
  88. }
  89. /** @var Assets $assets */
  90. $assets = $this->grav['assets'];
  91. // Add jquery library
  92. $assets->add('jquery', 101);
  93. $this->renderer = $this->debugbar->getJavascriptRenderer();
  94. $this->renderer->setIncludeVendors(false);
  95. // Get the required CSS files
  96. list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
  97. foreach ((array)$css_files as $css) {
  98. $assets->addCss($css);
  99. }
  100. $assets->addCss('/system/assets/debugger.css');
  101. foreach ((array)$js_files as $js) {
  102. $assets->addJs($js);
  103. }
  104. }
  105. return $this;
  106. }
  107. public function getCaller($limit = 2)
  108. {
  109. $trace = debug_backtrace(false, $limit);
  110. return array_pop($trace);
  111. }
  112. /**
  113. * Adds a data collector
  114. *
  115. * @param $collector
  116. *
  117. * @return $this
  118. * @throws \DebugBar\DebugBarException
  119. */
  120. public function addCollector($collector)
  121. {
  122. $this->debugbar->addCollector($collector);
  123. return $this;
  124. }
  125. /**
  126. * Returns a data collector
  127. *
  128. * @param $collector
  129. *
  130. * @return \DebugBar\DataCollector\DataCollectorInterface
  131. * @throws \DebugBar\DebugBarException
  132. */
  133. public function getCollector($collector)
  134. {
  135. return $this->debugbar->getCollector($collector);
  136. }
  137. /**
  138. * Displays the debug bar
  139. *
  140. * @return $this
  141. */
  142. public function render()
  143. {
  144. if ($this->enabled()) {
  145. // Only add assets if Page is HTML
  146. $page = $this->grav['page'];
  147. if (!$this->renderer || $page->templateFormat() !== 'html') {
  148. return $this;
  149. }
  150. $this->addDeprecations();
  151. echo $this->renderer->render();
  152. }
  153. return $this;
  154. }
  155. /**
  156. * Sends the data through the HTTP headers
  157. *
  158. * @return $this
  159. */
  160. public function sendDataInHeaders()
  161. {
  162. if ($this->enabled()) {
  163. $this->addDeprecations();
  164. $this->debugbar->sendDataInHeaders();
  165. }
  166. return $this;
  167. }
  168. /**
  169. * Returns collected debugger data.
  170. *
  171. * @return array
  172. */
  173. public function getData()
  174. {
  175. if (!$this->enabled()) {
  176. return null;
  177. }
  178. $this->addDeprecations();
  179. $this->timers = [];
  180. return $this->debugbar->getData();
  181. }
  182. /**
  183. * Start a timer with an associated name and description
  184. *
  185. * @param $name
  186. * @param string|null $description
  187. *
  188. * @return $this
  189. */
  190. public function startTimer($name, $description = null)
  191. {
  192. if ($name[0] === '_' || $this->enabled()) {
  193. $this->debugbar['time']->startMeasure($name, $description);
  194. $this->timers[] = $name;
  195. }
  196. return $this;
  197. }
  198. /**
  199. * Stop the named timer
  200. *
  201. * @param string $name
  202. *
  203. * @return $this
  204. */
  205. public function stopTimer($name)
  206. {
  207. if (in_array($name, $this->timers, true) && ($name[0] === '_' || $this->enabled())) {
  208. $this->debugbar['time']->stopMeasure($name);
  209. }
  210. return $this;
  211. }
  212. /**
  213. * Dump variables into the Messages tab of the Debug Bar
  214. *
  215. * @param $message
  216. * @param string $label
  217. * @param bool $isString
  218. *
  219. * @return $this
  220. */
  221. public function addMessage($message, $label = 'info', $isString = true)
  222. {
  223. if ($this->enabled()) {
  224. $this->debugbar['messages']->addMessage($message, $label, $isString);
  225. }
  226. return $this;
  227. }
  228. /**
  229. * Dump exception into the Messages tab of the Debug Bar
  230. *
  231. * @param \Exception $e
  232. * @return Debugger
  233. */
  234. public function addException(\Exception $e)
  235. {
  236. if ($this->enabled()) {
  237. $this->debugbar['exceptions']->addException($e);
  238. }
  239. return $this;
  240. }
  241. public function setErrorHandler()
  242. {
  243. $this->errorHandler = set_error_handler(
  244. [$this, 'deprecatedErrorHandler']
  245. );
  246. }
  247. /**
  248. * @param int $errno
  249. * @param string $errstr
  250. * @param string $errfile
  251. * @param int $errline
  252. * @return bool
  253. */
  254. public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
  255. {
  256. if ($errno !== E_USER_DEPRECATED) {
  257. if ($this->errorHandler) {
  258. return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
  259. }
  260. return true;
  261. }
  262. if (!$this->enabled()) {
  263. return true;
  264. }
  265. $backtrace = debug_backtrace(false);
  266. // Skip current call.
  267. array_shift($backtrace);
  268. // Skip vendor libraries and the method where error was triggered.
  269. while ($current = array_shift($backtrace)) {
  270. if (isset($current['file']) && strpos($current['file'], 'vendor') !== false) {
  271. continue;
  272. }
  273. if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
  274. $current = array_shift($backtrace);
  275. }
  276. break;
  277. }
  278. // Add back last call.
  279. array_unshift($backtrace, $current);
  280. // Filter arguments.
  281. foreach ($backtrace as &$current) {
  282. if (isset($current['args'])) {
  283. $args = [];
  284. foreach ($current['args'] as $arg) {
  285. if (\is_string($arg)) {
  286. $args[] = "'" . $arg . "'";
  287. } elseif (\is_bool($arg)) {
  288. $args[] = $arg ? 'true' : 'false';
  289. } elseif (\is_scalar($arg)) {
  290. $args[] = $arg;
  291. } elseif (\is_object($arg)) {
  292. $args[] = get_class($arg) . ' $object';
  293. } elseif (\is_array($arg)) {
  294. $args[] = '$array';
  295. } else {
  296. $args[] = '$object';
  297. }
  298. }
  299. $current['args'] = $args;
  300. }
  301. }
  302. unset($current);
  303. $this->deprecations[] = [
  304. 'message' => $errstr,
  305. 'file' => $errfile,
  306. 'line' => $errline,
  307. 'trace' => $backtrace,
  308. ];
  309. // Do not pass forward.
  310. return true;
  311. }
  312. protected function addDeprecations()
  313. {
  314. if (!$this->deprecations) {
  315. return;
  316. }
  317. $collector = new MessagesCollector('deprecated');
  318. $this->addCollector($collector);
  319. $collector->addMessage('Your site is using following deprecated features:');
  320. /** @var array $deprecated */
  321. foreach ($this->deprecations as $deprecated) {
  322. list($message, $scope) = $this->getDepracatedMessage($deprecated);
  323. $collector->addMessage($message, $scope);
  324. }
  325. }
  326. protected function getDepracatedMessage($deprecated)
  327. {
  328. $scope = 'unknown';
  329. if (stripos($deprecated['message'], 'grav') !== false) {
  330. $scope = 'grav';
  331. } elseif (!isset($deprecated['file'])) {
  332. $scope = 'unknown';
  333. } elseif (stripos($deprecated['file'], 'twig') !== false) {
  334. $scope = 'twig';
  335. } elseif (stripos($deprecated['file'], 'yaml') !== false) {
  336. $scope = 'yaml';
  337. } elseif (stripos($deprecated['file'], 'vendor') !== false) {
  338. $scope = 'vendor';
  339. }
  340. $trace = [];
  341. foreach ($deprecated['trace'] as $current) {
  342. $class = isset($current['class']) ? $current['class'] : '';
  343. $type = isset($current['type']) ? $current['type'] : '';
  344. $function = $this->getFunction($current);
  345. if (isset($current['file'])) {
  346. $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
  347. }
  348. unset($current['class'], $current['type'], $current['function'], $current['args']);
  349. $trace[] = ['call' => $class . $type . $function] + $current;
  350. }
  351. return [
  352. [
  353. 'message' => $deprecated['message'],
  354. 'trace' => $trace
  355. ],
  356. $scope
  357. ];
  358. }
  359. protected function getFunction($trace)
  360. {
  361. if (!isset($trace['function'])) {
  362. return '';
  363. }
  364. return $trace['function'] . '(' . implode(', ', $trace['args']) . ')';
  365. }
  366. }