Debugger.php 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 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(static 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. // TODO: remove when upgrading to Twig 2+
  734. if (str_contains($errstr, '#[\ReturnTypeWillChange]') || str_contains($errstr, 'Passing null to parameter')) {
  735. return true;
  736. }
  737. } elseif (stripos($errfile, '/yaml/') !== false) {
  738. $scope = 'yaml';
  739. } elseif (strpos($errfile, '/vendor/') !== false) {
  740. $scope = 'vendor';
  741. }
  742. // Clean up backtrace to make it more useful.
  743. $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
  744. // Skip current call.
  745. array_shift($backtrace);
  746. // Find yaml file where the error happened.
  747. if ($scope === 'yaml') {
  748. foreach ($backtrace as $current) {
  749. if (isset($current['args'])) {
  750. foreach ($current['args'] as $arg) {
  751. if ($arg instanceof SplFileInfo) {
  752. $arg = $arg->getPathname();
  753. }
  754. if (is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
  755. $errfile = $arg;
  756. $errline = 0;
  757. break 2;
  758. }
  759. }
  760. }
  761. }
  762. }
  763. // Filter arguments.
  764. $cut = 0;
  765. $previous = null;
  766. foreach ($backtrace as $i => &$current) {
  767. if (isset($current['args'])) {
  768. $args = [];
  769. foreach ($current['args'] as $arg) {
  770. if (is_string($arg)) {
  771. $arg = "'" . $arg . "'";
  772. if (mb_strlen($arg) > 100) {
  773. $arg = 'string';
  774. }
  775. } elseif (is_bool($arg)) {
  776. $arg = $arg ? 'true' : 'false';
  777. } elseif (is_scalar($arg)) {
  778. $arg = $arg;
  779. } elseif (is_object($arg)) {
  780. $arg = get_class($arg) . ' $object';
  781. } elseif (is_array($arg)) {
  782. $arg = '$array';
  783. } else {
  784. $arg = '$object';
  785. }
  786. $args[] = $arg;
  787. }
  788. $current['args'] = $args;
  789. }
  790. $object = $current['object'] ?? null;
  791. unset($current['object']);
  792. $reflection = null;
  793. if ($object instanceof TemplateWrapper) {
  794. $reflection = new ReflectionObject($object);
  795. $property = $reflection->getProperty('template');
  796. $property->setAccessible(true);
  797. $object = $property->getValue($object);
  798. }
  799. if ($object instanceof Template) {
  800. $file = $current['file'] ?? null;
  801. if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) {
  802. $current = null;
  803. continue;
  804. }
  805. $debugInfo = $object->getDebugInfo();
  806. $line = 1;
  807. if (!$reflection) {
  808. foreach ($debugInfo as $codeLine => $templateLine) {
  809. if ($codeLine <= $current['line']) {
  810. $line = $templateLine;
  811. break;
  812. }
  813. }
  814. }
  815. $src = $object->getSourceContext();
  816. //$code = preg_split('/\r\n|\r|\n/', $src->getCode());
  817. //$current['twig']['twig'] = trim($code[$line - 1]);
  818. $current['twig']['file'] = $src->getPath();
  819. $current['twig']['line'] = $line;
  820. $prevFile = $previous['file'] ?? null;
  821. if ($prevFile && $file === $prevFile) {
  822. $prevLine = $previous['line'];
  823. $line = 1;
  824. foreach ($debugInfo as $codeLine => $templateLine) {
  825. if ($codeLine <= $prevLine) {
  826. $line = $templateLine;
  827. break;
  828. }
  829. }
  830. //$previous['twig']['twig'] = trim($code[$line - 1]);
  831. $previous['twig']['file'] = $src->getPath();
  832. $previous['twig']['line'] = $line;
  833. }
  834. $cut = $i;
  835. } elseif ($object instanceof ProcessorInterface) {
  836. $cut = $cut ?: $i;
  837. break;
  838. }
  839. $previous = &$backtrace[$i];
  840. }
  841. unset($current);
  842. if ($cut) {
  843. $backtrace = array_slice($backtrace, 0, $cut + 1);
  844. }
  845. $backtrace = array_values(array_filter($backtrace));
  846. // Skip vendor libraries and the method where error was triggered.
  847. foreach ($backtrace as $i => $current) {
  848. if (!isset($current['file'])) {
  849. continue;
  850. }
  851. if (strpos($current['file'], '/vendor/') !== false) {
  852. $cut = $i + 1;
  853. continue;
  854. }
  855. if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
  856. $cut = $i + 1;
  857. continue;
  858. }
  859. break;
  860. }
  861. if ($cut) {
  862. $backtrace = array_slice($backtrace, $cut);
  863. }
  864. $backtrace = array_values(array_filter($backtrace));
  865. $current = reset($backtrace);
  866. // If the issue happened inside twig file, change the file and line to match that file.
  867. $file = $current['twig']['file'] ?? '';
  868. if ($file) {
  869. $errfile = $file;
  870. $errline = $current['twig']['line'] ?? 0;
  871. }
  872. $deprecation = [
  873. 'scope' => $scope,
  874. 'message' => $errstr,
  875. 'file' => $errfile,
  876. 'line' => $errline,
  877. 'trace' => $backtrace,
  878. 'count' => 1
  879. ];
  880. $this->deprecations[] = $deprecation;
  881. // Do not pass forward.
  882. return true;
  883. }
  884. /**
  885. * @return array
  886. */
  887. protected function getDeprecations(): array
  888. {
  889. if (!$this->deprecations) {
  890. return [];
  891. }
  892. $list = [];
  893. /** @var array $deprecated */
  894. foreach ($this->deprecations as $deprecated) {
  895. $list[] = $this->getDepracatedMessage($deprecated)[0];
  896. }
  897. return $list;
  898. }
  899. /**
  900. * @return void
  901. * @throws DebugBarException
  902. */
  903. protected function addDeprecations()
  904. {
  905. if (!$this->deprecations) {
  906. return;
  907. }
  908. $collector = new MessagesCollector('deprecated');
  909. $this->addCollector($collector);
  910. $collector->addMessage('Your site is using following deprecated features:');
  911. /** @var array $deprecated */
  912. foreach ($this->deprecations as $deprecated) {
  913. list($message, $scope) = $this->getDepracatedMessage($deprecated);
  914. $collector->addMessage($message, $scope);
  915. }
  916. }
  917. /**
  918. * @param array $deprecated
  919. * @return array
  920. */
  921. protected function getDepracatedMessage($deprecated)
  922. {
  923. $scope = $deprecated['scope'];
  924. $trace = [];
  925. if (isset($deprecated['trace'])) {
  926. foreach ($deprecated['trace'] as $current) {
  927. $class = $current['class'] ?? '';
  928. $type = $current['type'] ?? '';
  929. $function = $this->getFunction($current);
  930. if (isset($current['file'])) {
  931. $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
  932. }
  933. unset($current['class'], $current['type'], $current['function'], $current['args']);
  934. if (isset($current['twig'])) {
  935. $trace[] = $current['twig'];
  936. } else {
  937. $trace[] = ['call' => $class . $type . $function] + $current;
  938. }
  939. }
  940. }
  941. $array = [
  942. 'message' => $deprecated['message'],
  943. 'file' => $deprecated['file'],
  944. 'line' => $deprecated['line'],
  945. 'trace' => $trace
  946. ];
  947. return [
  948. array_filter($array),
  949. $scope
  950. ];
  951. }
  952. /**
  953. * @param array $trace
  954. * @return string
  955. */
  956. protected function getFunction($trace)
  957. {
  958. if (!isset($trace['function'])) {
  959. return '';
  960. }
  961. return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
  962. }
  963. /**
  964. * @param callable $callable
  965. * @return string
  966. */
  967. protected function resolveCallable(callable $callable)
  968. {
  969. if (is_array($callable)) {
  970. return get_class($callable[0]) . '->' . $callable[1] . '()';
  971. }
  972. return 'unknown';
  973. }
  974. }