[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |