first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,262 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_debug</name>
<author>Joomla! Project</author>
<creationDate>2006-12</creationDate>
<copyright>(C) 2006 Open Source Matters, Inc.</copyright>
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
<authorEmail>admin@joomla.org</authorEmail>
<authorUrl>www.joomla.org</authorUrl>
<version>3.0.0</version>
<description>PLG_DEBUG_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Debug</namespace>
<files>
<folder plugin="debug">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_debug.ini</language>
<language tag="en-GB">language/en-GB/plg_system_debug.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="refresh_assets"
type="radio"
label="PLG_DEBUG_FIELD_REFRESH_ASSETS_LABEL"
description="PLG_DEBUG_FIELD_REFRESH_ASSETS_DESC"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="filter_groups"
type="usergrouplist"
label="PLG_DEBUG_FIELD_ALLOWED_GROUPS_LABEL"
multiple="true"
layout="joomla.form.field.list-fancy-select"
filter="intarray"
/>
<field
name="memory"
type="radio"
label="PLG_DEBUG_FIELD_MEMORY_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="request"
type="radio"
label="PLG_DEBUG_FIELD_REQUEST_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="session"
type="radio"
label="PLG_DEBUG_FIELD_SESSION_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="profile"
type="radio"
label="PLG_DEBUG_FIELD_PROFILING_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="queries"
type="radio"
label="PLG_DEBUG_FIELD_QUERIES_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="query_traces"
type="radio"
label="PLG_DEBUG_FIELD_QUERY_TRACES_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
showon="queries:1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="query_profiles"
type="radio"
label="PLG_DEBUG_FIELD_QUERY_PROFILES_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
showon="queries:1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="query_explains"
type="radio"
label="PLG_DEBUG_FIELD_QUERY_EXPLAINS_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
showon="queries:1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="track_request_history"
type="radio"
label="PLG_DEBUG_FIELD_TRACK_REQUEST_HISTORY_LABEL"
description="PLG_DEBUG_FIELD_TRACK_REQUEST_HISTORY_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
</fieldset>
<fieldset
name="language"
label="PLG_DEBUG_LANGUAGE_FIELDSET_LABEL"
>
<field
name="language_errorfiles"
type="radio"
label="PLG_DEBUG_FIELD_LANGUAGE_ERRORFILES_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="language_files"
type="radio"
label="PLG_DEBUG_FIELD_LANGUAGE_FILES_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="language_strings"
type="radio"
label="PLG_DEBUG_FIELD_LANGUAGE_STRING_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="strip-first"
type="radio"
label="PLG_DEBUG_FIELD_STRIP_FIRST_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="strip-prefix"
type="textarea"
label="PLG_DEBUG_FIELD_STRIP_PREFIX_LABEL"
description="PLG_DEBUG_FIELD_STRIP_PREFIX_DESC"
cols="30"
rows="4"
/>
<field
name="strip-suffix"
type="textarea"
label="PLG_DEBUG_FIELD_STRIP_SUFFIX_LABEL"
description="PLG_DEBUG_FIELD_STRIP_SUFFIX_DESC"
cols="30"
rows="4"
/>
</fieldset>
<fieldset
name="logging"
label="PLG_DEBUG_LOGGING_FIELDSET_LABEL"
>
<field
name="logs"
type="radio"
label="PLG_DEBUG_FIELD_LOGS_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="log-deprecated-core"
type="radio"
label="PLG_DEBUG_FIELD_LOG_DEPRECATED_CORE_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
showon="logs:1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.debug
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
\defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Debug\Extension\Debug;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.4.0
*/
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
return new Debug(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'debug'),
Factory::getApplication(),
$container->get(DatabaseInterface::class)
);
}
);
}
};

View File

@ -0,0 +1,113 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug;
use DebugBar\DataCollector\DataCollector;
use DebugBar\DataCollector\Renderable;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* AbstractDataCollector
*
* @since 4.0.0
*/
abstract class AbstractDataCollector extends DataCollector implements Renderable
{
/**
* Parameters.
*
* @var Registry
* @since 4.0.0
*/
protected $params;
/**
* The default formatter.
*
* @var DataFormatter
* @since 4.0.0
*/
private static $defaultDataFormatter;
/**
* AbstractDataCollector constructor.
*
* @param Registry $params Parameters.
*
* @since 4.0.0
*/
public function __construct(Registry $params)
{
$this->params = $params;
}
/**
* Get a data formatter.
*
* @since 4.0.0
* @return DataFormatter
*/
public function getDataFormatter(): DataFormatter
{
if ($this->dataFormater === null) {
$this->dataFormater = self::getDefaultDataFormatter();
}
return $this->dataFormater;
}
/**
* Returns the default data formatter
*
* @since 4.0.0
* @return DataFormatter
*/
public static function getDefaultDataFormatter(): DataFormatter
{
if (self::$defaultDataFormatter === null) {
self::$defaultDataFormatter = new DataFormatter();
}
return self::$defaultDataFormatter;
}
/**
* Strip the Joomla! root path.
*
* @param string $path The path.
*
* @return string
*
* @since 4.0.0
*/
public function formatPath($path): string
{
return $this->getDataFormatter()->formatPath($path);
}
/**
* Format a string from back trace.
*
* @param array $call The array to format
*
* @return string
*
* @since 4.0.0
*/
public function formatCallerInfo(array $call): string
{
return $this->getDataFormatter()->formatCallerInfo($call);
}
}

View File

@ -0,0 +1,217 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DataCollector\AssetProvider;
use Joomla\CMS\Application\AdministratorApplication;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
use Joomla\Registry\Registry;
use Psr\Http\Message\ResponseInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* InfoDataCollector
*
* @since 4.0.0
*/
class InfoCollector extends AbstractDataCollector implements AssetProvider
{
/**
* Collector name.
*
* @var string
* @since 4.0.0
*/
private $name = 'info';
/**
* Request ID.
*
* @var string
* @since 4.0.0
*/
private $requestId;
/**
* InfoDataCollector constructor.
*
* @param Registry $params Parameters
* @param string $requestId Request ID
*
* @since 4.0.0
*/
public function __construct(Registry $params, $requestId)
{
$this->requestId = $requestId;
parent::__construct($params);
}
/**
* Returns the unique name of the collector
*
* @since 4.0.0
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @since 4.0.0
* @return array
*/
public function getWidgets(): array
{
return [
'info' => [
'icon' => 'info-circle',
'title' => 'J! Info',
'widget' => 'PhpDebugBar.Widgets.InfoWidget',
'map' => $this->name,
'default' => '{}',
],
];
}
/**
* Returns an array with the following keys:
* - base_path
* - base_url
* - css: an array of filenames
* - js: an array of filenames
*
* @since 4.0.0
* @return array
*/
public function getAssets(): array
{
return [
'js' => Uri::root(true) . '/media/plg_system_debug/widgets/info/widget.min.js',
'css' => Uri::root(true) . '/media/plg_system_debug/widgets/info/widget.min.css',
];
}
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.0.0
*
* @return array Collected data
*/
public function collect(): array
{
/** @type SiteApplication|AdministratorApplication $application */
$application = Factory::getApplication();
$model = $application->bootComponent('com_admin')
->getMVCFactory()->createModel('Sysinfo', 'Administrator');
return [
'phpVersion' => PHP_VERSION,
'joomlaVersion' => JVERSION,
'requestId' => $this->requestId,
'identity' => $this->getIdentityInfo($application->getIdentity()),
'response' => $this->getResponseInfo($application->getResponse()),
'template' => $this->getTemplateInfo($application->getTemplate(true)),
'database' => $this->getDatabaseInfo($model->getInfo()),
];
}
/**
* Get Identity info.
*
* @param User $identity The identity.
*
* @since 4.0.0
*
* @return array
*/
private function getIdentityInfo(User $identity): array
{
if (!$identity->id) {
return ['type' => 'guest'];
}
return [
'type' => 'user',
'id' => $identity->id,
'name' => $identity->name,
'username' => $identity->username,
];
}
/**
* Get response info.
*
* @param ResponseInterface $response The response.
*
* @since 4.0.0
*
* @return array
*/
private function getResponseInfo(ResponseInterface $response): array
{
return [
'status_code' => $response->getStatusCode(),
];
}
/**
* Get template info.
*
* @param object $template The template.
*
* @since 4.0.0
*
* @return array
*/
private function getTemplateInfo($template): array
{
return [
'template' => $template->template ?? '',
'home' => $template->home ?? '',
'id' => $template->id ?? '',
];
}
/**
* Get database info.
*
* @param array $info General information.
*
* @since 4.0.0
*
* @return array
*/
private function getDatabaseInfo(array $info): array
{
return [
'dbserver' => $info['dbserver'] ?? '',
'dbversion' => $info['dbversion'] ?? '',
'dbcollation' => $info['dbcollation'] ?? '',
'dbconnectioncollation' => $info['dbconnectioncollation'] ?? '',
'dbconnectionencryption' => $info['dbconnectionencryption'] ?? '',
'dbconnencryptsupported' => $info['dbconnencryptsupported'] ?? '',
];
}
}

