[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/system/debug/ -> debug.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  System.Debug
   6   *
   7   * @copyright   (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  use DebugBar\DataCollector\MemoryCollector;
  14  use DebugBar\DataCollector\MessagesCollector;
  15  use DebugBar\DataCollector\RequestDataCollector;
  16  use DebugBar\DebugBar;
  17  use DebugBar\OpenHandler;
  18  use Joomla\Application\ApplicationEvents;
  19  use Joomla\CMS\Application\CMSApplicationInterface;
  20  use Joomla\CMS\Document\HtmlDocument;
  21  use Joomla\CMS\Log\Log;
  22  use Joomla\CMS\Log\LogEntry;
  23  use Joomla\CMS\Plugin\CMSPlugin;
  24  use Joomla\CMS\Profiler\Profiler;
  25  use Joomla\CMS\Session\Session;
  26  use Joomla\CMS\Uri\Uri;
  27  use Joomla\Database\DatabaseDriver;
  28  use Joomla\Database\Event\ConnectionEvent;
  29  use Joomla\Event\DispatcherInterface;
  30  use Joomla\Event\SubscriberInterface;
  31  use Joomla\Plugin\System\Debug\DataCollector\InfoCollector;
  32  use Joomla\Plugin\System\Debug\DataCollector\LanguageErrorsCollector;
  33  use Joomla\Plugin\System\Debug\DataCollector\LanguageFilesCollector;
  34  use Joomla\Plugin\System\Debug\DataCollector\LanguageStringsCollector;
  35  use Joomla\Plugin\System\Debug\DataCollector\ProfileCollector;
  36  use Joomla\Plugin\System\Debug\DataCollector\QueryCollector;
  37  use Joomla\Plugin\System\Debug\DataCollector\SessionCollector;
  38  use Joomla\Plugin\System\Debug\JavascriptRenderer;
  39  use Joomla\Plugin\System\Debug\JoomlaHttpDriver;
  40  use Joomla\Plugin\System\Debug\Storage\FileStorage;
  41  
  42  // phpcs:disable PSR1.Files.SideEffects
  43  \defined('_JEXEC') or die;
  44  // phpcs:enable PSR1.Files.SideEffects
  45  
  46  /**
  47   * Joomla! Debug plugin.
  48   *
  49   * @since  1.5
  50   */
  51  class PlgSystemDebug extends CMSPlugin implements SubscriberInterface
  52  {
  53      /**
  54       * True if debug lang is on.
  55       *
  56       * @var    boolean
  57       * @since  3.0
  58       */
  59      private $debugLang = false;
  60  
  61      /**
  62       * Holds log entries handled by the plugin.
  63       *
  64       * @var    LogEntry[]
  65       * @since  3.1
  66       */
  67      private $logEntries = [];
  68  
  69      /**
  70       * Holds SHOW PROFILES of queries.
  71       *
  72       * @var    array
  73       * @since  3.1.2
  74       */
  75      private $sqlShowProfiles = [];
  76  
  77      /**
  78       * Holds all SHOW PROFILE FOR QUERY n, indexed by n-1.
  79       *
  80       * @var    array
  81       * @since  3.1.2
  82       */
  83      private $sqlShowProfileEach = [];
  84  
  85      /**
  86       * Holds all EXPLAIN EXTENDED for all queries.
  87       *
  88       * @var    array
  89       * @since  3.1.2
  90       */
  91      private $explains = [];
  92  
  93      /**
  94       * Holds total amount of executed queries.
  95       *
  96       * @var    int
  97       * @since  3.2
  98       */
  99      private $totalQueries = 0;
 100  
 101      /**
 102       * Application object.
 103       *
 104       * @var    CMSApplicationInterface
 105       * @since  3.3
 106       */
 107      protected $app;
 108  
 109      /**
 110       * Database object.
 111       *
 112       * @var    DatabaseDriver
 113       * @since  3.8.0
 114       */
 115      protected $db;
 116  
 117      /**
 118       * @var DebugBar
 119       * @since 4.0.0
 120       */
 121      private $debugBar;
 122  
 123      /**
 124       * The query monitor.
 125       *
 126       * @var    \Joomla\Database\Monitor\DebugMonitor
 127       * @since  4.0.0
 128       */
 129      private $queryMonitor;
 130  
 131      /**
 132       * AJAX marker
 133       *
 134       * @var   bool
 135       * @since 4.0.0
 136       */
 137      protected $isAjax = false;
 138  
 139      /**
 140       * Whether displaing a logs is enabled
 141       *
 142       * @var   bool
 143       * @since 4.0.0
 144       */
 145      protected $showLogs = false;
 146  
 147      /**
 148       * Returns an array of events this subscriber will listen to.
 149       *
 150       * @return  array
 151       *
 152       * @since   4.1.3
 153       */
 154      public static function getSubscribedEvents(): array
 155      {
 156          return [
 157              'onBeforeCompileHead' => 'onBeforeCompileHead',
 158              'onAjaxDebug'         => 'onAjaxDebug',
 159              'onBeforeRespond'     => 'onBeforeRespond',
 160              'onAfterRespond'      => 'onAfterRespond',
 161              ApplicationEvents::AFTER_RESPOND => 'onAfterRespond',
 162              'onAfterDisconnect'   => 'onAfterDisconnect',
 163          ];
 164      }
 165  
 166      /**
 167       * Constructor.
 168       *
 169       * @param   DispatcherInterface  &$subject  The object to observe.
 170       * @param   array                $config    An optional associative array of configuration settings.
 171       *
 172       * @since   1.5
 173       */
 174      public function __construct(&$subject, $config)
 175      {
 176          parent::__construct($subject, $config);
 177  
 178          $this->debugLang = $this->app->get('debug_lang');
 179  
 180          // Skip the plugin if debug is off
 181          if (!$this->debugLang && !$this->app->get('debug')) {
 182              return;
 183          }
 184  
 185          $this->app->getConfig()->set('gzip', false);
 186          ob_start();
 187          ob_implicit_flush(false);
 188  
 189          /** @var \Joomla\Database\Monitor\DebugMonitor */
 190          $this->queryMonitor = $this->db->getMonitor();
 191  
 192          if (!$this->params->get('queries', 1)) {
 193              // Remove the database driver monitor
 194              $this->db->setMonitor(null);
 195          }
 196  
 197          $storagePath = JPATH_CACHE . '/plg_system_debug_' . $this->app->getName();
 198  
 199          $this->debugBar = new DebugBar();
 200          $this->debugBar->setStorage(new FileStorage($storagePath));
 201          $this->debugBar->setHttpDriver(new JoomlaHttpDriver($this->app));
 202  
 203          $this->isAjax = $this->app->input->get('option') === 'com_ajax'
 204              && $this->app->input->get('plugin') === 'debug' && $this->app->input->get('group') === 'system';
 205  
 206          $this->showLogs = (bool) $this->params->get('logs', true);
 207  
 208          // Log deprecated class aliases
 209          if ($this->showLogs && $this->app->get('log_deprecated')) {
 210              foreach (JLoader::getDeprecatedAliases() as $deprecation) {
 211                  Log::add(
 212                      sprintf(
 213                          '%1$s has been aliased to %2$s and the former class name is deprecated. The alias will be removed in %3$s.',
 214                          $deprecation['old'],
 215                          $deprecation['new'],
 216                          $deprecation['version']
 217                      ),
 218                      Log::WARNING,
 219                      'deprecation-notes'
 220                  );
 221              }
 222          }
 223      }
 224  
 225      /**
 226       * Add an assets for debugger.
 227       *
 228       * @return  void
 229       *
 230       * @since   4.0.0
 231       */
 232      public function onBeforeCompileHead()
 233      {
 234          // Only if debugging or language debug is enabled.
 235          if ((JDEBUG || $this->debugLang) && $this->isAuthorisedDisplayDebug() && $this->app->getDocument() instanceof HtmlDocument) {
 236              // Use our own jQuery and fontawesome instead of the debug bar shipped version
 237              $assetManager = $this->app->getDocument()->getWebAssetManager();
 238              $assetManager->registerAndUseStyle(
 239                  'plg.system.debug',
 240                  'plg_system_debug/debug.css',
 241                  [],
 242                  [],
 243                  ['fontawesome']
 244              );
 245              $assetManager->registerAndUseScript(
 246                  'plg.system.debug',
 247                  'plg_system_debug/debug.min.js',
 248                  [],
 249                  ['defer' => true],
 250                  ['jquery']
 251              );
 252          }
 253  
 254          // Disable asset media version if needed.
 255          if (JDEBUG && (int) $this->params->get('refresh_assets', 1) === 0) {
 256              $this->app->getDocument()->setMediaVersion(null);
 257          }
 258      }
 259  
 260      /**
 261       * Show the debug info.
 262       *
 263       * @return  void
 264       *
 265       * @since   1.6
 266       */
 267      public function onAfterRespond()
 268      {
 269          // Do not collect data if debugging or language debug is not enabled.
 270          if (!JDEBUG && !$this->debugLang || $this->isAjax) {
 271              return;
 272          }
 273  
 274          // User has to be authorised to see the debug information.
 275          if (!$this->isAuthorisedDisplayDebug()) {
 276              return;
 277          }
 278  
 279          // Load language.
 280          $this->loadLanguage();
 281  
 282          $this->debugBar->addCollector(new InfoCollector($this->params, $this->debugBar->getCurrentRequestId()));
 283  
 284          if (JDEBUG) {
 285              if ($this->params->get('memory', 1)) {
 286                  $this->debugBar->addCollector(new MemoryCollector());
 287              }
 288  
 289              if ($this->params->get('request', 1)) {
 290                  $this->debugBar->addCollector(new RequestDataCollector());
 291              }
 292  
 293              if ($this->params->get('session', 1)) {
 294                  $this->debugBar->addCollector(new SessionCollector($this->params));
 295              }
 296  
 297              if ($this->params->get('profile', 1)) {
 298                  $this->debugBar->addCollector(new ProfileCollector($this->params));
 299              }
 300  
 301              if ($this->params->get('queries', 1)) {
 302                  // Call $db->disconnect() here to trigger the onAfterDisconnect() method here in this class!
 303                  $this->db->disconnect();
 304                  $this->debugBar->addCollector(new QueryCollector($this->params, $this->queryMonitor, $this->sqlShowProfileEach, $this->explains));
 305              }
 306  
 307              if ($this->showLogs) {
 308                  $this->collectLogs();
 309              }
 310          }
 311  
 312          if ($this->debugLang) {
 313              $this->debugBar->addCollector(new LanguageFilesCollector($this->params));
 314              $this->debugBar->addCollector(new LanguageStringsCollector($this->params));
 315              $this->debugBar->addCollector(new LanguageErrorsCollector($this->params));
 316          }
 317  
 318          // Only render for HTML output.
 319          if (!($this->app->getDocument() instanceof HtmlDocument)) {
 320              $this->debugBar->stackData();
 321  
 322              return;
 323          }
 324  
 325          $debugBarRenderer = new JavascriptRenderer($this->debugBar, Uri::root(true) . '/media/vendor/debugbar/');
 326          $openHandlerUrl   = Uri::base(true) . '/index.php?option=com_ajax&plugin=debug&group=system&format=raw&action=openhandler';
 327          $openHandlerUrl  .= '&' . Session::getFormToken() . '=1';
 328  
 329          $debugBarRenderer->setOpenHandlerUrl($openHandlerUrl);
 330  
 331          /**
 332           * @todo disable highlightjs from the DebugBar, import it through NPM
 333           *       and deliver it through Joomla's API
 334           *       Also every DebugBar script and stylesheet needs to use Joomla's API
 335           *       $debugBarRenderer->disableVendor('highlightjs');
 336           */
 337  
 338          // Capture output.
 339          $contents = ob_get_contents();
 340  
 341          if ($contents) {
 342              ob_end_clean();
 343          }
 344  
 345          // No debug for Safari and Chrome redirection.
 346          if (
 347              strpos($contents, '<html><head><meta http-equiv="refresh" content="0;') === 0
 348              && strpos(strtolower($_SERVER['HTTP_USER_AGENT'] ?? ''), 'webkit') !== false
 349          ) {
 350              $this->debugBar->stackData();
 351  
 352              echo $contents;
 353  
 354              return;
 355          }
 356  
 357          echo str_replace('</body>', $debugBarRenderer->renderHead() . $debugBarRenderer->render() . '</body>', $contents);
 358      }
 359  
 360      /**
 361       * AJAX handler
 362       *
 363       * @param Joomla\Event\Event $event
 364       *
 365       * @return  void
 366       *
 367       * @since  4.0.0
 368       */
 369      public function onAjaxDebug($event)
 370      {
 371          // Do not render if debugging or language debug is not enabled.
 372          if (!JDEBUG && !$this->debugLang) {
 373              return;
 374          }
 375  
 376          // User has to be authorised to see the debug information.
 377          if (!$this->isAuthorisedDisplayDebug() || !Session::checkToken('request')) {
 378              return;
 379          }
 380  
 381          switch ($this->app->input->get('action')) {
 382              case 'openhandler':
 383                  $result  = $event['result'] ?: [];
 384                  $handler = new OpenHandler($this->debugBar);
 385  
 386                  $result[] = $handler->handle($this->app->input->request->getArray(), false, false);
 387                  $event['result'] = $result;
 388          }
 389      }
 390  
 391      /**
 392       * Method to check if the current user is allowed to see the debug information or not.
 393       *
 394       * @return  boolean  True if access is allowed.
 395       *
 396       * @since   3.0
 397       */
 398      private function isAuthorisedDisplayDebug(): bool
 399      {
 400          static $result = null;
 401  
 402          if ($result !== null) {
 403              return $result;
 404          }
 405  
 406          // If the user is not allowed to view the output then end here.
 407          $filterGroups = (array) $this->params->get('filter_groups', []);
 408  
 409          if (!empty($filterGroups)) {
 410              $userGroups = $this->app->getIdentity()->get('groups');
 411  
 412              if (!array_intersect($filterGroups, $userGroups)) {
 413                  $result = false;
 414  
 415                  return false;
 416              }
 417          }
 418  
 419          $result = true;
 420  
 421          return true;
 422      }
 423  
 424      /**
 425       * Disconnect handler for database to collect profiling and explain information.
 426       *
 427       * @param   ConnectionEvent  $event  Event object
 428       *
 429       * @return  void
 430       *
 431       * @since   4.0.0
 432       */
 433      public function onAfterDisconnect(ConnectionEvent $event)
 434      {
 435          if (!JDEBUG) {
 436              return;
 437          }
 438  
 439          $db = $event->getDriver();
 440  
 441          // Remove the monitor to avoid monitoring the following queries
 442          $db->setMonitor(null);
 443  
 444          $this->totalQueries = $db->getCount();
 445  
 446          if ($this->params->get('query_profiles') && $db->getServerType() === 'mysql') {
 447              try {
 448                  // Check if profiling is enabled.
 449                  $db->setQuery("SHOW VARIABLES LIKE 'have_profiling'");
 450                  $hasProfiling = $db->loadResult();
 451  
 452                  if ($hasProfiling) {
 453                      // Run a SHOW PROFILE query.
 454                      $db->setQuery('SHOW PROFILES');
 455                      $this->sqlShowProfiles = $db->loadAssocList();
 456  
 457                      if ($this->sqlShowProfiles) {
 458                          foreach ($this->sqlShowProfiles as $qn) {
 459                              // Run SHOW PROFILE FOR QUERY for each query where a profile is available (max 100).
 460                              $db->setQuery('SHOW PROFILE FOR QUERY ' . (int) $qn['Query_ID']);
 461                              $this->sqlShowProfileEach[(int) ($qn['Query_ID'] - 1)] = $db->loadAssocList();
 462                          }
 463                      }
 464                  } else {
 465                      $this->sqlShowProfileEach[0] = [['Error' => 'MySql have_profiling = off']];
 466                  }
 467              } catch (Exception $e) {
 468                  $this->sqlShowProfileEach[0] = [['Error' => $e->getMessage()]];
 469              }
 470          }
 471  
 472          if ($this->params->get('query_explains') && in_array($db->getServerType(), ['mysql', 'postgresql'], true)) {
 473              $logs        = $this->queryMonitor->getLogs();
 474              $boundParams = $this->queryMonitor->getBoundParams();
 475  
 476              foreach ($logs as $k => $query) {
 477                  $dbVersion56 = $db->getServerType() === 'mysql' && version_compare($db->getVersion(), '5.6', '>=');
 478                  $dbVersion80 = $db->getServerType() === 'mysql' && version_compare($db->getVersion(), '8.0', '>=');
 479  
 480                  if ($dbVersion80) {
 481                      $dbVersion56 = false;
 482                  }
 483  
 484                  if ((stripos($query, 'select') === 0) || ($dbVersion56 && ((stripos($query, 'delete') === 0) || (stripos($query, 'update') === 0)))) {
 485                      try {
 486                          $queryInstance = $db->getQuery(true);
 487                          $queryInstance->setQuery('EXPLAIN ' . ($dbVersion56 ? 'EXTENDED ' : '') . $query);
 488  
 489                          if ($boundParams[$k]) {
 490                              foreach ($boundParams[$k] as $key => $obj) {
 491                                  $queryInstance->bind($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions);
 492                              }
 493                          }
 494  
 495                          $this->explains[$k] = $db->setQuery($queryInstance)->loadAssocList();
 496                      } catch (Exception $e) {
 497                          $this->explains[$k] = [['error' => $e->getMessage()]];
 498                      }
 499                  }
 500              }
 501          }
 502      }
 503  
 504      /**
 505       * Store log messages so they can be displayed later.
 506       * This function is passed log entries by JLogLoggerCallback.
 507       *
 508       * @param   LogEntry  $entry  A log entry.
 509       *
 510       * @return  void
 511       *
 512       * @since   3.1
 513       *
 514       * @deprecated  5.0  Use Log::add(LogEntry $entry);
 515       */
 516      public function logger(LogEntry $entry)
 517      {
 518          if (!$this->showLogs) {
 519              return;
 520          }
 521  
 522          $this->logEntries[] = $entry;
 523      }
 524  
 525      /**
 526       * Collect log messages.
 527       *
 528       * @return $this
 529       *
 530       * @since 4.0.0
 531       */
 532      private function collectLogs(): self
 533      {
 534          $loggerOptions = ['group' => 'default'];
 535          $logger        = new Joomla\CMS\Log\Logger\InMemoryLogger($loggerOptions);
 536          $logEntries    = $logger->getCollectedEntries();
 537  
 538          if (!$this->logEntries && !$logEntries) {
 539              return $this;
 540          }
 541  
 542          if ($this->logEntries) {
 543              $logEntries = array_merge($logEntries, $this->logEntries);
 544          }
 545  
 546          $logDeprecated = $this->app->get('log_deprecated', 0);
 547          $logDeprecatedCore = $this->params->get('log-deprecated-core', 0);
 548  
 549          $this->debugBar->addCollector(new MessagesCollector('log'));
 550  
 551          if ($logDeprecated) {
 552              $this->debugBar->addCollector(new MessagesCollector('deprecated'));
 553              $this->debugBar->addCollector(new MessagesCollector('deprecation-notes'));
 554          }
 555  
 556          if ($logDeprecatedCore) {
 557              $this->debugBar->addCollector(new MessagesCollector('deprecated-core'));
 558          }
 559  
 560          foreach ($logEntries as $entry) {
 561              switch ($entry->category) {
 562                  case 'deprecation-notes':
 563                      if ($logDeprecated) {
 564                          $this->debugBar[$entry->category]->addMessage($entry->message);
 565                      }
 566                      break;
 567                  case 'deprecated':
 568                      if (!$logDeprecated && !$logDeprecatedCore) {
 569                          break;
 570                      }
 571  
 572                      $file = '';
 573                      $line = '';
 574  
 575                      // Find the caller, skip Log methods and trigger_error function
 576                      foreach ($entry->callStack as $stackEntry) {
 577                          if (
 578                              !empty($stackEntry['class'])
 579                              && ($stackEntry['class'] === 'Joomla\CMS\Log\LogEntry' || $stackEntry['class'] === 'Joomla\CMS\Log\Log')
 580                          ) {
 581                              continue;
 582                          }
 583  
 584                          if (
 585                              empty($stackEntry['class']) && !empty($stackEntry['function'])
 586                              && $stackEntry['function'] === 'trigger_error'
 587                          ) {
 588                              continue;
 589                          }
 590  
 591                          $file = $stackEntry['file'] ?? '';
 592                          $line = $stackEntry['line'] ?? '';
 593  
 594                          break;
 595                      }
 596  
 597                      $category = $entry->category;
 598                      $relative = $file ? str_replace(JPATH_ROOT, '', $file) : '';
 599  
 600                      if ($relative && 0 === strpos($relative, '/libraries/src')) {
 601                          if (!$logDeprecatedCore) {
 602                              break;
 603                          }
 604  
 605                          $category .= '-core';
 606                      } elseif (!$logDeprecated) {
 607                          break;
 608                      }
 609  
 610                      $message = [
 611                          'message' => $entry->message,
 612                          'caller' => $file . ':' . $line,
 613                          // @todo 'stack' => $entry->callStack;
 614                      ];
 615                      $this->debugBar[$category]->addMessage($message, 'warning');
 616                      break;
 617  
 618                  case 'databasequery':
 619                      // Should be collected by its own collector
 620                      break;
 621  
 622                  default:
 623                      switch ($entry->priority) {
 624                          case Log::EMERGENCY:
 625                          case Log::ALERT:
 626                          case Log::CRITICAL:
 627                          case Log::ERROR:
 628                              $level = 'error';
 629                              break;
 630                          case Log::WARNING:
 631                              $level = 'warning';
 632                              break;
 633                          default:
 634                              $level = 'info';
 635                      }
 636  
 637                      $this->debugBar['log']->addMessage($entry->category . ' - ' . $entry->message, $level);
 638                      break;
 639              }
 640          }
 641  
 642          return $this;
 643      }
 644  
 645      /**
 646       * Add server timing headers when profile is activated.
 647       *
 648       * @return  void
 649       *
 650       * @since   4.1.0
 651       */
 652      public function onBeforeRespond(): void
 653      {
 654          if (!JDEBUG || !$this->params->get('profile', 1)) {
 655              return;
 656          }
 657  
 658          $metrics    = '';
 659          $moduleTime = 0;
 660          $accessTime = 0;
 661  
 662          foreach (Profiler::getInstance('Application')->getMarks() as $index => $mark) {
 663              // Ignore the before mark as the after one contains the timing of the action
 664              if (stripos($mark->label, 'before') !== false) {
 665                  continue;
 666              }
 667  
 668              // Collect the module render time
 669              if (strpos($mark->label, 'mod_') !== false) {
 670                  $moduleTime += $mark->time;
 671                  continue;
 672              }
 673  
 674              // Collect the access render time
 675              if (strpos($mark->label, 'Access:') !== false) {
 676                  $accessTime += $mark->time;
 677                  continue;
 678              }
 679  
 680              $desc     = str_ireplace('after', '', $mark->label);
 681              $name     = preg_replace('/[^\da-z]/i', '', $desc);
 682              $metrics .= sprintf('%s;dur=%f;desc="%s", ', $index . $name, $mark->time, $desc);
 683  
 684              // Do not create too large headers, some web servers don't love them
 685              if (strlen($metrics) > 3000) {
 686                  $metrics .= 'System;dur=0;desc="Data truncated to 3000 characters", ';
 687                  break;
 688              }
 689          }
 690  
 691          // Add the module entry
 692          $metrics .= 'Modules;dur=' . $moduleTime . ';desc="Modules", ';
 693  
 694          // Add the access entry
 695          $metrics .= 'Access;dur=' . $accessTime . ';desc="Access"';
 696  
 697          $this->app->setHeader('Server-Timing', $metrics);
 698      }
 699  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer