Debugger.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Clockwork\Clockwork;
  10. use Clockwork\DataSource\MonologDataSource;
  11. use Clockwork\DataSource\PsrMessageDataSource;
  12. use Clockwork\DataSource\XdebugDataSource;
  13. use Clockwork\Helpers\ServerTiming;
  14. use Clockwork\Request\UserData;
  15. use Clockwork\Storage\FileStorage;
  16. use DebugBar\DataCollector\ConfigCollector;
  17. use DebugBar\DataCollector\DataCollectorInterface;
  18. use DebugBar\DataCollector\ExceptionsCollector;
  19. use DebugBar\DataCollector\MemoryCollector;
  20. use DebugBar\DataCollector\MessagesCollector;
  21. use DebugBar\DataCollector\PhpInfoCollector;
  22. use DebugBar\DataCollector\RequestDataCollector;
  23. use DebugBar\DataCollector\TimeDataCollector;
  24. use DebugBar\DebugBar;
  25. use DebugBar\DebugBarException;
  26. use DebugBar\JavascriptRenderer;
  27. use Grav\Common\Config\Config;
  28. use Grav\Common\Processors\ProcessorInterface;
  29. use Grav\Common\Twig\TwigClockworkDataSource;
  30. use Grav\Framework\Psr7\Response;
  31. use Monolog\Logger;
  32. use Psr\Http\Message\RequestInterface;
  33. use Psr\Http\Message\ResponseInterface;
  34. use Psr\Http\Message\ServerRequestInterface;
  35. use ReflectionObject;
  36. use SplFileInfo;
  37. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  38. use Throwable;
  39. use Twig\Environment;
  40. use Twig\Template;
  41. use Twig\TemplateWrapper;
  42. use function array_slice;
  43. use function call_user_func;
  44. use function count;
  45. use function define;
  46. use function defined;
  47. use function extension_loaded;
  48. use function get_class;
  49. use function gettype;
  50. use function is_array;
  51. use function is_bool;
  52. use function is_object;
  53. use function is_scalar;
  54. use function is_string;
  55. /**
  56. * Class Debugger
  57. * @package Grav\Common
  58. */
  59. class Debugger
  60. {
  61. /** @var static */
  62. protected static $instance;
  63. /** @var Grav|null */
  64. protected $grav;
  65. /** @var Config|null */
  66. protected $config;
  67. /** @var JavascriptRenderer|null */
  68. protected $renderer;
  69. /** @var DebugBar|null */
  70. protected $debugbar;
  71. /** @var Clockwork|null */
  72. protected $clockwork;
  73. /** @var bool */
  74. protected $enabled = false;
  75. /** @var bool */
  76. protected $initialized = false;
  77. /** @var array */
  78. protected $timers = [];
  79. /** @var array */
  80. protected $deprecations = [];
  81. /** @var callable|null */
  82. protected $errorHandler;
  83. /** @var float */
  84. protected $requestTime;
  85. /** @var float */
  86. protected $currentTime;
  87. /** @var int */
  88. protected $profiling = 0;
  89. /** @var bool */
  90. protected $censored = false;
  91. /**
  92. * Debugger constructor.
  93. */
  94. public function __construct()
  95. {
  96. static::$instance = $this;
  97. $this->currentTime = microtime(true);
  98. if (!defined('GRAV_REQUEST_TIME')) {
  99. define('GRAV_REQUEST_TIME', $this->currentTime);
  100. }
  101. $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME;
  102. // Set deprecation collector.
  103. $this->setErrorHandler();
  104. }
  105. /**
  106. * @return Clockwork|null
  107. */
  108. public function getClockwork(): ?Clockwork
  109. {
  110. return $this->enabled ? $this->clockwork : null;
  111. }
  112. /**
  113. * Initialize the debugger
  114. *
  115. * @return $this
  116. * @throws DebugBarException
  117. */
  118. public function init()
  119. {
  120. if ($this->initialized) {
  121. return $this;
  122. }
  123. $this->grav = Grav::instance();
  124. $this->config = $this->grav['config'];
  125. // Enable/disable debugger based on configuration.
  126. $this->enabled = (bool)$this->config->get('system.debugger.enabled');
  127. $this->censored = (bool)$this->config->get('system.debugger.censored', false);
  128. if ($this->enabled) {
  129. $this->initialized = true;
  130. $clockwork = $debugbar = null;
  131. switch ($this->config->get('system.debugger.provider', 'debugbar')) {
  132. case 'clockwork':
  133. $this->clockwork = $clockwork = new Clockwork();
  134. break;
  135. default:
  136. $this->debugbar = $debugbar = new DebugBar();
  137. }
  138. $plugins_config = (array)$this->config->get('plugins');
  139. ksort($plugins_config);
  140. if ($clockwork) {
  141. $log = $this->grav['log'];
  142. $clockwork->setStorage(new FileStorage('cache://clockwork'));
  143. if (extension_loaded('xdebug')) {
  144. $clockwork->addDataSource(new XdebugDataSource());
  145. }
  146. if ($log instanceof Logger) {
  147. $clockwork->addDataSource(new MonologDataSource($log));
  148. }
  149. $timeline = $clockwork->timeline();
  150. if ($this->requestTime !== GRAV_REQUEST_TIME) {
  151. $event = $timeline->event('Server');
  152. $event->finalize($this->requestTime, GRAV_REQUEST_TIME);
  153. }
  154. if ($this->currentTime !== GRAV_REQUEST_TIME) {
  155. $event = $timeline->event('Loading');
  156. $event->finalize(GRAV_REQUEST_TIME, $this->currentTime);
  157. }
  158. $event = $timeline->event('Site Setup');
  159. $event->finalize($this->currentTime, microtime(true));
  160. }
  161. if ($this->censored) {
  162. $censored = ['CENSORED' => true];
  163. }
  164. if ($debugbar) {
  165. $debugbar->addCollector(new PhpInfoCollector());
  166. $debugbar->addCollector(new MessagesCollector());
  167. if (!$this->censored) {
  168. $debugbar->addCollector(new RequestDataCollector());
  169. }
  170. $debugbar->addCollector(new TimeDataCollector($this->requestTime));
  171. $debugbar->addCollector(new MemoryCollector());
  172. $debugbar->addCollector(new ExceptionsCollector());
  173. $debugbar->addCollector(new ConfigCollector($censored ?? (array)$this->config->get('system'), 'Config'));
  174. $debugbar->addCollector(new ConfigCollector($censored ?? $plugins_config, 'Plugins'));
  175. $debugbar->addCollector(new ConfigCollector($this->config->get('streams.schemes'), 'Streams'));
  176. if ($this->requestTime !== GRAV_REQUEST_TIME) {
  177. $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
  178. }
  179. if ($this->currentTime !== GRAV_REQUEST_TIME) {
  180. $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $this->currentTime);
  181. }
  182. $debugbar['time']->addMeasure('Site Setup', $this->currentTime, microtime(true));
  183. }
  184. $this->addMessage('Grav v' . GRAV_VERSION . ' - PHP ' . PHP_VERSION);
  185. $this->config->debug();
  186. if ($clockwork) {
  187. $clockwork->info('System Configuration', $censored ?? $this->config->get('system'));
  188. $clockwork->info('Plugins Configuration', $censored ?? $plugins_config);
  189. $clockwork->info('Streams', $this->config->get('streams.schemes'));
  190. }
  191. }
  192. return $this;
  193. }
  194. public function finalize(): void
  195. {
  196. if ($this->clockwork && $this->enabled) {
  197. $this->stopProfiling('Profiler Analysis');
  198. $this->addMeasures();
  199. $deprecations = $this->getDeprecations();
  200. $count = count($deprecations);
  201. if (!$count) {
  202. return;
  203. }
  204. /** @var UserData $userData */
  205. $userData = $this->clockwork->userData('Deprecated');
  206. $userData->counters([
  207. 'Deprecated' => count($deprecations)
  208. ]);
  209. /*
  210. foreach ($deprecations as &$deprecation) {
  211. $d = $deprecation;
  212. unset($d['message']);
  213. $this->clockwork->log('deprecated', $deprecation['message'], $d);
  214. }
  215. unset($deprecation);
  216. */
  217. $userData->table('Your site is using following deprecated features', $deprecations);
  218. }
  219. }
  220. public function logRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
  221. {
  222. if (!$this->enabled || !$this->clockwork) {
  223. return $response;
  224. }
  225. $clockwork = $this->clockwork;
  226. $this->finalize();
  227. $clockwork->timeline()->finalize($request->getAttribute('request_time'));
  228. if ($this->censored) {
  229. $censored = 'CENSORED';
  230. $request = $request
  231. ->withCookieParams([$censored => ''])
  232. ->withUploadedFiles([])
  233. ->withHeader('cookie', $censored);
  234. $request = $request->withParsedBody([$censored => '']);
  235. }
  236. $clockwork->addDataSource(new PsrMessageDataSource($request, $response));
  237. $clockwork->resolveRequest();
  238. $clockwork->storeRequest();
  239. $clockworkRequest = $clockwork->getRequest();
  240. $response = $response
  241. ->withHeader('X-Clockwork-Id', $clockworkRequest->id)
  242. ->withHeader('X-Clockwork-Version', $clockwork::VERSION);
  243. $grav = Grav::instance();
  244. $basePath = $this->grav['base_url_relative'] . $grav['pages']->base();
  245. if ($basePath) {
  246. $response = $response->withHeader('X-Clockwork-Path', $basePath . '/__clockwork/');
  247. }
  248. return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value());
  249. }
  250. public function debuggerRequest(RequestInterface $request): Response
  251. {
  252. $clockwork = $this->clockwork;
  253. $headers = [
  254. 'Content-Type' => 'application/json',
  255. 'Grav-Internal-SkipShutdown' => 1
  256. ];
  257. $path = $request->getUri()->getPath();
  258. $clockworkDataUri = '#/__clockwork(?:/(?<id>[0-9-]+))?(?:/(?<direction>(?:previous|next)))?(?:/(?<count>\d+))?#';
  259. if (preg_match($clockworkDataUri, $path, $matches) === false) {
  260. $response = ['message' => 'Bad Input'];
  261. return new Response(400, $headers, json_encode($response));
  262. }
  263. $id = $matches['id'] ?? null;
  264. $direction = $matches['direction'] ?? null;
  265. $count = $matches['count'] ?? null;
  266. $storage = $clockwork->getStorage();
  267. if ($direction === 'previous') {
  268. $data = $storage->previous($id, $count);
  269. } elseif ($direction === 'next') {
  270. $data = $storage->next($id, $count);
  271. } elseif ($id === 'latest') {
  272. $data = $storage->latest();
  273. } else {
  274. $data = $storage->find($id);
  275. }
  276. if (preg_match('#(?<id>[0-9-]+|latest)/extended#', $path)) {
  277. $clockwork->extendRequest($data);
  278. }
  279. if (!$data) {
  280. $response = ['message' => 'Not Found'];
  281. return new Response(404, $headers, json_encode($response));
  282. }
  283. $data = is_array($data) ? array_map(function ($item) {
  284. return $item->toArray();
  285. }, $data) : $data->toArray();
  286. return new Response(200, $headers, json_encode($data));
  287. }
  288. /**
  289. * @return void
  290. */
  291. protected function addMeasures(): void
  292. {
  293. if (!$this->enabled) {
  294. return;
  295. }
  296. $nowTime = microtime(true);
  297. $clkTimeLine = $this->clockwork ? $this->clockwork->timeline() : null;
  298. $debTimeLine = $this->debugbar ? $this->debugbar['time'] : null;
  299. foreach ($this->timers as $name => $data) {
  300. $description = $data[0];
  301. $startTime = $data[1] ?? null;
  302. $endTime = $data[2] ?? $nowTime;
  303. if ($clkTimeLine) {
  304. $event = $clkTimeLine->event($description);
  305. $event->finalize($startTime, $endTime);
  306. } elseif ($debTimeLine) {
  307. if ($endTime - $startTime < 0.001) {
  308. continue;
  309. }
  310. $debTimeLine->addMeasure($description ?? $name, $startTime, $endTime);
  311. }
  312. }
  313. $this->timers = [];
  314. }
  315. /**
  316. * Set/get the enabled state of the debugger
  317. *
  318. * @param bool|null $state If null, the method returns the enabled value. If set, the method sets the enabled state
  319. * @return bool
  320. */
  321. public function enabled($state = null)
  322. {
  323. if ($state !== null) {
  324. $this->enabled = (bool)$state;
  325. }
  326. return $this->enabled;
  327. }
  328. /**
  329. * Add the debugger assets to the Grav Assets
  330. *
  331. * @return $this
  332. */
  333. public function addAssets()
  334. {
  335. if ($this->enabled) {
  336. // Only add assets if Page is HTML
  337. $page = $this->grav['page'];
  338. if ($page->templateFormat() !== 'html') {
  339. return $this;
  340. }
  341. /** @var Assets $assets */
  342. $assets = $this->grav['assets'];
  343. // Clockwork specific assets
  344. if ($this->clockwork) {
  345. $assets->addCss('/system/assets/debugger/clockwork.css', ['loading' => 'inline']);
  346. $assets->addJs('/system/assets/debugger/clockwork.js', ['loading' => 'inline']);
  347. }
  348. // Debugbar specific assets
  349. if ($this->debugbar) {
  350. // Add jquery library
  351. $assets->add('jquery', 101);
  352. $this->renderer = $this->debugbar->getJavascriptRenderer();
  353. $this->renderer->setIncludeVendors(false);
  354. [$css_files, $js_files] = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
  355. foreach ((array)$css_files as $css) {
  356. $assets->addCss($css);
  357. }
  358. $assets->addCss('/system/assets/debugger/phpdebugbar.css', ['loading' => 'inline']);
  359. foreach ((array)$js_files as $js) {
  360. $assets->addJs($js);
  361. }
  362. }
  363. }
  364. return $this;
  365. }
  366. /**
  367. * @param int $limit
  368. * @return array
  369. */
  370. public function getCaller($limit = 2)
  371. {
  372. $trace = debug_backtrace(false, $limit);
  373. return array_pop($trace);
  374. }
  375. /**
  376. * Adds a data collector
  377. *
  378. * @param DataCollectorInterface $collector
  379. * @return $this
  380. * @throws DebugBarException
  381. */
  382. public function addCollector($collector)
  383. {
  384. if ($this->debugbar && !$this->debugbar->hasCollector($collector->getName())) {
  385. $this->debugbar->addCollector($collector);
  386. }
  387. return $this;
  388. }
  389. /**
  390. * Returns a data collector
  391. *
  392. * @param string $name
  393. * @return DataCollectorInterface|null
  394. * @throws DebugBarException
  395. */
  396. public function getCollector($name)
  397. {
  398. if ($this->debugbar && $this->debugbar->hasCollector($name)) {
  399. return $this->debugbar->getCollector($name);
  400. }
  401. return null;
  402. }
  403. /**
  404. * Displays the debug bar
  405. *
  406. * @return $this
  407. */
  408. public function render()
  409. {
  410. if ($this->enabled && $this->debugbar) {
  411. // Only add assets if Page is HTML
  412. $page = $this->grav['page'];
  413. if (!$this->renderer || $page->templateFormat() !== 'html') {
  414. return $this;
  415. }
  416. $this->addMeasures();
  417. $this->addDeprecations();
  418. echo $this->renderer->render();
  419. }
  420. return $this;
  421. }
  422. /**
  423. * Sends the data through the HTTP headers
  424. *
  425. * @return $this
  426. */
  427. public function sendDataInHeaders()
  428. {
  429. if ($this->enabled && $this->debugbar) {
  430. $this->addMeasures();
  431. $this->addDeprecations();
  432. $this->debugbar->sendDataInHeaders();
  433. }
  434. return $this;
  435. }
  436. /**
  437. * Returns collected debugger data.
  438. *
  439. * @return array|null
  440. */
  441. public function getData()
  442. {
  443. if (!$this->enabled || !$this->debugbar) {
  444. return null;
  445. }
  446. $this->addMeasures();
  447. $this->addDeprecations();
  448. $this->timers = [];
  449. return $this->debugbar->getData();
  450. }
  451. /**
  452. * Hierarchical Profiler support.
  453. *
  454. * @param callable $callable
  455. * @param string|null $message
  456. * @return mixed
  457. */
  458. public function profile(callable $callable, string $message = null)
  459. {
  460. $this->startProfiling();
  461. $response = $callable();
  462. $this->stopProfiling($message);
  463. return $response;
  464. }
  465. public function addTwigProfiler(Environment $twig): void
  466. {
  467. $clockwork = $this->getClockwork();
  468. if ($clockwork) {
  469. $source = new TwigClockworkDataSource($twig);
  470. $source->listenToEvents();
  471. $clockwork->addDataSource($source);
  472. }
  473. }
  474. /**
  475. * Start profiling code.
  476. *
  477. * @return void
  478. */
  479. public function startProfiling(): void
  480. {
  481. if ($this->enabled && extension_loaded('tideways_xhprof')) {
  482. $this->profiling++;
  483. if ($this->profiling === 1) {
  484. // @phpstan-ignore-next-line
  485. \tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_NO_BUILTINS);
  486. }
  487. }
  488. }
  489. /**
  490. * Stop profiling code. Returns profiling array or null if profiling couldn't be done.
  491. *
  492. * @param string|null $message
  493. * @return array|null
  494. */
  495. public function stopProfiling(string $message = null): ?array
  496. {
  497. $timings = null;
  498. if ($this->enabled && extension_loaded('tideways_xhprof')) {
  499. $profiling = $this->profiling - 1;
  500. if ($profiling === 0) {
  501. // @phpstan-ignore-next-line
  502. $timings = \tideways_xhprof_disable();
  503. $timings = $this->buildProfilerTimings($timings);
  504. if ($this->clockwork) {
  505. /** @var UserData $userData */
  506. $userData = $this->clockwork->userData('Profiler');
  507. $userData->counters([
  508. 'Calls' => count($timings)
  509. ]);
  510. $userData->table('Profiler', $timings);
  511. } else {
  512. $this->addMessage($message ?? 'Profiler Analysis', 'debug', $timings);
  513. }
  514. }
  515. $this->profiling = max(0, $profiling);
  516. }
  517. return $timings;
  518. }
  519. /**
  520. * @param array $timings
  521. * @return array
  522. */
  523. protected function buildProfilerTimings(array $timings): array
  524. {
  525. // Filter method calls which take almost no time.
  526. $timings = array_filter($timings, function ($value) {
  527. return $value['wt'] > 50;
  528. });
  529. uasort($timings, function (array $a, array $b) {
  530. return $b['wt'] <=> $a['wt'];
  531. });
  532. $table = [];
  533. foreach ($timings as $key => $timing) {
  534. $parts = explode('==>', $key);
  535. $method = $this->parseProfilerCall(array_pop($parts));
  536. $context = $this->parseProfilerCall(array_pop($parts));
  537. // Skip redundant method calls.
  538. if ($context === 'Grav\Framework\RequestHandler\RequestHandler::handle()') {
  539. continue;
  540. }
  541. // Do not profile library calls.
  542. if (strpos($context, 'Grav\\') !== 0) {
  543. continue;
  544. }
  545. $table[] = [
  546. 'Context' => $context,
  547. 'Method' => $method,
  548. 'Calls' => $timing['ct'],
  549. 'Time (ms)' => $timing['wt'] / 1000,
  550. ];
  551. }
  552. return $table;
  553. }
  554. /**
  555. * @param string|null $call
  556. * @return mixed|string|null
  557. */
  558. protected function parseProfilerCall(?string $call)
  559. {
  560. if (null === $call) {
  561. return '';
  562. }
  563. if (strpos($call, '@')) {
  564. [$call,] = explode('@', $call);
  565. }
  566. if (strpos($call, '::')) {
  567. [$class, $call] = explode('::', $call);
  568. }
  569. if (!isset($class)) {
  570. return $call;
  571. }
  572. // It is also possible to display twig files, but they are being logged in views.
  573. /*
  574. if (strpos($class, '__TwigTemplate_') === 0 && class_exists($class)) {
  575. $env = new Environment();
  576. / ** @var Template $template * /
  577. $template = new $class($env);
  578. return $template->getTemplateName();
  579. }
  580. */
  581. return "{$class}::{$call}()";
  582. }
  583. /**
  584. * Start a timer with an associated name and description
  585. *
  586. * @param string $name
  587. * @param string|null $description
  588. * @return $this
  589. */
  590. public function startTimer($name, $description = null)
  591. {
  592. $this->timers[$name] = [$description, microtime(true)];
  593. return $this;
  594. }
  595. /**
  596. * Stop the named timer
  597. *
  598. * @param string $name
  599. * @return $this
  600. */
  601. public function stopTimer($name)
  602. {
  603. if (isset($this->timers[$name])) {
  604. $endTime = microtime(true);
  605. $this->timers[$name][] = $endTime;
  606. }
  607. return $this;
  608. }
  609. /**
  610. * Dump variables into the Messages tab of the Debug Bar
  611. *
  612. * @param mixed $message
  613. * @param string $label
  614. * @param mixed|bool $isString
  615. * @return $this
  616. */
  617. public function addMessage($message, $label = 'info', $isString = true)
  618. {
  619. if ($this->enabled) {
  620. if ($this->censored) {
  621. if (!is_scalar($message)) {
  622. $message = 'CENSORED';
  623. }
  624. if (!is_scalar($isString)) {
  625. $isString = ['CENSORED'];
  626. }
  627. }
  628. if ($this->debugbar) {
  629. if (is_array($isString)) {
  630. $message = $isString;
  631. $isString = false;
  632. } elseif (is_string($isString)) {
  633. $message = $isString;
  634. $isString = true;
  635. }
  636. $this->debugbar['messages']->addMessage($message, $label, $isString);
  637. }
  638. if ($this->clockwork) {
  639. $context = $isString;
  640. if (!is_scalar($message)) {
  641. $context = $message;
  642. $message = gettype($context);
  643. }
  644. if (is_bool($context)) {
  645. $context = [];
  646. } elseif (!is_array($context)) {
  647. $type = gettype($context);
  648. $context = [$type => $context];
  649. }
  650. $this->clockwork->log($label, $message, $context);
  651. }
  652. }
  653. return $this;
  654. }
  655. /**
  656. * @param string $name
  657. * @param object $event
  658. * @param EventDispatcherInterface $dispatcher
  659. * @param float|null $time
  660. * @return $this
  661. */
  662. public function addEvent(string $name, $event, EventDispatcherInterface $dispatcher, float $time = null)
  663. {
  664. if ($this->enabled && $this->clockwork) {
  665. $time = $time ?? microtime(true);
  666. $duration = (microtime(true) - $time) * 1000;
  667. $data = null;
  668. if ($event && method_exists($event, '__debugInfo')) {
  669. $data = $event;
  670. }
  671. $listeners = [];
  672. foreach ($dispatcher->getListeners($name) as $listener) {
  673. $listeners[] = $this->resolveCallable($listener);
  674. }
  675. $this->clockwork->addEvent($name, $data, $time, ['listeners' => $listeners, 'duration' => $duration]);
  676. }
  677. return $this;
  678. }
  679. /**
  680. * Dump exception into the Messages tab of the Debug Bar
  681. *
  682. * @param Throwable $e
  683. * @return Debugger
  684. */
  685. public function addException(Throwable $e)
  686. {
  687. if ($this->initialized && $this->enabled) {
  688. if ($this->debugbar) {
  689. $this->debugbar['exceptions']->addThrowable($e);
  690. }
  691. if ($this->clockwork) {
  692. /** @var UserData $exceptions */
  693. $exceptions = $this->clockwork->userData('Exceptions');
  694. $exceptions->data(['message' => $e->getMessage()]);
  695. $this->clockwork->alert($e->getMessage(), ['exception' => $e]);
  696. }
  697. }
  698. return $this;
  699. }
  700. /**
  701. * @return void
  702. */
  703. public function setErrorHandler()
  704. {
  705. $this->errorHandler = set_error_handler(
  706. [$this, 'deprecatedErrorHandler']
  707. );
  708. }
  709. /**
  710. * @param int $errno
  711. * @param string $errstr
  712. * @param string $errfile
  713. * @param int $errline
  714. * @return bool
  715. */
  716. public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
  717. {
  718. if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) {
  719. if ($this->errorHandler) {
  720. return call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
  721. }
  722. return true;
  723. }
  724. if (!$this->enabled) {
  725. return true;
  726. }
  727. // Figure out error scope from the error.
  728. $scope = 'unknown';
  729. if (stripos($errstr, 'grav') !== false) {
  730. $scope = 'grav';
  731. } elseif (strpos($errfile, '/twig/') !== false) {
  732. $scope = 'twig';
  733. } elseif (stripos($errfile, '/yaml/') !== false) {
  734. $scope = 'yaml';
  735. } elseif (strpos($errfile, '/vendor/') !== false) {
  736. $scope = 'vendor';
  737. }
  738. // Clean up backtrace to make it more useful.
  739. $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
  740. // Skip current call.
  741. array_shift($backtrace);
  742. // Find yaml file where the error happened.
  743. if ($scope === 'yaml') {
  744. foreach ($backtrace as $current) {
  745. if (isset($current['args'])) {
  746. foreach ($current['args'] as $arg) {
  747. if ($arg instanceof SplFileInfo) {
  748. $arg = $arg->getPathname();
  749. }
  750. if (is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
  751. $errfile = $arg;
  752. $errline = 0;
  753. break 2;
  754. }
  755. }
  756. }
  757. }
  758. }
  759. // Filter arguments.
  760. $cut = 0;
  761. $previous = null;
  762. foreach ($backtrace as $i => &$current) {
  763. if (isset($current['args'])) {
  764. $args = [];
  765. foreach ($current['args'] as $arg) {
  766. if (is_string($arg)) {
  767. $arg = "'" . $arg . "'";
  768. if (mb_strlen($arg) > 100) {
  769. $arg = 'string';
  770. }
  771. } elseif (is_bool($arg)) {
  772. $arg = $arg ? 'true' : 'false';
  773. } elseif (is_scalar($arg)) {
  774. $arg = $arg;
  775. } elseif (is_object($arg)) {
  776. $arg = get_class($arg) . ' $object';
  777. } elseif (is_array($arg)) {
  778. $arg = '$array';
  779. } else {
  780. $arg = '$object';
  781. }
  782. $args[] = $arg;
  783. }
  784. $current['args'] = $args;
  785. }
  786. $object = $current['object'] ?? null;
  787. unset($current['object']);
  788. $reflection = null;
  789. if ($object instanceof TemplateWrapper) {
  790. $reflection = new ReflectionObject($object);
  791. $property = $reflection->getProperty('template');
  792. $property->setAccessible(true);
  793. $object = $property->getValue($object);
  794. }
  795. if ($object instanceof Template) {
  796. $file = $current['file'] ?? null;
  797. if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) {
  798. $current = null;
  799. continue;
  800. }
  801. $debugInfo = $object->getDebugInfo();
  802. $line = 1;
  803. if (!$reflection) {
  804. foreach ($debugInfo as $codeLine => $templateLine) {
  805. if ($codeLine <= $current['line']) {
  806. $line = $templateLine;
  807. break;
  808. }
  809. }
  810. }
  811. $src = $object->getSourceContext();
  812. //$code = preg_split('/\r\n|\r|\n/', $src->getCode());
  813. //$current['twig']['twig'] = trim($code[$line - 1]);
  814. $current['twig']['file'] = $src->getPath();
  815. $current['twig']['line'] = $line;
  816. $prevFile = $previous['file'] ?? null;
  817. if ($prevFile && $file === $prevFile) {
  818. $prevLine = $previous['line'];
  819. $line = 1;
  820. foreach ($debugInfo as $codeLine => $templateLine) {
  821. if ($codeLine <= $prevLine) {
  822. $line = $templateLine;
  823. break;
  824. }
  825. }
  826. //$previous['twig']['twig'] = trim($code[$line - 1]);
  827. $previous['twig']['file'] = $src->getPath();
  828. $previous['twig']['line'] = $line;
  829. }
  830. $cut = $i;
  831. } elseif ($object instanceof ProcessorInterface) {
  832. $cut = $cut ?: $i;
  833. break;
  834. }
  835. $previous = &$backtrace[$i];
  836. }
  837. unset($current);
  838. if ($cut) {
  839. $backtrace = array_slice($backtrace, 0, $cut + 1);
  840. }
  841. $backtrace = array_values(array_filter($backtrace));
  842. // Skip vendor libraries and the method where error was triggered.
  843. foreach ($backtrace as $i => $current) {
  844. if (!isset($current['file'])) {
  845. continue;
  846. }
  847. if (strpos($current['file'], '/vendor/') !== false) {
  848. $cut = $i + 1;
  849. continue;
  850. }
  851. if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
  852. $cut = $i + 1;
  853. continue;
  854. }
  855. break;
  856. }
  857. if ($cut) {
  858. $backtrace = array_slice($backtrace, $cut);
  859. }
  860. $backtrace = array_values(array_filter($backtrace));
  861. $current = reset($backtrace);
  862. // If the issue happened inside twig file, change the file and line to match that file.
  863. $file = $current['twig']['file'] ?? '';
  864. if ($file) {
  865. $errfile = $file;
  866. $errline = $current['twig']['line'] ?? 0;
  867. }
  868. $deprecation = [
  869. 'scope' => $scope,
  870. 'message' => $errstr,
  871. 'file' => $errfile,
  872. 'line' => $errline,
  873. 'trace' => $backtrace,
  874. 'count' => 1
  875. ];
  876. $this->deprecations[] = $deprecation;
  877. // Do not pass forward.
  878. return true;
  879. }
  880. /**
  881. * @return array
  882. */
  883. protected function getDeprecations(): array
  884. {
  885. if (!$this->deprecations) {
  886. return [];
  887. }
  888. $list = [];
  889. /** @var array $deprecated */
  890. foreach ($this->deprecations as $deprecated) {
  891. $list[] = $this->getDepracatedMessage($deprecated)[0];
  892. }
  893. return $list;
  894. }
  895. /**
  896. * @return void
  897. * @throws DebugBarException
  898. */
  899. protected function addDeprecations()
  900. {
  901. if (!$this->deprecations) {
  902. return;
  903. }
  904. $collector = new MessagesCollector('deprecated');
  905. $this->addCollector($collector);
  906. $collector->addMessage('Your site is using following deprecated features:');
  907. /** @var array $deprecated */
  908. foreach ($this->deprecations as $deprecated) {
  909. list($message, $scope) = $this->getDepracatedMessage($deprecated);
  910. $collector->addMessage($message, $scope);
  911. }
  912. }
  913. /**
  914. * @param array $deprecated
  915. * @return array
  916. */
  917. protected function getDepracatedMessage($deprecated)
  918. {
  919. $scope = $deprecated['scope'];
  920. $trace = [];
  921. if (isset($deprecated['trace'])) {
  922. foreach ($deprecated['trace'] as $current) {
  923. $class = $current['class'] ?? '';
  924. $type = $current['type'] ?? '';
  925. $function = $this->getFunction($current);
  926. if (isset($current['file'])) {
  927. $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
  928. }
  929. unset($current['class'], $current['type'], $current['function'], $current['args']);
  930. if (isset($current['twig'])) {
  931. $trace[] = $current['twig'];
  932. } else {
  933. $trace[] = ['call' => $class . $type . $function] + $current;
  934. }
  935. }
  936. }
  937. $array = [
  938. 'message' => $deprecated['message'],
  939. 'file' => $deprecated['file'],
  940. 'line' => $deprecated['line'],
  941. 'trace' => $trace
  942. ];
  943. return [
  944. array_filter($array),
  945. $scope
  946. ];
  947. }
  948. /**
  949. * @param array $trace
  950. * @return string
  951. */
  952. protected function getFunction($trace)
  953. {
  954. if (!isset($trace['function'])) {
  955. return '';
  956. }
  957. return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
  958. }
  959. /**
  960. * @param callable $callable
  961. * @return string
  962. */
  963. protected function resolveCallable(callable $callable)
  964. {
  965. if (is_array($callable)) {
  966. return get_class($callable[0]) . '->' . $callable[1] . '()';
  967. }
  968. return 'unknown';
  969. }
  970. }