View File

@ -0,0 +1,153 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DataCollector\AssetProvider;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* LanguageErrorsDataCollector
*
* @since 4.0.0
*/
class LanguageErrorsCollector extends AbstractDataCollector implements AssetProvider
{
/**
* Collector name.
*
* @var string
* @since 4.0.0
*/
private $name = 'languageErrors';
/**
* The count.
*
* @var integer
* @since 4.0.0
*/
private $count = 0;
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.0.0
*
* @return array Collected data
*/
public function collect(): array
{
return [
'data' => [
'files' => $this->getData(),
'jroot' => JPATH_ROOT,
'xdebugLink' => $this->getXdebugLinkTemplate(),
],
'count' => $this->getCount(),
];
}
/**
* Returns the unique name of the collector
*
* @since 4.0.0
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @since 4.0.0
*
* @return array
*/
public function getWidgets(): array
{
return [
'errors' => [
'icon' => 'warning',
'widget' => 'PhpDebugBar.Widgets.languageErrorsWidget',
'map' => $this->name . '.data',
'default' => '',
],
'errors:badge' => [
'map' => $this->name . '.count',
'default' => 'null',
],
];
}
/**
* Returns an array with the following keys:
* - base_path
* - base_url
* - css: an array of filenames
* - js: an array of filenames
*
* @since 4.0.0
* @return array
*/
public function getAssets()
{
return [
'js' => Uri::root(true) . '/media/plg_system_debug/widgets/languageErrors/widget.min.js',
'css' => Uri::root(true) . '/media/plg_system_debug/widgets/languageErrors/widget.min.css',
];
}
/**
* Collect data.
*
* @return array
*
* @since 4.0.0
*/
private function getData(): array
{
$errorFiles = Factory::getLanguage()->getErrorFiles();
$errors = [];
if (\count($errorFiles)) {
foreach ($errorFiles as $file => $lines) {
foreach ($lines as $line) {
$errors[] = [$file, $line];
$this->count++;
}
}
}
return $errors;
}
/**
* Get a count value.
*
* @return int
*
* @since 4.0.0
*/
private function getCount(): int
{
return $this->count;
}
}

View File

@ -0,0 +1,130 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DataCollector\AssetProvider;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* LanguageFilesDataCollector
*
* @since 4.0.0
*/
class LanguageFilesCollector extends AbstractDataCollector implements AssetProvider
{
/**
* Collector name.
*
* @var string
* @since 4.0.0
*/
private $name = 'languageFiles';
/**
* The count.
*
* @var integer
* @since 4.0.0
*/
private $count = 0;
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.0.0
*
* @return array Collected data
*/
public function collect(): array
{
$paths = Factory::getLanguage()->getPaths();
$loaded = [];
foreach ($paths as $extension => $files) {
$loaded[$extension] = [];
foreach ($files as $file => $status) {
$loaded[$extension][$file] = $status;
if ($status) {
$this->count++;
}
}
}
return [
'loaded' => $loaded,
'xdebugLink' => $this->getXdebugLinkTemplate(),
'jroot' => JPATH_ROOT,
'count' => $this->count,
];
}
/**
* Returns the unique name of the collector
*
* @since 4.0.0
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @since 4.0.0
*
* @return array
*/
public function getWidgets(): array
{
return [
'loaded' => [
'icon' => 'language',
'widget' => 'PhpDebugBar.Widgets.languageFilesWidget',
'map' => $this->name,
'default' => '[]',
],
'loaded:badge' => [
'map' => $this->name . '.count',
'default' => 'null',
],
];
}
/**
* Returns an array with the following keys:
* - base_path
* - base_url
* - css: an array of filenames
* - js: an array of filenames
*
* @since 4.0.0
* @return array
*/
public function getAssets(): array
{
return [
'js' => Uri::root(true) . '/media/plg_system_debug/widgets/languageFiles/widget.min.js',
'css' => Uri::root(true) . '/media/plg_system_debug/widgets/languageFiles/widget.min.css',
];
}
}

View File

@ -0,0 +1,189 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DataCollector\AssetProvider;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Uri\Uri;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* LanguageStringsDataCollector
*
* @since 4.0.0
*/
class LanguageStringsCollector extends AbstractDataCollector implements AssetProvider
{
/**
* Collector name.
*
* @var string
* @since 4.0.0
*/
private $name = 'languageStrings';
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.0.0
*
* @return array Collected data
*/
public function collect(): array
{
return [
'data' => $this->getData(),
'count' => $this->getCount(),
];
}
/**
* Returns the unique name of the collector
*
* @since 4.0.0
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @since 4.0.0
*
* @return array
*/
public function getWidgets(): array
{
return [
'untranslated' => [
'icon' => 'question-circle',
'widget' => 'PhpDebugBar.Widgets.languageStringsWidget',
'map' => $this->name . '.data',
'default' => '',
],
'untranslated:badge' => [
'map' => $this->name . '.count',
'default' => 'null',
],
];
}
/**
* Returns an array with the following keys:
* - base_path
* - base_url
* - css: an array of filenames
* - js: an array of filenames
*
* @since 4.0.0
* @return array
*/
public function getAssets(): array
{
return [
'js' => Uri::root(true) . '/media/plg_system_debug/widgets/languageStrings/widget.min.js',
'css' => Uri::root(true) . '/media/plg_system_debug/widgets/languageStrings/widget.min.css',
];
}
/**
* Collect data.
*
* @return array
*
* @since 4.0.0
*/
private function getData(): array
{
$orphans = Factory::getLanguage()->getOrphans();
$data = [];
foreach ($orphans as $orphan => $occurrences) {
$data[$orphan] = [];
foreach ($occurrences as $occurrence) {
$item = [];
$item['string'] = $occurrence['string'] ?? 'n/a';
$item['trace'] = [];
$item['caller'] = '';
if (isset($occurrence['trace'])) {
$cnt = 0;
$trace = [];
$callerLocation = '';
array_shift($occurrence['trace']);
foreach ($occurrence['trace'] as $i => $stack) {
$class = $stack['class'] ?? '';
$file = $stack['file'] ?? '';
$line = $stack['line'] ?? '';
$caller = $this->formatCallerInfo($stack);
$location = $file && $line ? "$file:$line" : 'same';
$isCaller = 0;
if (!$callerLocation && $class !== Language::class && !strpos($file, 'Text.php')) {
$callerLocation = $location;
$isCaller = 1;
}
$trace[] = [
\count($occurrence['trace']) - $cnt,
$isCaller,
$caller,
$file,
$line,
];
$cnt++;
}
$item['trace'] = $trace;
$item['caller'] = $callerLocation;
}
$data[$orphan][] = $item;
}
}
return [
'orphans' => $data,
'jroot' => JPATH_ROOT,
'xdebugLink' => $this->getXdebugLinkTemplate(),
];
}
/**
* Get a count value.
*
* @return integer
*
* @since 4.0.0
*/
private function getCount(): int
{
return \count(Factory::getLanguage()->getOrphans());
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Collects info about the request duration as well as providing
* a way to log duration of any operations
*
* @since 4.4.0
*/
class MemoryCollector extends AbstractDataCollector
{
/**
* @var boolean
* @since 4.4.0
*/
protected $realUsage = false;
/**
* @var float
* @since 4.4.0
*/
protected $peakUsage = 0;
/**
* @param Registry $params Parameters.
* @param float $peakUsage
* @param boolean $realUsage
*
* @since 4.4.0
*/
public function __construct(Registry $params, $peakUsage = null, $realUsage = null)
{
parent::__construct($params);
if ($peakUsage !== null) {
$this->peakUsage = $peakUsage;
}
if ($realUsage !== null) {
$this->realUsage = $realUsage;
}
}
/**
* Returns whether total allocated memory page size is used instead of actual used memory size
* by the application. See $real_usage parameter on memory_get_peak_usage for details.
*
* @return boolean
*
* @since 4.4.0
*/
public function getRealUsage()
{
return $this->realUsage;
}
/**
* Sets whether total allocated memory page size is used instead of actual used memory size
* by the application. See $real_usage parameter on memory_get_peak_usage for details.
*
* @param boolean $realUsage
*
* @since 4.4.0
*/
public function setRealUsage($realUsage)
{
$this->realUsage = $realUsage;
}
/**
* Returns the peak memory usage
*
* @return integer
*
* @since 4.4.0
*/
public function getPeakUsage()
{
return $this->peakUsage;
}
/**
* Updates the peak memory usage value
*
* @since 4.4.0
*/
public function updatePeakUsage()
{
if ($this->peakUsage === null) {
$this->peakUsage = memory_get_peak_usage($this->realUsage);
}
}
/**
* @return array
*
* @since 4.4.0
*/
public function collect()
{
$this->updatePeakUsage();
return [
'peak_usage' => $this->peakUsage,
'peak_usage_str' => $this->getDataFormatter()->formatBytes($this->peakUsage, 3),
];
}
/**
* @return string
*
* @since 4.4.0
*/
public function getName()
{
return 'memory';
}
/**
* @return array
*
* @since 4.4.0
*/
public function getWidgets()
{
return [
'memory' => [
'icon' => 'cogs',
'tooltip' => 'Memory Usage',
'map' => 'memory.peak_usage_str',
'default' => "'0B'",
],
];
}
}

View File

@ -0,0 +1,342 @@
<?php
/**
* This file is part of the DebugBar package.
*
* @copyright (c) 2013 Maxime Bouroumeau-Fuseau
* @license For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DebugBarException;
use Joomla\CMS\Profiler\Profiler;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Collects info about the request duration as well as providing
* a way to log duration of any operations
*
* @since version
*/
class ProfileCollector extends AbstractDataCollector
{
/**
* Request start time.
*
* @var float
* @since 4.0.0
*/
protected $requestStartTime;
/**
* Request end time.
*
* @var float
* @since 4.0.0
*/
protected $requestEndTime;
/**
* Started measures.
*
* @var array
* @since 4.0.0
*/
protected $startedMeasures = [];
/**
* Measures.
*
* @var array
* @since 4.0.0
*/
protected $measures = [];
/**
* Constructor.
*
* @param Registry $params Parameters.
*
* @since 4.0.0
*/
public function __construct(Registry $params)
{
if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
$this->requestStartTime = $_SERVER['REQUEST_TIME_FLOAT'];
} else {
$this->requestStartTime = microtime(true);
}
parent::__construct($params);
}
/**
* Starts a measure.
*
* @param string $name Internal name, used to stop the measure
* @param string|null $label Public name
* @param string|null $collector The source of the collector
*
* @return void
*
* @since 4.0.0
*/
public function startMeasure($name, $label = null, $collector = null)
{
$start = microtime(true);
$this->startedMeasures[$name] = [
'label' => $label ?: $name,
'start' => $start,
'collector' => $collector,
];
}
/**
* Check a measure exists
*
* @param string $name Group name.
*
* @return bool
*
* @since 4.0.0
*/
public function hasStartedMeasure($name): bool
{
return isset($this->startedMeasures[$name]);
}
/**
* Stops a measure.
*
* @param string $name Measurement name.
* @param array $params Parameters
*
* @return void
*
* @since 4.0.0
*
* @throws DebugBarException
*/
public function stopMeasure($name, array $params = [])
{
$end = microtime(true);
if (!$this->hasStartedMeasure($name)) {
throw new DebugBarException("Failed stopping measure '$name' because it hasn't been started");
}
$this->addMeasure($this->startedMeasures[$name]['label'], $this->startedMeasures[$name]['start'], $end, $params, $this->startedMeasures[$name]['collector']);
unset($this->startedMeasures[$name]);
}
/**
* Adds a measure
*
* @param string $label A label.
* @param float $start Start of request.
* @param float $end End of request.
* @param array $params Parameters.
* @param string|null $collector A collector.
*
* @return void
*
* @since 4.0.0
*/
public function addMeasure($label, $start, $end, array $params = [], $collector = null)
{
$this->measures[] = [
'label' => $label,
'start' => $start,
'relative_start' => $start - $this->requestStartTime,
'end' => $end,
'relative_end' => $end - $this->requestEndTime,
'duration' => $end - $start,
'duration_str' => $this->getDataFormatter()->formatDuration($end - $start),
'params' => $params,
'collector' => $collector,
];
}
/**
* Utility function to measure the execution of a Closure
*
* @param string $label A label.
* @param \Closure $closure A closure.
* @param string|null $collector A collector.
*
* @return void
*
* @since 4.0.0
*/
public function measure($label, \Closure $closure, $collector = null)
{
$name = spl_object_hash($closure);
$this->startMeasure($name, $label, $collector);
$result = $closure();
$params = \is_array($result) ? $result : [];
$this->stopMeasure($name, $params);
}
/**
* Returns an array of all measures
*
* @return array
*
* @since 4.0.0
*/
public function getMeasures(): array
{
return $this->measures;
}
/**
* Returns the request start time
*
* @return float
*
* @since 4.0.0
*/
public function getRequestStartTime(): float
{
return $this->requestStartTime;
}
/**
* Returns the request end time
*
* @return float
*
* @since 4.0.0
*/
public function getRequestEndTime(): float
{
return $this->requestEndTime;
}
/**
* Returns the duration of a request
*
* @return float
*
* @since 4.0.0
*/
public function getRequestDuration(): float
{
if ($this->requestEndTime !== null) {
return $this->requestEndTime - $this->requestStartTime;
}
return microtime(true) - $this->requestStartTime;
}
/**
* Sets request end time.
*
* @param float $time Request end time.
*
* @return $this
*
* @since 4.4.0
*/
public function setRequestEndTime($time): self
{
$this->requestEndTime = $time;
return $this;
}
/**
* Called by the DebugBar when data needs to be collected
*
* @return array Collected data
*
* @since 4.0.0
*/
public function collect(): array
{
$this->requestEndTime = $this->requestEndTime ?? microtime(true);
$start = $this->requestStartTime;
$marks = Profiler::getInstance('Application')->getMarks();
foreach ($marks as $mark) {
$mem = $this->getDataFormatter()->formatBytes(abs($mark->memory) * 1048576);
$label = $mark->label . " ($mem)";
$end = $start + $mark->time / 1000;
$this->addMeasure($label, $start, $end);
$start = $end;
}
foreach (array_keys($this->startedMeasures) as $name) {
$this->stopMeasure($name);
}
usort(
$this->measures,
function ($a, $b) {
if ($a['start'] === $b['start']) {
return 0;
}
return $a['start'] < $b['start'] ? -1 : 1;
}
);
return [
'start' => $this->requestStartTime,
'end' => $this->requestEndTime,
'duration' => $this->getRequestDuration(),
'duration_str' => $this->getDataFormatter()->formatDuration($this->getRequestDuration()),
'measures' => array_values($this->measures),
'rawMarks' => $marks,
];
}
/**
* Returns the unique name of the collector
*
* @return string
*
* @since 4.0.0
*/
public function getName(): string
{
return 'profile';
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @return array
*
* @since 4.0.0
*/
public function getWidgets(): array
{
return [
'profileTime' => [
'icon' => 'clock-o',
'tooltip' => 'Request Duration',
'map' => 'profile.duration_str',
'default' => "'0ms'",
],
'profile' => [
'icon' => 'clock-o',
'widget' => 'PhpDebugBar.Widgets.TimelineWidget',
'map' => 'profile',
'default' => '{}',
],
];
}
}

View File

@ -0,0 +1,258 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DataCollector\AssetProvider;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\Monitor\DebugMonitor;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* QueryDataCollector
*
* @since 4.0.0
*/
class QueryCollector extends AbstractDataCollector implements AssetProvider
{
/**
* Collector name.
*
* @var string
* @since 4.0.0
*/
private $name = 'queries';
/**
* The query monitor.
*
* @var DebugMonitor
* @since 4.0.0
*/
private $queryMonitor;
/**
* Profile data.
*
* @var array
* @since 4.0.0
*/
private $profiles;
/**
* Explain data.
*
* @var array
* @since 4.0.0
*/
private $explains;
/**
* Accumulated Duration.
*
* @var integer
* @since 4.0.0
*/
private $accumulatedDuration = 0;
/**
* Accumulated Memory.
*
* @var integer
* @since 4.0.0
*/
private $accumulatedMemory = 0;
/**
* Constructor.
*
* @param Registry $params Parameters.
* @param DebugMonitor $queryMonitor Query monitor.
* @param array $profiles Profile data.
* @param array $explains Explain data
*
* @since 4.0.0
*/
public function __construct(Registry $params, DebugMonitor $queryMonitor, array $profiles, array $explains)
{
$this->queryMonitor = $queryMonitor;
parent::__construct($params);
$this->profiles = $profiles;
$this->explains = $explains;
}
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.0.0
*
* @return array Collected data
*/
public function collect(): array
{
$statements = $this->getStatements();
return [
'data' => [
'statements' => $statements,
'nb_statements' => \count($statements),
'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($this->accumulatedDuration),
'memory_usage_str' => $this->getDataFormatter()->formatBytes($this->accumulatedMemory),
'xdebug_link' => $this->getXdebugLinkTemplate(),
'root_path' => JPATH_ROOT,
],
'count' => \count($this->queryMonitor->getLogs()),
];
}
/**
* Returns the unique name of the collector
*
* @since 4.0.0
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @since 4.0.0
*
* @return array
*/
public function getWidgets(): array
{
return [
'queries' => [
'icon' => 'database',
'widget' => 'PhpDebugBar.Widgets.SQLQueriesWidget',
'map' => $this->name . '.data',
'default' => '[]',
],
'queries:badge' => [
'map' => $this->name . '.count',
'default' => 'null',
],
];
}
/**
* Assets for the collector.
*
* @since 4.0.0
*
* @return array
*/
public function getAssets(): array
{
return [
'css' => Uri::root(true) . '/media/plg_system_debug/widgets/sqlqueries/widget.min.css',
'js' => Uri::root(true) . '/media/plg_system_debug/widgets/sqlqueries/widget.min.js',
];
}
/**
* Prepare the executed statements data.
*
* @since 4.0.0
*
* @return array
*/
private function getStatements(): array
{
$statements = [];
$logs = $this->queryMonitor->getLogs();
$boundParams = $this->queryMonitor->getBoundParams();
$timings = $this->queryMonitor->getTimings();
$memoryLogs = $this->queryMonitor->getMemoryLogs();
$stacks = $this->queryMonitor->getCallStacks();
$collectStacks = $this->params->get('query_traces');
foreach ($logs as $id => $item) {
$queryTime = 0;
$queryMemory = 0;
if ($timings && isset($timings[$id * 2 + 1])) {
// Compute the query time.
$queryTime = ($timings[$id * 2 + 1] - $timings[$id * 2]);
$this->accumulatedDuration += $queryTime;
}
if ($memoryLogs && isset($memoryLogs[$id * 2 + 1])) {
// Compute the query memory usage.
$queryMemory = ($memoryLogs[$id * 2 + 1] - $memoryLogs[$id * 2]);
$this->accumulatedMemory += $queryMemory;
}
$trace = [];
$callerLocation = '';
if (isset($stacks[$id])) {
$cnt = 0;
foreach ($stacks[$id] as $i => $stack) {
$class = $stack['class'] ?? '';
$file = $stack['file'] ?? '';
$line = $stack['line'] ?? '';
$caller = $this->formatCallerInfo($stack);
$location = $file && $line ? "$file:$line" : 'same';
$isCaller = 0;
if (\Joomla\Database\DatabaseDriver::class === $class && false === strpos($file, 'DatabaseDriver.php')) {
$callerLocation = $location;
$isCaller = 1;
}
if ($collectStacks) {
$trace[] = [\count($stacks[$id]) - $cnt, $isCaller, $caller, $file, $line];
}
$cnt++;
}
}
$explain = $this->explains[$id] ?? [];
$explainColumns = [];
// Extract column labels for Explain table
if ($explain) {
$explainColumns = array_keys(reset($explain));
}
$statements[] = [
'sql' => $item,
'params' => $boundParams[$id] ?? [],
'duration_str' => $this->getDataFormatter()->formatDuration($queryTime),
'memory_str' => $this->getDataFormatter()->formatBytes($queryMemory),
'caller' => $callerLocation,
'callstack' => $trace,
'explain' => $explain,
'explain_col' => $explainColumns,
'profile' => $this->profiles[$id] ?? [],
];
}
return $statements;
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use Joomla\Plugin\System\Debug\Extension\Debug;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Collects info about the request content while redacting potentially secret content
*
* @since 4.2.4
*/
class RequestDataCollector extends \DebugBar\DataCollector\RequestDataCollector
{
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.2.4
*
* @return array
*/
public function collect()
{
$vars = ['_GET', '_POST', '_SESSION', '_COOKIE', '_SERVER'];
$returnData = [];
foreach ($vars as $var) {
if (isset($GLOBALS[$var])) {
$key = "$" . $var;
$data = $GLOBALS[$var];
// Replace Joomla session data from session data, it will be collected by SessionCollector
if ($var === '_SESSION' && !empty($data['joomla'])) {
$data['joomla'] = '***redacted***';
}
array_walk_recursive($data, static function (&$value, $key) {
if (!preg_match(Debug::PROTECTED_COLLECTOR_KEYS, $key)) {
return;
}
$value = '***redacted***';
});
if ($this->isHtmlVarDumperUsed()) {
$returnData[$key] = $this->getVarDumper()->renderVar($data);
} else {
$returnData[$key] = $this->getDataFormatter()->formatVar($data);
}
}
}
return $returnData;
}
}

View File

@ -0,0 +1,125 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use Joomla\CMS\Factory;
use Joomla\Plugin\System\Debug\AbstractDataCollector;
use Joomla\Plugin\System\Debug\Extension\Debug;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* SessionDataCollector
*
* @since 4.0.0
*/
class SessionCollector extends AbstractDataCollector
{
/**
* Collector name.
*
* @var string
* @since 4.0.0
*/
private $name = 'session';
/**
* Collected data.
*
* @var array
* @since 4.4.0
*/
protected $sessionData;
/**
* Constructor.
*
* @param Registry $params Parameters.
* @param bool $collect Collect the session data.
*
* @since 4.4.0
*/
public function __construct($params, $collect = false)
{
parent::__construct($params);
if ($collect) {
$this->collect();
}
}
/**
* Called by the DebugBar when data needs to be collected
*
* @param bool $overwrite Overwrite the previously collected session data.
*
* @return array Collected data
*
* @since 4.0.0
*/
public function collect($overwrite = false)
{
if ($this->sessionData === null || $overwrite) {
$this->sessionData = [];
$data = Factory::getApplication()->getSession()->all();
// redact value of potentially secret keys
array_walk_recursive($data, static function (&$value, $key) {
if (!preg_match(Debug::PROTECTED_COLLECTOR_KEYS, $key)) {
return;
}
$value = '***redacted***';
});
foreach ($data as $key => $value) {
$this->sessionData[$key] = $this->getDataFormatter()->formatVar($value);
}
}
return ['data' => $this->sessionData];
}
/**
* Returns the unique name of the collector
*
* @since 4.0.0
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Returns a hash where keys are control names and their values
* an array of options as defined in {@see \DebugBar\JavascriptRenderer::addControl()}
*
* @since 4.0.0
*
* @return array
*/
public function getWidgets()
{
return [
'session' => [
'icon' => 'key',
'widget' => 'PhpDebugBar.Widgets.VariableListWidget',
'map' => $this->name . '.data',
'default' => '[]',
],
];
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\DataCollector;
use DebugBar\DataCollector\DataCollectorInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\User\UserFactoryInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User collector that stores the user id of the person making the request allowing us to filter on it after storage
*
* @since 4.2.4
*/
class UserCollector implements DataCollectorInterface
{
/**
* Collector name.
*
* @var string
* @since 4.2.4
*/
private $name = 'juser';
/**
* Called by the DebugBar when data needs to be collected
*
* @since 4.2.4
*
* @return array Collected data
*/
public function collect()
{
$user = Factory::getApplication()->getIdentity()
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
return ['user_id' => $user->id];
}
/**
* Returns the unique name of the collector
*
* @since 4.2.4
*
* @return string
*/
public function getName()
{
return $this->name;
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug;
use DebugBar\DataFormatter\DataFormatter as DebugBarDataFormatter;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* DataFormatter
*
* @since 4.0.0
*/
class DataFormatter extends DebugBarDataFormatter
{
/**
* Strip the root path.
*
* @param string $path The path.
* @param string $replacement The replacement
*
* @return string
*
* @since 4.0.0
*/
public function formatPath($path, $replacement = ''): string
{
return str_replace(JPATH_ROOT, $replacement, $path);
}
/**
* Format a string from back trace.
*
* @param array $call The array to format
*
* @return string
*
* @since 4.0.0
*/
public function formatCallerInfo(array $call): string
{
$string = '';
if (isset($call['class'])) {
// If entry has Class/Method print it.
$string .= htmlspecialchars($call['class'] . $call['type'] . $call['function']) . '()';
} elseif (isset($call['args'][0]) && \is_array($call['args'][0])) {
$string .= htmlspecialchars($call['function']) . ' (';
foreach ($call['args'][0] as $arg) {
// Check if the arguments can be used as string
if (\is_object($arg) && !method_exists($arg, '__toString')) {
$arg = \get_class($arg);
}
// Keep only the size of array
if (\is_array($arg)) {
$arg = 'Array(count=' . \count($arg) . ')';
}
$string .= htmlspecialchars($arg) . ', ';
}
$string = rtrim($string, ', ') . ')';
} elseif (isset($call['args'][0])) {
$string .= htmlspecialchars($call['function']) . '(';
if (is_scalar($call['args'][0])) {
$string .= $call['args'][0];
} elseif (\is_object($call['args'][0])) {
$string .= \get_class($call['args'][0]);
} else {
$string .= \gettype($call['args'][0]);
}
$string .= ')';
} else {
// It's a function.
$string .= htmlspecialchars($call['function']) . '()';
}
return $string;
}
}

View File

@ -0,0 +1,712 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.debug
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\Extension;
use DebugBar\DataCollector\MessagesCollector;
use DebugBar\DebugBar;
use DebugBar\OpenHandler;
use Joomla\Application\ApplicationEvents;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Event\Plugin\AjaxEvent;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Log\LogEntry;
use Joomla\CMS\Log\Logger\InMemoryLogger;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Profiler\Profiler;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\Event\ConnectionEvent;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Joomla\Event\Priority;
use Joomla\Event\SubscriberInterface;
use Joomla\Plugin\System\Debug\DataCollector\InfoCollector;
use Joomla\Plugin\System\Debug\DataCollector\LanguageErrorsCollector;
use Joomla\Plugin\System\Debug\DataCollector\LanguageFilesCollector;
use Joomla\Plugin\System\Debug\DataCollector\LanguageStringsCollector;
use Joomla\Plugin\System\Debug\DataCollector\MemoryCollector;
use Joomla\Plugin\System\Debug\DataCollector\ProfileCollector;
use Joomla\Plugin\System\Debug\DataCollector\QueryCollector;
use Joomla\Plugin\System\Debug\DataCollector\RequestDataCollector;
use Joomla\Plugin\System\Debug\DataCollector\SessionCollector;
use Joomla\Plugin\System\Debug\DataCollector\UserCollector;
use Joomla\Plugin\System\Debug\JavascriptRenderer;
use Joomla\Plugin\System\Debug\JoomlaHttpDriver;
use Joomla\Plugin\System\Debug\Storage\FileStorage;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Debug plugin.
*
* @since 1.5
*/
final class Debug extends CMSPlugin implements SubscriberInterface
{
use DatabaseAwareTrait;
/**
* List of protected keys that will be redacted in multiple data collected
*
* @since 4.2.4
*/
public const PROTECTED_COLLECTOR_KEYS = "/password|passwd|pwd|secret|token|server_auth|_pass|smtppass|otpKey|otep/i";
/**
* True if debug lang is on.
*
* @var boolean
* @since 3.0
*/
private $debugLang;
/**
* Holds log entries handled by the plugin.
*
* @var LogEntry[]
* @since 3.1
*/
private $logEntries = [];
/**
* Holds all SHOW PROFILE FOR QUERY n, indexed by n-1.
*
* @var array
* @since 3.1.2
*/
private $sqlShowProfileEach = [];
/**
* Holds all EXPLAIN EXTENDED for all queries.
*
* @var array
* @since 3.1.2
*/
private $explains = [];
/**
* @var DebugBar
* @since 4.0.0
*/
private $debugBar;
/**
* The query monitor.
*
* @var \Joomla\Database\Monitor\DebugMonitor
* @since 4.0.0
*/
private $queryMonitor;
/**
* AJAX marker
*
* @var bool
* @since 4.0.0
*/
protected $isAjax = false;
/**
* Whether displaying a logs is enabled
*
* @var bool
* @since 4.0.0
*/
protected $showLogs = false;
/**
* The time spent in onAfterDisconnect()
*
* @var float
* @since 4.4.0
*/
protected $timeInOnAfterDisconnect = 0;
/**
* @return array
*
* @since 4.1.3
*/
public static function getSubscribedEvents(): array
{
return [
'onBeforeCompileHead' => 'onBeforeCompileHead',
'onAjaxDebug' => 'onAjaxDebug',
'onBeforeRespond' => 'onBeforeRespond',
'onAfterRespond' => [
'onAfterRespond',
Priority::MIN,
],
ApplicationEvents::AFTER_RESPOND => [
'onAfterRespond',
Priority::MIN,
],
'onAfterDisconnect' => 'onAfterDisconnect',
];
}
/**
* @param DispatcherInterface $dispatcher The object to observe -- event dispatcher.
* @param array $config An optional associative array of configuration settings.
* @param CMSApplicationInterface $app The app
* @param DatabaseInterface $db The db
*
* @since 1.5
*/
public function __construct(DispatcherInterface $dispatcher, array $config, CMSApplicationInterface $app, DatabaseInterface $db)
{
parent::__construct($dispatcher, $config);
$this->setApplication($app);
$this->setDatabase($db);
$this->debugLang = $this->getApplication()->get('debug_lang');
// Skip the plugin if debug is off
if (!$this->debugLang && !$this->getApplication()->get('debug')) {
return;
}
$this->getApplication()->set('gzip', false);
ob_start();
ob_implicit_flush(false);
/** @var \Joomla\Database\Monitor\DebugMonitor */
$this->queryMonitor = $this->getDatabase()->getMonitor();
if (!$this->params->get('queries', 1)) {
// Remove the database driver monitor
$this->getDatabase()->setMonitor(null);
}
$this->debugBar = new DebugBar();
// Check whether we want to track the request history for future use.
if ($this->params->get('track_request_history', false)) {
$storagePath = JPATH_CACHE . '/plg_system_debug_' . $this->getApplication()->getName();
$this->debugBar->setStorage(new FileStorage($storagePath));
}
$this->debugBar->setHttpDriver(new JoomlaHttpDriver($this->getApplication()));
$this->isAjax = $this->getApplication()->getInput()->get('option') === 'com_ajax'
&& $this->getApplication()->getInput()->get('plugin') === 'debug' && $this->getApplication()->getInput()->get('group') === 'system';
$this->showLogs = (bool) $this->params->get('logs', true);
// Log deprecated class aliases
if ($this->showLogs && $this->getApplication()->get('log_deprecated')) {
foreach (\JLoader::getDeprecatedAliases() as $deprecation) {
Log::add(
sprintf(
'%1$s has been aliased to %2$s and the former class name is deprecated. The alias will be removed in %3$s.',
$deprecation['old'],
$deprecation['new'],
$deprecation['version']
),
Log::WARNING,
'deprecation-notes'
);
}
}
}
/**
* Add an assets for debugger.
*
* @return void
*
* @since 4.0.0
*/
public function onBeforeCompileHead()
{
// Only if debugging or language debug is enabled.
if ((JDEBUG || $this->debugLang) && $this->isAuthorisedDisplayDebug() && $this->getApplication()->getDocument() instanceof HtmlDocument) {
// Use our own jQuery and fontawesome instead of the debug bar shipped version
$assetManager = $this->getApplication()->getDocument()->getWebAssetManager();
$assetManager->registerAndUseStyle(
'plg.system.debug',
'plg_system_debug/debug.css',
[],
[],
['fontawesome']
);
$assetManager->registerAndUseScript(
'plg.system.debug',
'plg_system_debug/debug.min.js',
[],
['defer' => true],
['jquery']
);
}
// Disable asset media version if needed.
if (JDEBUG && (int) $this->params->get('refresh_assets', 1) === 0) {
$this->getApplication()->getDocument()->setMediaVersion('');
}
}
/**
* Show the debug info.
*
* @return void
*
* @since 1.6
*/
public function onAfterRespond()
{
$endTime = microtime(true) - $this->timeInOnAfterDisconnect;
$endMemory = memory_get_peak_usage(false);
// Do not collect data if debugging or language debug is not enabled.
if ((!JDEBUG && !$this->debugLang) || $this->isAjax) {
return;
}
// User has to be authorised to see the debug information.
if (!$this->isAuthorisedDisplayDebug()) {
return;
}
// Load language.
$this->loadLanguage();
$this->debugBar->addCollector(new InfoCollector($this->params, $this->debugBar->getCurrentRequestId()));
$this->debugBar->addCollector(new UserCollector());
if (JDEBUG) {
if ($this->params->get('memory', 1)) {
$this->debugBar->addCollector(new MemoryCollector($this->params, $endMemory));
}
if ($this->params->get('request', 1)) {
$this->debugBar->addCollector(new RequestDataCollector());
}
if ($this->params->get('session', 1)) {
$this->debugBar->addCollector(new SessionCollector($this->params, true));
}
if ($this->params->get('profile', 1)) {
$this->debugBar->addCollector((new ProfileCollector($this->params))->setRequestEndTime($endTime));
}
if ($this->params->get('queries', 1)) {
// Remember session form token for possible future usage.
$formToken = Session::getFormToken();
// Close session to collect possible session-related queries.
$this->getApplication()->getSession()->close();
// Call $db->disconnect() here to trigger the onAfterDisconnect() method here in this class!
$this->getDatabase()->disconnect();
$this->debugBar->addCollector(new QueryCollector($this->params, $this->queryMonitor, $this->sqlShowProfileEach, $this->explains));
}
if ($this->showLogs) {
$this->collectLogs();
}
}
if ($this->debugLang) {
$this->debugBar->addCollector(new LanguageFilesCollector($this->params));
$this->debugBar->addCollector(new LanguageStringsCollector($this->params));
$this->debugBar->addCollector(new LanguageErrorsCollector($this->params));
}
// Only render for HTML output.
if (!($this->getApplication()->getDocument() instanceof HtmlDocument)) {
$this->debugBar->stackData();
return;
}
$debugBarRenderer = new JavascriptRenderer($this->debugBar, Uri::root(true) . '/media/vendor/debugbar/');
$openHandlerUrl = Uri::base(true) . '/index.php?option=com_ajax&plugin=debug&group=system&format=raw&action=openhandler';
$openHandlerUrl .= '&' . ($formToken ?? Session::getFormToken()) . '=1';
$debugBarRenderer->setOpenHandlerUrl($openHandlerUrl);
/**
* @todo disable highlightjs from the DebugBar, import it through NPM
* and deliver it through Joomla's API
* Also every DebugBar script and stylesheet needs to use Joomla's API
* $debugBarRenderer->disableVendor('highlightjs');
*/
// Capture output.
$contents = ob_get_contents();
if ($contents) {
ob_end_clean();
}
// No debug for Safari and Chrome redirection.
if (
strpos($contents, '<html><head><meta http-equiv="refresh" content="0;') === 0
&& strpos(strtolower($_SERVER['HTTP_USER_AGENT'] ?? ''), 'webkit') !== false
) {
$this->debugBar->stackData();
echo $contents;
return;
}
echo str_replace('</body>', $debugBarRenderer->renderHead() . $debugBarRenderer->render() . '</body>', $contents);
}
/**
* AJAX handler
*
* @param AjaxEvent $event
*
* @return void
*
* @since 4.0.0
*/
public function onAjaxDebug(AjaxEvent $event)
{
// Do not render if debugging or language debug is not enabled.
if (!JDEBUG && !$this->debugLang) {
return;
}
// User has to be authorised to see the debug information.
if (!$this->isAuthorisedDisplayDebug() || !Session::checkToken('request')) {
return;
}
switch ($this->getApplication()->getInput()->get('action')) {
case 'openhandler':
$handler = new OpenHandler($this->debugBar);
$result = $handler->handle($this->getApplication()->getInput()->request->getArray(), false, false);
$event->addResult($result);
}
}
/**
* Method to check if the current user is allowed to see the debug information or not.
*
* @return boolean True if access is allowed.
*
* @since 3.0
*/
private function isAuthorisedDisplayDebug(): bool
{
static $result;
if ($result !== null) {
return $result;
}
// If the user is not allowed to view the output then end here.
$filterGroups = (array) $this->params->get('filter_groups', []);
if (!empty($filterGroups)) {
$userGroups = $this->getApplication()->getIdentity()->get('groups');
if (!array_intersect($filterGroups, $userGroups)) {
$result = false;
return false;
}
}
$result = true;
return true;
}
/**
* Disconnect handler for database to collect profiling and explain information.
*
* @param ConnectionEvent $event Event object
*
* @return void
*
* @since 4.0.0
*/
public function onAfterDisconnect(ConnectionEvent $event)
{
if (!JDEBUG) {
return;
}
$startTime = microtime(true);
$db = $event->getDriver();
// Remove the monitor to avoid monitoring the following queries
$db->setMonitor(null);
if ($this->params->get('query_profiles') && $db->getServerType() === 'mysql') {
try {
// Check if profiling is enabled.
$db->setQuery("SHOW VARIABLES LIKE 'have_profiling'");
$hasProfiling = $db->loadResult();
if ($hasProfiling) {
// Run a SHOW PROFILE query.
$db->setQuery('SHOW PROFILES');
$sqlShowProfiles = $db->loadAssocList();
if ($sqlShowProfiles) {
foreach ($sqlShowProfiles as $qn) {
// Run SHOW PROFILE FOR QUERY for each query where a profile is available (max 100).
$db->setQuery('SHOW PROFILE FOR QUERY ' . (int) $qn['Query_ID']);
$this->sqlShowProfileEach[$qn['Query_ID'] - 1] = $db->loadAssocList();
}
}
} else {
$this->sqlShowProfileEach[0] = [['Error' => 'MySql have_profiling = off']];
}
} catch (\Exception $e) {
$this->sqlShowProfileEach[0] = [['Error' => $e->getMessage()]];
}
}
if ($this->params->get('query_explains') && \in_array($db->getServerType(), ['mysql', 'postgresql'], true)) {
$logs = $this->queryMonitor->getLogs();
$boundParams = $this->queryMonitor->getBoundParams();
foreach ($logs as $k => $query) {
$dbVersion56 = $db->getServerType() === 'mysql' && version_compare($db->getVersion(), '5.6', '>=');
$dbVersion80 = $db->getServerType() === 'mysql' && version_compare($db->getVersion(), '8.0', '>=');
if ($dbVersion80) {
$dbVersion56 = false;
}
if ((stripos($query, 'select') === 0) || ($dbVersion56 && ((stripos($query, 'delete') === 0) || (stripos($query, 'update') === 0)))) {
try {
$queryInstance = $db->getQuery(true);
$queryInstance->setQuery('EXPLAIN ' . ($dbVersion56 ? 'EXTENDED ' : '') . $query);
if ($boundParams[$k]) {
foreach ($boundParams[$k] as $key => $obj) {
$queryInstance->bind($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions);
}
}
$this->explains[$k] = $db->setQuery($queryInstance)->loadAssocList();
} catch (\Exception $e) {
$this->explains[$k] = [['error' => $e->getMessage()]];
}
}
}
}
$this->timeInOnAfterDisconnect = microtime(true) - $startTime;
}
/**
* Store log messages so they can be displayed later.
* This function is passed log entries by JLogLoggerCallback.
*
* @param LogEntry $entry A log entry.
*
* @return void
*
* @since 3.1
*
* @deprecated 4.3 will be removed in 6.0
* Use \Joomla\CMS\Log\Log::add(LogEntry $entry) instead
*/
public function logger(LogEntry $entry)
{
if (!$this->showLogs) {
return;
}
$this->logEntries[] = $entry;
}
/**
* Collect log messages.
*
* @return void
*
* @since 4.0.0
*/
private function collectLogs()
{
$loggerOptions = ['group' => 'default'];
$logger = new InMemoryLogger($loggerOptions);
$logEntries = $logger->getCollectedEntries();
if (!$this->logEntries && !$logEntries) {
return;
}
if ($this->logEntries) {
$logEntries = array_merge($logEntries, $this->logEntries);
}
$logDeprecated = $this->getApplication()->get('log_deprecated', 0);
$logDeprecatedCore = $this->params->get('log-deprecated-core', 0);
$this->debugBar->addCollector(new MessagesCollector('log'));
if ($logDeprecated) {
$this->debugBar->addCollector(new MessagesCollector('deprecated'));
$this->debugBar->addCollector(new MessagesCollector('deprecation-notes'));
}
if ($logDeprecatedCore) {
$this->debugBar->addCollector(new MessagesCollector('deprecated-core'));
}
foreach ($logEntries as $entry) {
switch ($entry->category) {
case 'deprecation-notes':
if ($logDeprecated) {
$this->debugBar[$entry->category]->addMessage($entry->message);
}
break;
case 'deprecated':
if (!$logDeprecated && !$logDeprecatedCore) {
break;
}
$file = '';
$line = '';
// Find the caller, skip Log methods and trigger_error function
foreach ($entry->callStack as $stackEntry) {
if (
!empty($stackEntry['class'])
&& ($stackEntry['class'] === 'Joomla\CMS\Log\LogEntry' || $stackEntry['class'] === 'Joomla\CMS\Log\Log')
) {
continue;
}
if (
empty($stackEntry['class']) && !empty($stackEntry['function'])
&& $stackEntry['function'] === 'trigger_error'
) {
continue;
}
$file = $stackEntry['file'] ?? '';
$line = $stackEntry['line'] ?? '';
break;
}
$category = $entry->category;
$relative = $file ? str_replace(JPATH_ROOT, '', $file) : '';
if ($relative && 0 === strpos($relative, '/libraries/src')) {
if (!$logDeprecatedCore) {
break;
}
$category .= '-core';
} elseif (!$logDeprecated) {
break;
}
$message = [
'message' => $entry->message,
'caller' => $file . ':' . $line,
// @todo 'stack' => $entry->callStack;
];
$this->debugBar[$category]->addMessage($message, 'warning');
break;
case 'databasequery':
// Should be collected by its own collector
break;
default:
switch ($entry->priority) {
case Log::EMERGENCY:
case Log::ALERT:
case Log::CRITICAL:
case Log::ERROR:
$level = 'error';
break;
case Log::WARNING:
$level = 'warning';
break;
default:
$level = 'info';
}
$this->debugBar['log']->addMessage($entry->category . ' - ' . $entry->message, $level);
break;
}
}
}
/**
* Add server timing headers when profile is activated.
*
* @return void
*
* @since 4.1.0
*/
public function onBeforeRespond(): void
{
if (!JDEBUG || !$this->params->get('profile', 1)) {
return;
}
$metrics = '';
$moduleTime = 0;
$accessTime = 0;
foreach (Profiler::getInstance('Application')->getMarks() as $index => $mark) {
// Ignore the before mark as the after one contains the timing of the action
if (stripos($mark->label, 'before') !== false) {
continue;
}
// Collect the module render time
if (strpos($mark->label, 'mod_') !== false) {
$moduleTime += $mark->time;
continue;
}
// Collect the access render time
if (strpos($mark->label, 'Access:') !== false) {
$accessTime += $mark->time;
continue;
}
$desc = str_ireplace('after', '', $mark->label);
$name = preg_replace('/[^\da-z]/i', '', $desc);
$metrics .= sprintf('%s;dur=%f;desc="%s", ', $index . $name, $mark->time, $desc);
// Do not create too large headers, some web servers don't love them
if (\strlen($metrics) > 3000) {
$metrics .= 'System;dur=0;desc="Data truncated to 3000 characters", ';
break;
}
}
// Add the module entry
$metrics .= 'Modules;dur=' . $moduleTime . ';desc="Modules", ';
// Add the access entry
$metrics .= 'Access;dur=' . $accessTime . ';desc="Access"';
$this->getApplication()->setHeader('Server-Timing', $metrics);
}
}

View File

@ -0,0 +1,133 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug;
use DebugBar\DebugBar;
use DebugBar\JavascriptRenderer as DebugBarJavascriptRenderer;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Custom JavascriptRenderer for DebugBar
*
* @since 4.0.0
*/
class JavascriptRenderer extends DebugBarJavascriptRenderer
{
/**
* Class constructor.
*
* @param \DebugBar\DebugBar $debugBar DebugBar instance
* @param string $baseUrl The base URL from which assets will be served
* @param string $basePath The path which assets are relative to
*
* @since 4.0.0
*/
public function __construct(DebugBar $debugBar, $baseUrl = null, $basePath = null)
{
parent::__construct($debugBar, $baseUrl, $basePath);
// Disable features that loaded by Joomla! API, or not in use
$this->setEnableJqueryNoConflict(false);
$this->disableVendor('jquery');
$this->disableVendor('fontawesome');
}
/**
* Renders the html to include needed assets
*
* Only useful if Assetic is not used
*
* @return string
*
* @since 4.0.0
*/
public function renderHead()
{
list($cssFiles, $jsFiles, $inlineCss, $inlineJs, $inlineHead) = $this->getAssets(null, self::RELATIVE_URL);
$html = '';
$doc = Factory::getApplication()->getDocument();
foreach ($cssFiles as $file) {
$html .= sprintf('<link rel="stylesheet" type="text/css" href="%s">' . "\n", $file);
}
foreach ($inlineCss as $content) {
$html .= sprintf('<style>%s</style>' . "\n", $content);
}
foreach ($jsFiles as $file) {
$html .= sprintf('<script type="text/javascript" src="%s" defer></script>' . "\n", $file);
}
$nonce = '';
if ($doc->cspNonce) {
$nonce = ' nonce="' . $doc->cspNonce . '"';
}
foreach ($inlineJs as $content) {
$html .= sprintf('<script type="module"%s>%s</script>' . "\n", $nonce, $content);
}
foreach ($inlineHead as $content) {
$html .= $content . "\n";
}
return $html;
}
/**
* Returns the code needed to display the debug bar
*
* AJAX request should not render the initialization code.
*
* @param boolean $initialize Whether or not to render the debug bar initialization code
* @param boolean $renderStackedData Whether or not to render the stacked data
*
* @return string
*
* @since 4.0.0
*/
public function render($initialize = true, $renderStackedData = true)
{
$js = '';
$doc = Factory::getApplication()->getDocument();
if ($initialize) {
$js = $this->getJsInitializationCode();
}
if ($renderStackedData && $this->debugBar->hasStackedData()) {
foreach ($this->debugBar->getStackedData() as $id => $data) {
$js .= $this->getAddDatasetCode($id, $data, '(stacked)');
}
}
$suffix = !$initialize ? '(ajax)' : null;
$js .= $this->getAddDatasetCode($this->debugBar->getCurrentRequestId(), $this->debugBar->getData(), $suffix);
$nonce = '';
if ($doc->cspNonce) {
$nonce = ' nonce="' . $doc->cspNonce . '"';
}
if ($this->useRequireJs) {
return "<script type=\"module\"$nonce>\nrequire(['debugbar'], function(PhpDebugBar){ $js });\n</script>\n";
}
return "<script type=\"module\"$nonce>\n$js\n</script>\n";
}
}

View File

@ -0,0 +1,134 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug;
use DebugBar\HttpDriverInterface;
use Joomla\Application\WebApplicationInterface;
use Joomla\CMS\Application\CMSApplicationInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla HTTP driver for DebugBar
*
* @since 4.1.5
*/
final class JoomlaHttpDriver implements HttpDriverInterface
{
/**
* @var CMSApplicationInterface
*
* @since 4.1.5
*/
private $app;
/**
* @var array
*
* @since 4.1.5
*/
private $dummySession = [];
/**
* Constructor.
*
* @param CMSApplicationInterface $app
*
* @since 4.1.5
*/
public function __construct(CMSApplicationInterface $app)
{
$this->app = $app;
}
/**
* Sets HTTP headers
*
* @param array $headers
*
* @since 4.1.5
*/
public function setHeaders(array $headers)
{
if ($this->app instanceof WebApplicationInterface) {
foreach ($headers as $name => $value) {
$this->app->setHeader($name, $value, true);
}
}
}
/**
* Checks if the session is started
*
* @return boolean
*
* @since 4.1.5
*/
public function isSessionStarted()
{
return true;
}
/**
* Sets a value in the session
*
* @param string $name
* @param string $value
*
* @since 4.1.5
*/
public function setSessionValue($name, $value)
{
$this->dummySession[$name] = $value;
}
/**
* Checks if a value is in the session
*
* @param string $name
*
* @return boolean
*
* @since 4.1.5
*/
public function hasSessionValue($name)
{
return \array_key_exists($name, $this->dummySession);
}
/**
* Returns a value from the session
*
* @param string $name
*
* @return mixed
*
* @since 4.1.5
*/
public function getSessionValue($name)
{
return $this->dummySession[$name] ?? null;
}
/**
* Deletes a value from the session
*
* @param string $name
*
* @since 4.1.5
*/
public function deleteSessionValue($name)
{
unset($this->dummySession[$name]);
}
}

View File

@ -0,0 +1,192 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.Debug
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Debug\Storage;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Filesystem\File;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Stores collected data into files
*
* @since 4.0.0
*/
class FileStorage extends \DebugBar\Storage\FileStorage
{
/**
* Saves collected data
*
* @param string $id The log id
* @param string $data The log data
*
* @return void
*
* @since 4.0.0
*/
public function save($id, $data)
{
if (!file_exists($this->dirname)) {
Folder::create($this->dirname);
}
$dataStr = '<?php die(); ?>#(^-^)#' . json_encode($data);
File::write($this->makeFilename($id), $dataStr);
}
/**
* Returns collected data with the specified id
*
* @param string $id The log id
*
* @return array
*
* @since 4.0.0
*/
public function get($id)
{
$dataStr = file_get_contents($this->makeFilename($id));
$dataStr = str_replace('<?php die(); ?>#(^-^)#', '', $dataStr);
return json_decode($dataStr, true) ?: [];
}
/**
* Returns a metadata about collected data
*
* @param array $filters Filtering options
* @param integer $max The limit, items per page
* @param integer $offset The offset
*
* @return array
*
* @since 4.0.0
*/
public function find(array $filters = [], $max = 20, $offset = 0)
{
// Loop through all .php files and remember the modified time and id.
$files = [];
foreach (new \DirectoryIterator($this->dirname) as $file) {
if ($file->getExtension() == 'php') {
$files[] = [
'time' => $file->getMTime(),
'id' => $file->getBasename('.php'),
];
}
}
// Sort the files, newest first
usort(
$files,
function ($a, $b) {
if ($a['time'] === $b['time']) {
return 0;
}
return $a['time'] < $b['time'] ? 1 : -1;
}
);
// Load the metadata and filter the results.
$results = [];
$i = 0;
foreach ($files as $file) {
// When filter is empty, skip loading the offset
if ($i++ < $offset && empty($filters)) {
$results[] = null;
continue;
}
$data = $this->get($file['id']);
if (!$this->isSecureToReturnData($data)) {
continue;
}
$meta = $data['__meta'];
unset($data);
if ($this->filter($meta, $filters)) {
$results[] = $meta;
}
if (\count($results) >= ($max + $offset)) {
break;
}
}
return \array_slice($results, $offset, $max);
}
/**
* Get a full path to the file
*
* @param string $id The log id
*
* @return string
*
* @since 4.0.0
*/
public function makeFilename($id)
{
return $this->dirname . basename($id) . '.php';
}
/**
* Check if the user is allowed to view the request. Users can only see their own requests.
*
* @param array $data The data item to process
*
* @return boolean
*
* @since 4.2.4
*/
private function isSecureToReturnData($data): bool
{
/**
* We only started this collector in Joomla 4.2.4 - any older files we have to assume are insecure.
*/
if (!\array_key_exists('juser', $data)) {
return false;
}
$currentUser = Factory::getUser();
$currentUserId = $currentUser->id;
$currentUserSuperAdmin = $currentUser->authorise('core.admin');
/**
* Guests aren't allowed to look at other requests because there's no guarantee it's the same guest. Potentially
* in the future this could be refined to check the session ID to show some requests. But it's unlikely we want
* guests to be using the debug bar anyhow
*/
if ($currentUserId === 0) {
return false;
}
/** @var \Joomla\CMS\User\User $user */
$user = Factory::getContainer()->get(UserFactoryInterface::class)
->loadUserById($data['juser']['user_id']);
// Super users are allowed to look at other users requests. Otherwise users can only see their own requests.
if ($currentUserSuperAdmin || $user->id === $currentUserId) {
return true;
}
return false;
}
}