123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- <?php
- /**
- * @package Grav\Common
- *
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common;
- use DebugBar\DataCollector\ConfigCollector;
- use DebugBar\DataCollector\DataCollectorInterface;
- use DebugBar\DataCollector\ExceptionsCollector;
- use DebugBar\DataCollector\MemoryCollector;
- use DebugBar\DataCollector\MessagesCollector;
- use DebugBar\DataCollector\PhpInfoCollector;
- use DebugBar\DataCollector\RequestDataCollector;
- use DebugBar\DataCollector\TimeDataCollector;
- use DebugBar\DebugBar;
- use DebugBar\JavascriptRenderer;
- use DebugBar\StandardDebugBar;
- use Grav\Common\Config\Config;
- use Grav\Common\Processors\ProcessorInterface;
- use Twig\Template;
- use Twig\TemplateWrapper;
- class Debugger
- {
- /** @var Grav $grav */
- protected $grav;
- /** @var Config $config */
- protected $config;
- /** @var JavascriptRenderer $renderer */
- protected $renderer;
- /** @var StandardDebugBar $debugbar */
- protected $debugbar;
- /** @var bool */
- protected $enabled;
- protected $initialized = false;
- /** @var array */
- protected $timers = [];
- /** @var array $deprecations */
- protected $deprecations = [];
- /** @var callable */
- protected $errorHandler;
- /**
- * Debugger constructor.
- */
- public function __construct()
- {
- $currentTime = microtime(true);
- if (!\defined('GRAV_REQUEST_TIME')) {
- \define('GRAV_REQUEST_TIME', $currentTime);
- }
- // Enable debugger until $this->init() gets called.
- $this->enabled = true;
- $debugbar = new DebugBar();
- $debugbar->addCollector(new PhpInfoCollector());
- $debugbar->addCollector(new MessagesCollector());
- $debugbar->addCollector(new RequestDataCollector());
- $debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME));
- $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
- $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime);
- $debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true));
- $this->debugbar = $debugbar;
- // Set deprecation collector.
- $this->setErrorHandler();
- }
- /**
- * Initialize the debugger
- *
- * @return $this
- * @throws \DebugBar\DebugBarException
- */
- public function init()
- {
- if ($this->initialized) {
- return $this;
- }
- $this->grav = Grav::instance();
- $this->config = $this->grav['config'];
- // Enable/disable debugger based on configuration.
- $this->enabled = (bool)$this->config->get('system.debugger.enabled');
- if ($this->enabled()) {
- $this->initialized = true;
- $plugins_config = (array)$this->config->get('plugins');
- ksort($plugins_config);
- $debugbar = $this->debugbar;
- $debugbar->addCollector(new MemoryCollector());
- $debugbar->addCollector(new ExceptionsCollector());
- $debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
- $debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
- $this->addMessage('Grav v' . GRAV_VERSION);
- }
- return $this;
- }
- /**
- * Set/get the enabled state of the debugger
- *
- * @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state
- *
- * @return bool
- */
- public function enabled($state = null)
- {
- if ($state !== null) {
- $this->enabled = (bool)$state;
- }
- return $this->enabled;
- }
- /**
- * Add the debugger assets to the Grav Assets
- *
- * @return $this
- */
- public function addAssets()
- {
- if ($this->enabled()) {
- // Only add assets if Page is HTML
- $page = $this->grav['page'];
- if ($page->templateFormat() !== 'html') {
- return $this;
- }
- /** @var Assets $assets */
- $assets = $this->grav['assets'];
- // Add jquery library
- $assets->add('jquery', 101);
- $this->renderer = $this->debugbar->getJavascriptRenderer();
- $this->renderer->setIncludeVendors(false);
- // Get the required CSS files
- list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
- foreach ((array)$css_files as $css) {
- $assets->addCss($css);
- }
- $assets->addCss('/system/assets/debugger.css');
- foreach ((array)$js_files as $js) {
- $assets->addJs($js);
- }
- }
- return $this;
- }
- public function getCaller($limit = 2)
- {
- $trace = debug_backtrace(false, $limit);
- return array_pop($trace);
- }
- /**
- * Adds a data collector
- *
- * @param DataCollectorInterface $collector
- *
- * @return $this
- * @throws \DebugBar\DebugBarException
- */
- public function addCollector($collector)
- {
- $this->debugbar->addCollector($collector);
- return $this;
- }
- /**
- * Returns a data collector
- *
- * @param DataCollectorInterface $collector
- *
- * @return DataCollectorInterface
- * @throws \DebugBar\DebugBarException
- */
- public function getCollector($collector)
- {
- return $this->debugbar->getCollector($collector);
- }
- /**
- * Displays the debug bar
- *
- * @return $this
- */
- public function render()
- {
- if ($this->enabled()) {
- // Only add assets if Page is HTML
- $page = $this->grav['page'];
- if (!$this->renderer || $page->templateFormat() !== 'html') {
- return $this;
- }
- $this->addDeprecations();
- echo $this->renderer->render();
- }
- return $this;
- }
- /**
- * Sends the data through the HTTP headers
- *
- * @return $this
- */
- public function sendDataInHeaders()
- {
- if ($this->enabled()) {
- $this->addDeprecations();
- $this->debugbar->sendDataInHeaders();
- }
- return $this;
- }
- /**
- * Returns collected debugger data.
- *
- * @return array
- */
- public function getData()
- {
- if (!$this->enabled()) {
- return null;
- }
- $this->addDeprecations();
- $this->timers = [];
- return $this->debugbar->getData();
- }
- /**
- * Start a timer with an associated name and description
- *
- * @param string $name
- * @param string|null $description
- *
- * @return $this
- */
- public function startTimer($name, $description = null)
- {
- if (strpos($name, '_') === 0 || $this->enabled()) {
- $this->debugbar['time']->startMeasure($name, $description);
- $this->timers[] = $name;
- }
- return $this;
- }
- /**
- * Stop the named timer
- *
- * @param string $name
- *
- * @return $this
- */
- public function stopTimer($name)
- {
- if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) {
- $this->debugbar['time']->stopMeasure($name);
- }
- return $this;
- }
- /**
- * Dump variables into the Messages tab of the Debug Bar
- *
- * @param mixed $message
- * @param string $label
- * @param bool $isString
- *
- * @return $this
- */
- public function addMessage($message, $label = 'info', $isString = true)
- {
- if ($this->enabled()) {
- $this->debugbar['messages']->addMessage($message, $label, $isString);
- }
- return $this;
- }
- /**
- * Dump exception into the Messages tab of the Debug Bar
- *
- * @param \Exception $e
- * @return Debugger
- */
- public function addException(\Exception $e)
- {
- if ($this->initialized && $this->enabled()) {
- $this->debugbar['exceptions']->addException($e);
- }
- return $this;
- }
- public function setErrorHandler()
- {
- $this->errorHandler = set_error_handler(
- [$this, 'deprecatedErrorHandler']
- );
- }
- /**
- * @param int $errno
- * @param string $errstr
- * @param string $errfile
- * @param int $errline
- * @return bool
- */
- public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
- {
- if ($errno !== E_USER_DEPRECATED) {
- if ($this->errorHandler) {
- return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
- }
- return true;
- }
- if (!$this->enabled()) {
- return true;
- }
- // Figure out error scope from the error.
- $scope = 'unknown';
- if (stripos($errstr, 'grav') !== false) {
- $scope = 'grav';
- } elseif (strpos($errfile, '/twig/') !== false) {
- $scope = 'twig';
- } elseif (stripos($errfile, '/yaml/') !== false) {
- $scope = 'yaml';
- } elseif (strpos($errfile, '/vendor/') !== false) {
- $scope = 'vendor';
- }
- // Clean up backtrace to make it more useful.
- $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
- // Skip current call.
- array_shift($backtrace);
- // Find yaml file where the error happened.
- if ($scope === 'yaml') {
- foreach ($backtrace as $current) {
- if (isset($current['args'])) {
- foreach ($current['args'] as $arg) {
- if ($arg instanceof \SplFileInfo) {
- $arg = $arg->getPathname();
- }
- if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
- $errfile = $arg;
- $errline = 0;
- break 2;
- }
- }
- }
- }
- }
- // Filter arguments.
- $cut = 0;
- $previous = null;
- foreach ($backtrace as $i => &$current) {
- if (isset($current['args'])) {
- $args = [];
- foreach ($current['args'] as $arg) {
- if (\is_string($arg)) {
- $arg = "'" . $arg . "'";
- if (mb_strlen($arg) > 100) {
- $arg = 'string';
- }
- } elseif (\is_bool($arg)) {
- $arg = $arg ? 'true' : 'false';
- } elseif (\is_scalar($arg)) {
- $arg = $arg;
- } elseif (\is_object($arg)) {
- $arg = get_class($arg) . ' $object';
- } elseif (\is_array($arg)) {
- $arg = '$array';
- } else {
- $arg = '$object';
- }
- $args[] = $arg;
- }
- $current['args'] = $args;
- }
- $object = $current['object'] ?? null;
- unset($current['object']);
- $reflection = null;
- if ($object instanceof TemplateWrapper) {
- $reflection = new \ReflectionObject($object);
- $property = $reflection->getProperty('template');
- $property->setAccessible(true);
- $object = $property->getValue($object);
- }
- if ($object instanceof Template) {
- $file = $current['file'] ?? null;
- if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) {
- $current = null;
- continue;
- }
- $debugInfo = $object->getDebugInfo();
- $line = 1;
- if (!$reflection) {
- foreach ($debugInfo as $codeLine => $templateLine) {
- if ($codeLine <= $current['line']) {
- $line = $templateLine;
- break;
- }
- }
- }
- $src = $object->getSourceContext();
- //$code = preg_split('/\r\n|\r|\n/', $src->getCode());
- //$current['twig']['twig'] = trim($code[$line - 1]);
- $current['twig']['file'] = $src->getPath();
- $current['twig']['line'] = $line;
- $prevFile = $previous['file'] ?? null;
- if ($prevFile && $file === $prevFile) {
- $prevLine = $previous['line'];
- $line = 1;
- foreach ($debugInfo as $codeLine => $templateLine) {
- if ($codeLine <= $prevLine) {
- $line = $templateLine;
- break;
- }
- }
- //$previous['twig']['twig'] = trim($code[$line - 1]);
- $previous['twig']['file'] = $src->getPath();
- $previous['twig']['line'] = $line;
- }
- $cut = $i;
- } elseif ($object instanceof ProcessorInterface) {
- $cut = $cut ?: $i;
- break;
- }
- $previous = &$backtrace[$i];
- }
- unset($current);
- if ($cut) {
- $backtrace = array_slice($backtrace, 0, $cut + 1);
- }
- $backtrace = array_values(array_filter($backtrace));
- // Skip vendor libraries and the method where error was triggered.
- foreach ($backtrace as $i => $current) {
- if (!isset($current['file'])) {
- continue;
- }
- if (strpos($current['file'], '/vendor/') !== false) {
- $cut = $i + 1;
- continue;
- }
- if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
- $cut = $i + 1;
- continue;
- }
- break;
- }
- if ($cut) {
- $backtrace = array_slice($backtrace, $cut);
- }
- $backtrace = array_values(array_filter($backtrace));
- $current = reset($backtrace);
- // If the issue happened inside twig file, change the file and line to match that file.
- $file = $current['twig']['file'] ?? '';
- if ($file) {
- $errfile = $file;
- $errline = $current['twig']['line'] ?? 0;
- }
- $deprecation = [
- 'scope' => $scope,
- 'message' => $errstr,
- 'file' => $errfile,
- 'line' => $errline,
- 'trace' => $backtrace,
- 'count' => 1
- ];
- $this->deprecations[] = $deprecation;
- // Do not pass forward.
- return true;
- }
- protected function addDeprecations()
- {
- if (!$this->deprecations) {
- return;
- }
- $collector = new MessagesCollector('deprecated');
- $this->addCollector($collector);
- $collector->addMessage('Your site is using following deprecated features:');
- /** @var array $deprecated */
- foreach ($this->deprecations as $deprecated) {
- list($message, $scope) = $this->getDepracatedMessage($deprecated);
- $collector->addMessage($message, $scope);
- }
- }
- protected function getDepracatedMessage($deprecated)
- {
- $scope = $deprecated['scope'];
- $trace = [];
- if (isset($deprecated['trace'])) {
- foreach ($deprecated['trace'] as $current) {
- $class = $current['class'] ?? '';
- $type = $current['type'] ?? '';
- $function = $this->getFunction($current);
- if (isset($current['file'])) {
- $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
- }
- unset($current['class'], $current['type'], $current['function'], $current['args']);
- if (isset($current['twig'])) {
- $trace[] = $current['twig'];
- } else {
- $trace[] = ['call' => $class . $type . $function] + $current;
- }
- }
- }
- $array = [
- 'message' => $deprecated['message'],
- 'file' => $deprecated['file'],
- 'line' => $deprecated['line'],
- 'trace' => $trace
- ];
- return [
- array_filter($array),
- $scope
- ];
- }
- protected function getFunction($trace)
- {
- if (!isset($trace['function'])) {
- return '';
- }
- return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
- }
- }
|