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,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_accessibility</name>
<author>Joomla! Project</author>
<creationDate>2020-02-15</creationDate>
<copyright>(C) 2020 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>4.0.0</version>
<description>PLG_SYSTEM_ACCESSIBILITY_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Accessibility</namespace>
<files>
<folder plugin="accessibility">services</folder>
<folder>src</folder>
</files>
<languages folder="admin">
<language tag="en-GB">language/en-GB/plg_system_accessibility.ini</language>
<language tag="en-GB">language/en-GB/plg_system_accessibility.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="section"
type="list"
label="PLG_SYSTEM_ACCESSIBILITY_SECTION"
default="administrator"
validate="options"
>
<option value="site">PLG_SYSTEM_ACCESSIBILITY_SECTION_SITE</option>
<option value="administrator">PLG_SYSTEM_ACCESSIBILITY_SECTION_ADMIN</option>
<option value="both">PLG_SYSTEM_ACCESSIBILITY_SECTION_BOTH</option>
</field>
<field
name="useEmojis"
type="list"
label="PLG_SYSTEM_ACCESSIBILITY_EMOJIS"
default="true"
validate="options"
>
<option value="true">PLG_SYSTEM_ACCESSIBILITY_EMOJIS_TRUE</option>
<option value="false">PLG_SYSTEM_ACCESSIBILITY_EMOJIS_FALSE</option>
</field>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.accessibility
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Accessibility\Extension\Accessibility;
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) {
$plugin = new Accessibility(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'accessibility')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,114 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.accessibility
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Accessibility\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* System plugin to add additional accessibility features to the administrator interface.
*
* @since 4.0.0
*/
final class Accessibility extends CMSPlugin
{
/**
* Add the javascript for the accessibility menu
*
* @return void
*
* @since 4.0.0
*/
public function onBeforeCompileHead()
{
$section = $this->params->get('section', 'administrator');
if ($section !== 'both' && $this->getApplication()->isClient($section) !== true) {
return;
}
// Get the document object.
$document = $this->getApplication()->getDocument();
if ($document->getType() !== 'html') {
return;
}
// Are we in a modal?
if ($this->getApplication()->getInput()->get('tmpl', '', 'cmd') === 'component') {
return;
}
// Load language file.
$this->loadLanguage();
// Determine if it is an LTR or RTL language
$direction = $this->getApplication()->getLanguage()->isRtl() ? 'right' : 'left';
// Detect the current active language
$lang = $this->getApplication()->getLanguage()->getTag();
/**
* Add strings for translations in Javascript.
* Reference https://ranbuch.github.io/accessibility/
*/
$document->addScriptOptions(
'accessibility-options',
[
'labels' => [
'menuTitle' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_MENU_TITLE'),
'increaseText' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_INCREASE_TEXT'),
'decreaseText' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_DECREASE_TEXT'),
'increaseTextSpacing' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_INCREASE_SPACING'),
'decreaseTextSpacing' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_DECREASE_SPACING'),
'invertColors' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_INVERT_COLORS'),
'grayHues' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_GREY'),
'underlineLinks' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_UNDERLINE'),
'bigCursor' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_CURSOR'),
'readingGuide' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_READING'),
'textToSpeech' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_TTS'),
'speechToText' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_STT'),
'resetTitle' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_RESET'),
'closeTitle' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_ACCESSIBILITY_CLOSE'),
],
'icon' => [
'position' => [
$direction => [
'size' => '0',
'units' => 'px',
],
],
'useEmojis' => $this->params->get('useEmojis', 'true') === 'true',
],
'hotkeys' => [
'enabled' => true,
'helpTitles' => true,
],
'textToSpeechLang' => [$lang],
'speechToTextLang' => [$lang],
]
);
$document->getWebAssetManager()
->useScript('accessibility')
->addInlineScript(
'window.addEventListener("load", function() {'
. 'new Accessibility(Joomla.getOptions("accessibility-options") || {});'
. '});',
['name' => 'inline.plg.system.accessibility'],
['type' => 'module'],
['accessibility']
);
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_actionlogs</name>
<author>Joomla! Project</author>
<creationDate>2018-05</creationDate>
<copyright>(C) 2018 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.9.0</version>
<description>PLG_SYSTEM_ACTIONLOGS_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\ActionLogs</namespace>
<files>
<folder>forms</folder>
<folder plugin="actionlogs">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_actionlogs.ini</language>
<language tag="en-GB">language/en-GB/plg_system_actionlogs.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset name="actionlogs" label="PLG_SYSTEM_ACTIONLOGS_OPTIONS" addfieldprefix="Joomla\Component\Actionlogs\Administrator\Field">
<fields name="actionlogs">
<field
name="actionlogsNotify"
type="radio"
label="PLG_SYSTEM_ACTIONLOGS_NOTIFICATIONS"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
required="true"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="actionlogsExtensions"
type="logtype"
label="PLG_SYSTEM_ACTIONLOGS_EXTENSIONS_NOTIFICATIONS"
multiple="true"
validate="options"
layout="joomla.form.field.list-fancy-select"
showon="actionlogsNotify:1"
default="com_content"
/>
</fields>
</fieldset>
</form>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="params">
<fieldset name="information" label="PLG_SYSTEM_ACTIONLOGS_OPTIONS">
<field addfieldprefix="Joomla\Component\Actionlogs\Administrator\Field"
name="Information"
type="plugininfo"
label="PLG_SYSTEM_ACTIONLOGS_INFO_LABEL"
description="PLG_SYSTEM_ACTIONLOGS_INFO_DESC"
/>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,50 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.actionlogs
*
* @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\CMS\User\UserFactoryInterface;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\ActionLogs\Extension\ActionLogs;
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) {
$plugin = new ActionLogs(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'actionlogs')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
$plugin->setUserFactory($container->get(UserFactoryInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,350 @@
<?php
/**
* @package Joomla.Plugins
* @subpackage System.actionlogs
*
* @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\ActionLogs\Extension;
use Joomla\CMS\Form\Form;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Actionlogs\Administrator\Helper\ActionlogsHelper;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\ParameterType;
use Joomla\Event\DispatcherInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Users Actions Logging Plugin.
*
* @since 3.9.0
*/
final class ActionLogs extends CMSPlugin
{
use DatabaseAwareTrait;
use UserFactoryAwareTrait;
/**
* Constructor.
*
* @param DispatcherInterface $dispatcher The dispatcher
* @param array $config An optional associative array of configuration settings
*
* @since 3.9.0
*/
public function __construct(DispatcherInterface $dispatcher, array $config)
{
parent::__construct($dispatcher, $config);
// Import actionlog plugin group so that these plugins will be triggered for events
PluginHelper::importPlugin('actionlog');
}
/**
* Adds additional fields to the user editing form for logs e-mail notifications
*
* @param Form $form The form to be altered.
* @param mixed $data The associated data for the form.
*
* @return boolean
*
* @since 3.9.0
*
* @throws \Exception
*/
public function onContentPrepareForm(Form $form, $data)
{
$formName = $form->getName();
$allowedFormNames = [
'com_users.profile',
'com_users.user',
];
if (!\in_array($formName, $allowedFormNames, true)) {
return true;
}
/**
* We only allow users who have Super User permission to change this setting for themselves or for other
* users who have the same Super User permission
*/
$user = $this->getApplication()->getIdentity();
if (!$user || !$user->authorise('core.admin')) {
return true;
}
// Load plugin language files.
$this->loadLanguage();
// If we are on the save command, no data is passed to $data variable, we need to get it directly from request
$jformData = $this->getApplication()->getInput()->get('jform', [], 'array');
if ($jformData && !$data) {
$data = $jformData;
}
if (\is_array($data)) {
$data = (object) $data;
}
if (empty($data->id) || !$this->getUserFactory()->loadUserById($data->id)->authorise('core.admin')) {
return true;
}
Form::addFormPath(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms');
if ((!PluginHelper::isEnabled('actionlog', 'joomla')) && ($this->getApplication()->isClient('administrator'))) {
$form->loadFile('information', false);
return true;
}
if (!PluginHelper::isEnabled('actionlog', 'joomla')) {
return true;
}
$form->loadFile('actionlogs', false);
return true;
}
/**
* Runs on content preparation
*
* @param string $context The context for the data
* @param object $data An object containing the data for the form.
*
* @return boolean
*
* @since 3.9.0
*/
public function onContentPrepareData($context, $data)
{
if (!\in_array($context, ['com_users.profile', 'com_users.user'])) {
return true;
}
if (\is_array($data)) {
$data = (object) $data;
}
if (empty($data->id) || !$this->getUserFactory()->loadUserById($data->id)->authorise('core.admin')) {
return true;
}
$db = $this->getDatabase();
$id = (int) $data->id;
$query = $db->getQuery(true)
->select($db->quoteName(['notify', 'extensions']))
->from($db->quoteName('#__action_logs_users'))
->where($db->quoteName('user_id') . ' = :userid')
->bind(':userid', $id, ParameterType::INTEGER);
try {
$values = $db->setQuery($query)->loadObject();
} catch (ExecutionFailureException $e) {
return false;
}
if (!$values) {
return true;
}
// Load plugin language files.
$this->loadLanguage();
$data->actionlogs = new \stdClass();
$data->actionlogs->actionlogsNotify = $values->notify;
$data->actionlogs->actionlogsExtensions = $values->extensions;
if (!HTMLHelper::isRegistered('users.actionlogsNotify')) {
HTMLHelper::register('users.actionlogsNotify', [__CLASS__, 'renderActionlogsNotify']);
}
if (!HTMLHelper::isRegistered('users.actionlogsExtensions')) {
HTMLHelper::register('users.actionlogsExtensions', [__CLASS__, 'renderActionlogsExtensions']);
}
return true;
}
/**
* Utility method to act on a user after it has been saved.
*
* @param array $user Holds the new user data.
* @param boolean $isNew True if a new user is stored.
* @param boolean $success True if user was successfully stored in the database.
* @param string $msg Message.
*
* @return void
*
* @since 3.9.0
*/
public function onUserAfterSave($user, $isNew, $success, $msg): void
{
if (!$success) {
return;
}
// Clear access rights in case user groups were changed.
$userObject = $this->getUserFactory()->loadUserById($user['id']);
$userObject->clearAccessRights();
$authorised = $userObject->authorise('core.admin');
$userid = (int) $user['id'];
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__action_logs_users'))
->where($db->quoteName('user_id') . ' = :userid')
->bind(':userid', $userid, ParameterType::INTEGER);
try {
$exists = (bool) $db->setQuery($query)->loadResult();
} catch (ExecutionFailureException $e) {
return;
}
$query->clear();
// If preferences don't exist, insert.
if (!$exists && $authorised && isset($user['actionlogs'])) {
$notify = (int) $user['actionlogs']['actionlogsNotify'];
$values = [':userid', ':notify'];
$bind = [$userid, $notify];
$columns = ['user_id', 'notify'];
$query->bind($values, $bind, ParameterType::INTEGER);
if (isset($user['actionlogs']['actionlogsExtensions'])) {
$values[] = ':extension';
$columns[] = 'extensions';
$extension = json_encode($user['actionlogs']['actionlogsExtensions']);
$query->bind(':extension', $extension);
}
$query->insert($db->quoteName('#__action_logs_users'))
->columns($db->quoteName($columns))
->values(implode(',', $values));
} elseif ($exists && $authorised && isset($user['actionlogs'])) {
// Update preferences.
$notify = (int) $user['actionlogs']['actionlogsNotify'];
$values = [$db->quoteName('notify') . ' = :notify'];
$query->bind(':notify', $notify, ParameterType::INTEGER);
if (isset($user['actionlogs']['actionlogsExtensions'])) {
$values[] = $db->quoteName('extensions') . ' = :extension';
$extension = json_encode($user['actionlogs']['actionlogsExtensions']);
$query->bind(':extension', $extension);
}
$query->update($db->quoteName('#__action_logs_users'))
->set($values)
->where($db->quoteName('user_id') . ' = :userid')
->bind(':userid', $userid, ParameterType::INTEGER);
} elseif ($exists && !$authorised) {
// Remove preferences if user is not authorised.
$query->delete($db->quoteName('#__action_logs_users'))
->where($db->quoteName('user_id') . ' = :userid')
->bind(':userid', $userid, ParameterType::INTEGER);
} else {
return;
}
try {
$db->setQuery($query)->execute();
} catch (ExecutionFailureException $e) {
// Do nothing.
}
}
/**
* Removes user preferences
*
* Method is called after user data is deleted from the database
*
* @param array $user Holds the user data
* @param boolean $success True if user was successfully stored in the database
* @param string $msg Message
*
* @return void
*
* @since 3.9.0
*/
public function onUserAfterDelete($user, $success, $msg): void
{
if (!$success) {
return;
}
$db = $this->getDatabase();
$userid = (int) $user['id'];
$query = $db->getQuery(true)
->delete($db->quoteName('#__action_logs_users'))
->where($db->quoteName('user_id') . ' = :userid')
->bind(':userid', $userid, ParameterType::INTEGER);
try {
$db->setQuery($query)->execute();
} catch (ExecutionFailureException $e) {
// Do nothing.
}
}
/**
* Method to render a value.
*
* @param integer|string $value The value (0 or 1).
*
* @return string The rendered value.
*
* @since 3.9.16
*/
public static function renderActionlogsNotify($value)
{
return Text::_($value ? 'JYES' : 'JNO');
}
/**
* Method to render a list of extensions.
*
* @param array|string $extensions Array of extensions or an empty string if none selected.
*
* @return string The rendered value.
*
* @since 3.9.16
*/
public static function renderActionlogsExtensions($extensions)
{
// No extensions selected.
if (!$extensions) {
return Text::_('JNONE');
}
foreach ($extensions as &$extension) {
// Load extension language files and translate extension name.
ActionlogsHelper::loadTranslationFiles($extension);
$extension = Text::_($extension);
}
return implode(', ', $extensions);
}
}

59
plugins/system/cache/cache.xml vendored Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_cache</name>
<author>Joomla! Project</author>
<creationDate>2007-02</creationDate>
<copyright>(C) 2007 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_CACHE_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Cache</namespace>
<files>
<folder plugin="cache">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_cache.ini</language>
<language tag="en-GB">language/en-GB/plg_system_cache.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="browsercache"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_CACHE_FIELD_BROWSERCACHE_LABEL"
default="0"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="exclude_menu_items"
type="menuitem"
label="PLG_CACHE_FIELD_EXCLUDE_MENU_ITEMS_LABEL"
multiple="multiple"
filter="intarray"
layout="joomla.form.field.groupedlist-fancy-select"
/>
</fieldset>
<fieldset name="advanced">
<field
name="exclude"
type="textarea"
label="PLG_CACHE_FIELD_EXCLUDE_LABEL"
description="PLG_CACHE_FIELD_EXCLUDE_DESC"
rows="15"
filter="raw"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,52 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.cache
*
* @copyright (C) 2022 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\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Profiler\Profiler;
use Joomla\CMS\Router\SiteRouter;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Cache\Extension\Cache;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
* @since 4.2.0
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = PluginHelper::getPlugin('system', 'cache');
$dispatcher = $container->get(DispatcherInterface::class);
$documentFactory = $container->get('document.factory');
$cacheControllerFactory = $container->get(CacheControllerFactoryInterface::class);
$profiler = (\defined('JDEBUG') && JDEBUG) ? Profiler::getInstance('Application') : null;
$router = $container->has(SiteRouter::class) ? $container->get(SiteRouter::class) : null;
$plugin = new Cache($dispatcher, (array) $plugin, $documentFactory, $cacheControllerFactory, $profiler, $router);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,382 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.cache
*
* @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\Cache\Extension;
use Joomla\CMS\Cache\CacheController;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Document\FactoryInterface as DocumentFactoryInterface;
use Joomla\CMS\Event\AbstractEvent;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Profiler\Profiler;
use Joomla\CMS\Router\SiteRouter;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Joomla\Event\Priority;
use Joomla\Event\SubscriberInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Page Cache Plugin.
*
* @since 1.5
*/
final class Cache extends CMSPlugin implements SubscriberInterface
{
/**
* Cache instance.
*
* @var CacheController
* @since 1.5
*/
private $cache;
/**
* The application's document factory interface
*
* @var DocumentFactoryInterface
* @since 4.2.0
*/
private $documentFactory;
/**
* Cache controller factory interface
*
* @var CacheControllerFactoryInterface
* @since 4.2.0
*/
private $cacheControllerFactory;
/**
* The application profiler, used when Debug Site is set to Yes in Global Configuration.
*
* @var Profiler|null
* @since 4.2.0
*/
private $profiler;
/**
* The frontend router, injected by the service provider.
*
* @var SiteRouter|null
* @since 4.2.0
*/
private $router;
/**
* Constructor
*
* @param DispatcherInterface $dispatcher The object to observe
* @param array $config An optional associative
* array of configuration
* settings. Recognized key
* values include 'name',
* 'group', 'params',
* 'language'
* (this list is not meant
* to be comprehensive).
* @param DocumentFactoryInterface $documentFactory The application's
* document factory
* @param CacheControllerFactoryInterface $cacheControllerFactory Cache controller factory
* @param Profiler|null $profiler The application profiler
* @param SiteRouter|null $router The frontend router
*
* @since 4.2.0
*/
public function __construct(
DispatcherInterface $dispatcher,
array $config,
DocumentFactoryInterface $documentFactory,
CacheControllerFactoryInterface $cacheControllerFactory,
?Profiler $profiler,
?SiteRouter $router
) {
parent::__construct($dispatcher, $config);
$this->documentFactory = $documentFactory;
$this->cacheControllerFactory = $cacheControllerFactory;
$this->profiler = $profiler;
$this->router = $router;
}
/**
* Returns an array of CMS events this plugin will listen to and the respective handlers.
*
* @return array
*
* @since 4.2.0
*/
public static function getSubscribedEvents(): array
{
/**
* Note that onAfterRender and onAfterRespond must be the last handlers to run for this
* plugin to operate as expected. These handlers put pages into cache. We must make sure
* that a. the page SHOULD be cached and b. we are caching the complete page, as it's
* output to the browser.
*/
return [
'onAfterRoute' => 'onAfterRoute',
'onAfterRender' => ['onAfterRender', Priority::LOW],
'onAfterRespond' => ['onAfterRespond', Priority::LOW],
];
}
/**
* Returns a cached page if the current URL exists in the cache.
*
* @param Event $event The Joomla event being handled
*
* @return void
*
* @since 4.0.0
*/
public function onAfterRoute(Event $event)
{
if (!$this->appStateSupportsCaching()) {
return;
}
// If any `pagecache` plugins return false for onPageCacheSetCaching, do not use the cache.
PluginHelper::importPlugin('pagecache');
$results = $this->getApplication()->triggerEvent('onPageCacheSetCaching');
$this->getCacheController()->setCaching(!\in_array(false, $results, true));
$data = $this->getCacheController()->get($this->getCacheKey());
if ($data === false) {
// No cached data.
return;
}
// Set the page content from the cache and output it to the browser.
$this->getApplication()->setBody($data);
echo $this->getApplication()->toString((bool) $this->getApplication()->get('gzip'));
// Mark afterCache in debug and run debug onAfterRespond events, e.g. show Joomla Debug Console if debug is active.
if (JDEBUG) {
// Create a document instance and load it into the application.
$document = $this->documentFactory
->createDocument($this->getApplication()->getInput()->get('format', 'html'));
$this->getApplication()->loadDocument($document);
if ($this->profiler) {
$this->profiler->mark('afterCache');
}
$this->getDispatcher()->dispatch('onAfterRespond', AbstractEvent::create(
'onAfterRespond',
[
'subject' => $this->getApplication(),
]
));
}
// Closes the application.
$this->getApplication()->close();
}
/**
* Does the current application state allow for caching?
*
* The following conditions must be met:
* * This is the frontend application. This plugin does not apply to other applications.
* * This is a GET request. This plugin does not apply to POST, PUT etc.
* * There is no currently logged in user (pages might have userspecific content).
* * The message queue is empty.
*
* The first two tests are cached to make early returns possible; these conditions cannot change
* throughout the lifetime of the request.
*
* The other two tests MUST NOT be cached because autologin plugins may fire anytime within
* the application lifetime logging in a user and messages can be generated anytime within the
* application's lifetime.
*
* @return boolean
* @since 4.2.0
*/
private function appStateSupportsCaching(): bool
{
static $isSite = null;
static $isGET = null;
if ($isSite === null) {
$isSite = $this->getApplication()->isClient('site');
$isGET = $this->getApplication()->getInput()->getMethod() === 'GET';
}
// Boolean shortcircuit evaluation means this returns fast false when $isSite is false.
return $isSite
&& $isGET
&& $this->getApplication()->getIdentity()->guest
&& empty($this->getApplication()->getMessageQueue());
}
/**
* Get the cache controller
*
* @return CacheController
* @since 4.2.0
*/
private function getCacheController(): CacheController
{
if (!empty($this->cache)) {
return $this->cache;
}
// Set the cache options.
$options = [
'defaultgroup' => 'page',
'browsercache' => $this->params->get('browsercache', 0),
'caching' => false,
];
// Instantiate cache with previous options.
$this->cache = $this->cacheControllerFactory->createCacheController('page', $options);
return $this->cache;
}
/**
* Get a cache key for the current page based on the url and possible other factors.
*
* @return string
*
* @since 3.7
*/
private function getCacheKey(): string
{
static $key;
if (!$key) {
PluginHelper::importPlugin('pagecache');
$parts = $this->getApplication()->triggerEvent('onPageCacheGetKey');
$parts[] = Uri::getInstance()->toString();
$key = md5(serialize($parts));
}
return $key;
}
/**
* After Render Event. Check whether the current page is excluded from cache.
*
* @param Event $event The CMS event we are handling.
*
* @return void
*
* @since 3.9.12
*/
public function onAfterRender(Event $event)
{
if (!$this->appStateSupportsCaching() || $this->getCacheController()->getCaching() === false) {
return;
}
if ($this->isExcluded() === true) {
$this->getCacheController()->setCaching(false);
return;
}
// Disable compression before caching the page.
$this->getApplication()->set('gzip', false);
}
/**
* Check if the page is excluded from the cache or not.
*
* @return boolean True if the page is excluded else false
*
* @since 3.5
*/
private function isExcluded(): bool
{
// Check if menu items have been excluded.
$excludedMenuItems = $this->params->get('exclude_menu_items', []);
if ($excludedMenuItems) {
// Get the current menu item.
$active = $this->getApplication()->getMenu()->getActive();
if ($active && $active->id && \in_array((int) $active->id, (array) $excludedMenuItems)) {
return true;
}
}
// Check if regular expressions are being used.
$exclusions = $this->params->get('exclude', '');
if ($exclusions) {
// Convert the exclusions into a normalised array
$exclusions = str_replace(["\r\n", "\r"], "\n", $exclusions);
$exclusions = explode("\n", $exclusions);
$filterExpression = function ($x) {
return $x !== '';
};
$exclusions = array_filter($exclusions, $filterExpression);
// Gets the internal (non-SEF) and the external (possibly SEF) URIs.
$internalUrl = '/index.php?'
. Uri::getInstance()->buildQuery($this->router->getVars());
$externalUrl = Uri::getInstance()->toString();
$reduceCallback
= function (bool $carry, string $exclusion) use ($internalUrl, $externalUrl) {
// Test both external and internal URIs
return $carry && preg_match(
'#' . $exclusion . '#i',
$externalUrl . ' ' . $internalUrl,
$match
);
};
$excluded = array_reduce($exclusions, $reduceCallback, false);
if ($excluded) {
return true;
}
}
// If any pagecache plugins return true for onPageCacheIsExcluded, exclude.
PluginHelper::importPlugin('pagecache');
$results = $this->getApplication()->triggerEvent('onPageCacheIsExcluded');
return \in_array(true, $results, true);
}
/**
* After Respond Event. Stores page in cache.
*
* @param Event $event The application event we are handling.
*
* @return void
*
* @since 1.5
*/
public function onAfterRespond(Event $event)
{
if (!$this->appStateSupportsCaching() || $this->getCacheController()->getCaching() === false) {
return;
}
// Saves current page in cache.
$this->getCacheController()->store($this->getApplication()->getBody(), $this->getCacheKey());
}
}

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;
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_fields</name>
<author>Joomla! Project</author>
<creationDate>2016-03</creationDate>
<copyright>(C) 2016 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.7.0</version>
<description>PLG_SYSTEM_FIELDS_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Fields</namespace>
<files>
<folder plugin="fields">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_fields.ini</language>
<language tag="en-GB">language/en-GB/plg_system_fields.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,48 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.fields
*
* @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\CMS\User\UserFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Fields\Extension\Fields;
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) {
$plugin = new Fields(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'fields')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setUserFactory($container->get(UserFactoryInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,483 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.fields
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Fields\Extension;
use Joomla\CMS\Event\Content;
use Joomla\CMS\Event\Model;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Fields Plugin
*
* @since 3.7
*/
final class Fields extends CMSPlugin
{
use UserFactoryAwareTrait;
/**
* Normalizes the request data.
*
* @param Model\NormaliseRequestDataEvent $event The event object
*
* @return void
*
* @since 3.8.7
*/
public function onContentNormaliseRequestData(Model\NormaliseRequestDataEvent $event)
{
$context = $event->getContext();
$data = $event->getData();
$form = $event->getForm();
if (!FieldsHelper::extract($context, $data)) {
return;
}
// Loop over all fields
foreach ($form->getGroup('com_fields') as $field) {
if ($field->disabled === true) {
/**
* Disabled fields should NEVER be added to the request as
* they should NEVER be added by the browser anyway so nothing to check against
* as "disabled" means no interaction at all.
*/
// Make sure the data object has an entry before delete it
if (isset($data->com_fields[$field->fieldname])) {
unset($data->com_fields[$field->fieldname]);
}
continue;
}
// Make sure the data object has an entry
if (isset($data->com_fields[$field->fieldname])) {
continue;
}
// Set a default value for the field
$data->com_fields[$field->fieldname] = false;
}
}
/**
* The save event.
*
* @param string $context The context
* @param \Joomla\CMS\Table\Table $item The table
* @param boolean $isNew Is new item
* @param array $data The validated data
*
* @return void
*
* @since 3.7.0
*/
public function onContentAfterSave($context, $item, $isNew, $data = []): void
{
// Check if data is an array and the item has an id
if (!\is_array($data) || empty($item->id) || empty($data['com_fields'])) {
return;
}
// Create correct context for category
if ($context === 'com_categories.category') {
$context = $item->extension . '.categories';
// Set the catid on the category to get only the fields which belong to this category
$item->catid = $item->id;
}
// Check the context
$parts = FieldsHelper::extract($context, $item);
if (!$parts) {
return;
}
// Compile the right context for the fields
$context = $parts[0] . '.' . $parts[1];
// Loading the fields
$fields = FieldsHelper::getFields($context, $item);
if (!$fields) {
return;
}
// Loading the model
/** @var \Joomla\Component\Fields\Administrator\Model\FieldModel $model */
$model = $this->getApplication()->bootComponent('com_fields')->getMVCFactory()
->createModel('Field', 'Administrator', ['ignore_request' => true]);
// Loop over the fields
foreach ($fields as $field) {
// Determine the value if it is (un)available from the data
if (\array_key_exists($field->name, $data['com_fields'])) {
$value = $data['com_fields'][$field->name] === false ? null : $data['com_fields'][$field->name];
} else {
// Field not available on form, use stored value
$value = $field->rawvalue;
}
// If no value set (empty) remove value from database
if (\is_array($value) ? !\count($value) : !\strlen($value)) {
$value = null;
}
// JSON encode value for complex fields
if (\is_array($value) && (\count($value, COUNT_NORMAL) !== \count($value, COUNT_RECURSIVE) || !\count(array_filter(array_keys($value), 'is_numeric')))) {
$value = json_encode($value);
}
// Setting the value for the field and the item
$model->setFieldValue($field->id, $item->id, $value);
}
}
/**
* The save event.
*
* @param array $userData The date
* @param boolean $isNew Is new
* @param boolean $success Is success
* @param string $msg The message
*
* @return void
*
* @since 3.7.0
*/
public function onUserAfterSave($userData, $isNew, $success, $msg): void
{
// It is not possible to manipulate the user during save events
// Check if data is valid or we are in a recursion
if (!$userData['id'] || !$success) {
return;
}
$user = $this->getUserFactory()->loadUserById($userData['id']);
$task = $this->getApplication()->getInput()->getCmd('task');
// Skip fields save when we activate a user, because we will lose the saved data
if (\in_array($task, ['activate', 'block', 'unblock'])) {
return;
}
// Trigger the events with a real user
$this->onContentAfterSave('com_users.user', $user, false, $userData);
}
/**
* The delete event.
*
* @param string $context The context
* @param \stdClass $item The item
*
* @return void
*
* @since 3.7.0
*/
public function onContentAfterDelete($context, $item): void
{
// Set correct context for category
if ($context === 'com_categories.category') {
$context = $item->extension . '.categories';
}
$parts = FieldsHelper::extract($context, $item);
if (!$parts || empty($item->id)) {
return;
}
$context = $parts[0] . '.' . $parts[1];
/** @var \Joomla\Component\Fields\Administrator\Model\FieldModel $model */
$model = $this->getApplication()->bootComponent('com_fields')->getMVCFactory()
->createModel('Field', 'Administrator', ['ignore_request' => true]);
$model->cleanupValues($context, $item->id);
}
/**
* The user delete event.
*
* @param \stdClass $user The context
* @param boolean $success Is success
* @param string $msg The message
*
* @return void
*
* @since 3.7.0
*/
public function onUserAfterDelete($user, $success, $msg): void
{
$item = new \stdClass();
$item->id = $user['id'];
$this->onContentAfterDelete('com_users.user', $item);
}
/**
* The form event.
*
* @param Model\PrepareFormEvent $event The event object
*
* @return void
*
* @since 3.7.0
*/
public function onContentPrepareForm(Model\PrepareFormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
$context = $form->getName();
// When a category is edited, the context is com_categories.categorycom_content
if (strpos($context, 'com_categories.category') === 0) {
$context = str_replace('com_categories.category', '', $context) . '.categories';
$data = $data ?: $this->getApplication()->getInput()->get('jform', [], 'array');
// Set the catid on the category to get only the fields which belong to this category
if (\is_array($data) && \array_key_exists('id', $data)) {
$data['catid'] = $data['id'];
}
if (\is_object($data) && isset($data->id)) {
$data->catid = $data->id;
}
}
$parts = FieldsHelper::extract($context, $form);
if (!$parts) {
return;
}
$input = $this->getApplication()->getInput();
// If we are on the save command we need the actual data
$jformData = $input->get('jform', [], 'array');
if ($jformData && !$data) {
$data = $jformData;
}
if (\is_array($data)) {
$data = (object) $data;
}
FieldsHelper::prepareForm($parts[0] . '.' . $parts[1], $form, $data);
}
/**
* The display event.
*
* @param Content\AfterTitleEvent $event The event object
*
* @return void
*
* @since 3.7.0
*/
public function onContentAfterTitle(Content\AfterTitleEvent $event)
{
$event->addResult($this->display($event->getContext(), $event->getItem(), $event->getParams(), 1));
}
/**
* The display event.
*
* @param Content\BeforeDisplayEvent $event The event object
*
* @return void
*
* @since 3.7.0
*/
public function onContentBeforeDisplay(Content\BeforeDisplayEvent $event)
{
$event->addResult($this->display($event->getContext(), $event->getItem(), $event->getParams(), 2));
}
/**
* The display event.
*
* @param Content\AfterDisplayEvent $event The event object
*
* @return void
*
* @since 3.7.0
*/
public function onContentAfterDisplay(Content\AfterDisplayEvent $event)
{
$event->addResult($this->display($event->getContext(), $event->getItem(), $event->getParams(), 3));
}
/**
* Performs the display event.
*
* @param string $context The context
* @param \stdClass $item The item
* @param Registry $params The params
* @param integer $displayType The type
*
* @return string
*
* @since 3.7.0
*/
private function display($context, $item, $params, $displayType)
{
$parts = FieldsHelper::extract($context, $item);
if (!$parts) {
return '';
}
// If we have a category, set the catid field to fetch only the fields which belong to it
if ($parts[1] === 'categories' && !isset($item->catid)) {
$item->catid = $item->id;
}
$context = $parts[0] . '.' . $parts[1];
// Convert tags
if ($context == 'com_tags.tag' && !empty($item->type_alias)) {
// Set the context
$context = $item->type_alias;
$item = $this->prepareTagItem($item);
}
if (\is_string($params) || !$params) {
$params = new Registry($params);
}
$fields = FieldsHelper::getFields($context, $item, $displayType);
if ($fields) {
if ($this->getApplication()->isClient('site') && Multilanguage::isEnabled() && isset($item->language) && $item->language === '*') {
$lang = $this->getApplication()->getLanguage()->getTag();
foreach ($fields as $key => $field) {
if ($field->language === '*' || $field->language == $lang) {
continue;
}
unset($fields[$key]);
}
}
}
if ($fields) {
foreach ($fields as $key => $field) {
$fieldDisplayType = $field->params->get('display', '2');
if ($fieldDisplayType == $displayType) {
continue;
}
unset($fields[$key]);
}
}
if ($fields) {
return FieldsHelper::render(
$context,
'fields.render',
[
'item' => $item,
'context' => $context,
'fields' => $fields,
]
);
}
return '';
}
/**
* Performs the display event.
*
* @param Content\ContentPrepareEvent $event The event object
*
* @return void
*
* @since 3.7.0
*/
public function onContentPrepare(Content\ContentPrepareEvent $event)
{
$context = $event->getContext();
$item = $event->getItem();
// Check property exists (avoid costly & useless recreation), if need to recreate them, just unset the property!
if (isset($item->jcfields)) {
return;
}
$parts = FieldsHelper::extract($context, $item);
if (!$parts) {
return;
}
$context = $parts[0] . '.' . $parts[1];
// Convert tags
if ($context == 'com_tags.tag' && !empty($item->type_alias)) {
// Set the context
$context = $item->type_alias;
$item = $this->prepareTagItem($item);
}
// Get item's fields, also preparing their value property for manual display
// (calling plugins events and loading layouts to get their HTML display)
$fields = FieldsHelper::getFields($context, $item, true);
// Adding the fields to the object
$item->jcfields = [];
foreach ($fields as $key => $field) {
$item->jcfields[$field->id] = $field;
}
}
/**
* Prepares a tag item to be ready for com_fields.
*
* @param \stdClass $item The item
*
* @return object
*
* @since 3.8.4
*/
private function prepareTagItem($item)
{
// Map core fields
$item->id = $item->content_item_id;
$item->language = $item->core_language;
// Also handle the catid
if (!empty($item->core_catid)) {
$item->catid = $item->core_catid;
}
return $item;
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_guidedtours</name>
<author>Joomla! Project</author>
<creationDate>2023-02</creationDate>
<copyright>(C) 2023 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>4.3.0</version>
<description>PLG_SYSTEM_GUIDEDTOURS_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\GuidedTours</namespace>
<files>
<folder plugin="guidedtours">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_guidedtours.ini</language>
<language tag="en-GB">language/en-GB/plg_system_guidedtours.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,55 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.guidedtours
*
* @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\CMS\WebAsset\WebAssetRegistry;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\GuidedTours\Extension\GuidedTours;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.3.0
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$app = Factory::getApplication();
$plugin = new GuidedTours(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'guidedtours'),
$app->isClient('administrator')
);
$plugin->setApplication($app);
$wa = $container->get(WebAssetRegistry::class);
$wa->addRegistryFile('media/plg_system_guidedtours/joomla.asset.json');
return $plugin;
}
);
}
};

View File

@ -0,0 +1,260 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.guidedtours
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\GuidedTours\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Session\Session;
use Joomla\Component\Guidedtours\Administrator\Extension\GuidedtoursComponent;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Guided Tours plugin to add interactive tours to the administrator interface.
*
* @since 4.3.0
*/
final class GuidedTours extends CMSPlugin implements SubscriberInterface
{
/**
* A mapping for the step types
*
* @var string[]
* @since 4.3.0
*/
protected $stepType = [
GuidedtoursComponent::STEP_NEXT => 'next',
GuidedtoursComponent::STEP_REDIRECT => 'redirect',
GuidedtoursComponent::STEP_INTERACTIVE => 'interactive',
];
/**
* A mapping for the step interactive types
*
* @var string[]
* @since 4.3.0
*/
protected $stepInteractiveType = [
GuidedtoursComponent::STEP_INTERACTIVETYPE_FORM_SUBMIT => 'submit',
GuidedtoursComponent::STEP_INTERACTIVETYPE_TEXT => 'text',
GuidedtoursComponent::STEP_INTERACTIVETYPE_OTHER => 'other',
GuidedtoursComponent::STEP_INTERACTIVETYPE_BUTTON => 'button',
];
/**
* An internal flag whether plugin should listen any event.
*
* @var bool
*
* @since 4.3.0
*/
protected static $enabled = false;
/**
* Constructor
*
* @param DispatcherInterface $dispatcher The object to observe
* @param array $config An optional associative array of configuration settings.
* @param boolean $enabled An internal flag whether plugin should listen any event.
*
* @since 4.3.0
*/
public function __construct(DispatcherInterface $dispatcher, array $config = [], bool $enabled = false)
{
self::$enabled = $enabled;
parent::__construct($dispatcher, $config);
}
/**
* function for getSubscribedEvents : new Joomla 4 feature
*
* @return array
*
* @since 4.3.0
*/
public static function getSubscribedEvents(): array
{
return self::$enabled ? [
'onAjaxGuidedtours' => 'startTour',
'onBeforeCompileHead' => 'onBeforeCompileHead',
] : [];
}
/**
* Retrieve and starts a tour and its steps through Ajax.
*
* @return null|object
*
* @since 4.3.0
*/
public function startTour(Event $event)
{
$tourId = (int) $this->getApplication()->getInput()->getInt('id');
$tourUid = $this->getApplication()->getInput()->getString('uid', '');
$tourUid = $tourUid !== '' ? urldecode($tourUid) : '';
$tour = null;
// Load plugin language files
$this->loadLanguage();
if ($tourId > 0) {
$tour = $this->getTour($tourId);
} elseif ($tourUid !== '') {
$tour = $this->getTour($tourUid);
}
$event->setArgument('result', $tour ?? new \stdClass());
return $tour;
}
/**
* Listener for the `onBeforeCompileHead` event
*
* @return void
*
* @since 4.3.0
*/
public function onBeforeCompileHead()
{
$app = $this->getApplication();
$doc = $app->getDocument();
$user = $app->getIdentity();
if ($user != null && $user->id > 0) {
// Load plugin language files
$this->loadLanguage();
Text::script('JCANCEL');
Text::script('PLG_SYSTEM_GUIDEDTOURS_BACK');
Text::script('PLG_SYSTEM_GUIDEDTOURS_COMPLETE');
Text::script('PLG_SYSTEM_GUIDEDTOURS_COULD_NOT_LOAD_THE_TOUR');
Text::script('PLG_SYSTEM_GUIDEDTOURS_NEXT');
Text::script('PLG_SYSTEM_GUIDEDTOURS_START');
Text::script('PLG_SYSTEM_GUIDEDTOURS_STEP_NUMBER_OF');
Text::script('PLG_SYSTEM_GUIDEDTOURS_TOUR_ERROR');
$doc->addScriptOptions('com_guidedtours.token', Session::getFormToken());
// Load required assets
$doc->getWebAssetManager()
->usePreset('plg_system_guidedtours.guidedtours');
}
}
/**
* Get a tour and its steps or null if not found
*
* @param integer|string $tourId The ID or Uid of the tour to load
*
* @return null|object
*
* @since 4.3.0
*/
private function getTour($tourId)
{
$app = $this->getApplication();
$factory = $app->bootComponent('com_guidedtours')->getMVCFactory();
$tourModel = $factory->createModel(
'Tour',
'Administrator',
['ignore_request' => true]
);
$item = $tourModel->getItem($tourId);
return $this->processTour($item);
}
/**
* Return a tour and its steps or null if not found
*
* @param TourTable $item The tour to load
*
* @return null|object
*
* @since 5.0.0
*/
private function processTour($item)
{
$app = $this->getApplication();
$user = $app->getIdentity();
$factory = $app->bootComponent('com_guidedtours')->getMVCFactory();
if (empty($item->id) || $item->published < 1 || !\in_array($item->access, $user->getAuthorisedViewLevels())) {
return null;
}
// We don't want to show all parameters, so take only a subset of the tour attributes
$tour = new \stdClass();
$tour->id = $item->id;
$stepsModel = $factory->createModel(
'Steps',
'Administrator',
['ignore_request' => true]
);
$stepsModel->setState('filter.tour_id', $item->id);
$stepsModel->setState('filter.published', 1);
$stepsModel->setState('list.ordering', 'a.ordering');
$stepsModel->setState('list.direction', 'ASC');
$steps = $stepsModel->getItems();
$tour->steps = [];
$temp = new \stdClass();
$temp->id = 0;
$temp->title = $this->getApplication()->getLanguage()->_($item->title);
$temp->description = $this->getApplication()->getLanguage()->_($item->description);
$temp->url = $item->url;
// Replace 'images/' to '../images/' when using an image from /images in backend.
$temp->description = preg_replace('*src\=\"(?!administrator\/)images/*', 'src="../images/', $temp->description);
$tour->steps[] = $temp;
foreach ($steps as $i => $step) {
$temp = new \stdClass();
$temp->id = $i + 1;
$temp->title = $this->getApplication()->getLanguage()->_($step->title);
$temp->description = $this->getApplication()->getLanguage()->_($step->description);
$temp->position = $step->position;
$temp->target = $step->target;
$temp->type = $this->stepType[$step->type];
$temp->interactive_type = $this->stepInteractiveType[$step->interactive_type];
$temp->url = $step->url;
$temp->tour_id = $step->tour_id;
$temp->step_id = $step->id;
// Replace 'images/' to '../images/' when using an image from /images in backend.
$temp->description = preg_replace('*src\=\"(?!administrator\/)images/*', 'src="../images/', $temp->description);
$tour->steps[] = $temp;
}
return $tour;
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_highlight</name>
<author>Joomla! Project</author>
<creationDate>2011-08</creationDate>
<copyright>(C) 2011 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_SYSTEM_HIGHLIGHT_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Highlight</namespace>
<files>
<folder plugin="highlight">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_highlight.ini</language>
<language tag="en-GB">language/en-GB/plg_system_highlight.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.highlight
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Highlight\Extension\Highlight;
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) {
$plugin = new Highlight(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'highlight')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,127 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.highlight
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Highlight\Extension;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Finder\Administrator\Indexer\Result;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* System plugin to highlight terms.
*
* @since 2.5
*/
final class Highlight extends CMSPlugin
{
/**
* Method to catch the onAfterDispatch event.
*
* This is where we setup the click-through content highlighting for.
* The highlighting is done with JavaScript so we just
* need to check a few parameters and the JHtml behavior will do the rest.
*
* @return void
*
* @since 2.5
*/
public function onAfterDispatch()
{
// Check that we are in the site application.
if (!$this->getApplication()->isClient('site')) {
return;
}
// Set the variables.
$input = $this->getApplication()->getInput();
$extension = $input->get('option', '', 'cmd');
// Check if the highlighter is enabled.
if (!ComponentHelper::getParams($extension)->get('highlight_terms', 1)) {
return;
}
// Check if the highlighter should be activated in this environment.
if ($input->get('tmpl', '', 'cmd') === 'component' || $this->getApplication()->getDocument()->getType() !== 'html') {
return;
}
// Get the terms to highlight from the request.
$terms = $input->request->get('highlight', null, 'base64');
$terms = $terms ? json_decode(base64_decode($terms)) : null;
// Check the terms.
if (empty($terms)) {
return;
}
// Clean the terms array.
$filter = InputFilter::getInstance();
$cleanTerms = [];
foreach ($terms as $term) {
$cleanTerms[] = htmlspecialchars($filter->clean($term, 'string'));
}
/** @var \Joomla\CMS\Document\HtmlDocument $doc */
$doc = $this->getApplication()->getDocument();
// Activate the highlighter.
if (!empty($cleanTerms)) {
$doc->getWebAssetManager()->useScript('highlight');
$doc->addScriptOptions(
'highlight',
[[
'class' => 'js-highlight',
'highLight' => $cleanTerms,
]]
);
}
// Adjust the component buffer.
$buf = $doc->getBuffer('component');
$buf = '<div class="js-highlight">' . $buf . '</div>';
$doc->setBuffer($buf, 'component');
}
/**
* Method to catch the onFinderResult event.
*
* @param Result $item The search result
* @param object $query The search query of this result
*
* @return void
*
* @since 4.0.0
*/
public function onFinderResult($item, $query)
{
static $params;
if (\is_null($params)) {
$params = ComponentHelper::getParams('com_finder');
}
// Get the route with highlighting information.
if (
!empty($query->highlight)
&& empty($item->mime)
&& $params->get('highlight_terms', 1)
) {
$item->route .= '&highlight=' . base64_encode(json_encode($query->highlight));
}
}
}

View File

@ -0,0 +1,328 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_httpheaders</name>
<author>Joomla! Project</author>
<creationDate>2017-10</creationDate>
<copyright>(C) 2018 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>4.0.0</version>
<description>PLG_SYSTEM_HTTPHEADERS_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Httpheaders</namespace>
<files>
<folder>postinstall</folder>
<folder plugin="httpheaders">services</folder>
<folder>src</folder>
</files>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="xframeoptions"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_XFRAMEOPTIONS"
layout="joomla.form.field.radio.switcher"
default="1"
filter="integer"
validate="options"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="referrerpolicy"
type="list"
label="PLG_SYSTEM_HTTPHEADERS_REFERRERPOLICY"
default="strict-origin-when-cross-origin"
validate="options"
>
<option value="disabled">JDISABLED</option>
<option value="no-referrer">no-referrer</option>
<option value="no-referrer-when-downgrade">no-referrer-when-downgrade</option>
<option value="origin">origin</option>
<option value="origin-when-cross-origin">origin-when-cross-origin</option>
<option value="same-origin">same-origin</option>
<option value="strict-origin">strict-origin</option>
<option value="strict-origin-when-cross-origin">strict-origin-when-cross-origin</option>
<option value="unsafe-url">unsafe-url</option>
</field>
<field
name="coop"
type="list"
label="PLG_SYSTEM_HTTPHEADERS_COOP"
default="same-origin"
validate="options"
>
<option value="disabled">JDISABLED</option>
<option value="same-origin">same-origin</option>
<option value="same-origin-allow-popups">same-origin-allow-popups</option>
<option value="unsafe-none">unsafe-none</option>
</field>
<field
name="additional_httpheader"
type="subform"
label="PLG_SYSTEM_HTTPHEADERS_ADDITIONAL_HEADER"
multiple="true"
>
<form>
<field
name="key"
type="list"
label="PLG_SYSTEM_HTTPHEADERS_ADDITIONAL_HEADER_KEY"
validate="options"
class="col-md-4"
>
<option value="content-security-policy">Content-Security-Policy</option>
<option value="content-security-policy-report-only">Content-Security-Policy-Report-Only</option>
<option value="cross-origin-opener-policy">Cross-Origin-Opener-Policy</option>
<option value="expect-ct">Expect-CT</option>
<option value="feature-policy">Feature-Policy</option>
<option value="nel">NEL</option>
<option value="permissions-policy">Permissions-Policy</option>
<option value="referrer-policy">Referrer-Policy</option>
<option value="report-to">Report-To</option>
<option value="strict-transport-security">Strict-Transport-Security</option>
<option value="x-frame-options">X-Frame-Options</option>
</field>
<field
name="value"
type="text"
label="PLG_SYSTEM_HTTPHEADERS_ADDITIONAL_HEADER_VALUE"
class="col-md-10"
/>
<field
name="client"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT"
default="site"
validate="options"
class="col-md-12"
>
<option value="site">JSITE</option>
<option value="administrator">JADMINISTRATOR</option>
<option value="both">PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT_BOTH</option>
</field>
</form>
</field>
</fieldset>
<fieldset name="hsts" label="Strict-Transport-Security (HSTS)">
<field
name="hsts"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_HSTS"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
validate="options"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="hsts_maxage"
type="number"
label="PLG_SYSTEM_HTTPHEADERS_HSTS_MAXAGE"
description="PLG_SYSTEM_HTTPHEADERS_HSTS_MAXAGE_DESC"
default="31536000"
filter="integer"
validate="number"
min="300"
showon="hsts:1"
/>
<field
name="hsts_subdomains"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_HSTS_SUBDOMAINS"
description="PLG_SYSTEM_HTTPHEADERS_HSTS_SUBDOMAINS_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
validate="options"
showon="hsts:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="hsts_preload"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_HSTS_PRELOAD"
description="PLG_SYSTEM_HTTPHEADERS_HSTS_PRELOAD_NOTE_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
validate="options"
showon="hsts:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
</fieldset>
<fieldset name="csp" label="Content-Security-Policy (CSP)">
<field
name="contentsecuritypolicy"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="contentsecuritypolicy_client"
type="list"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_CLIENT"
default="site"
validate="options"
showon="contentsecuritypolicy:1"
>
<option value="site">JSITE</option>
<option value="administrator">JADMINISTRATOR</option>
<option value="both">PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT_BOTH</option>
</field>
<field
name="contentsecuritypolicy_report_only"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_REPORT_ONLY"
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_REPORT_ONLY_DESC"
layout="joomla.form.field.radio.switcher"
default="1"
showon="contentsecuritypolicy:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="nonce_enabled"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_NONCE_ENABLED"
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_NONCE_ENABLED_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
showon="contentsecuritypolicy:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="script_hashes_enabled"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_SCRIPT_HASHES_ENABLED"
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_SCRIPT_HASHES_ENABLED_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
showon="contentsecuritypolicy:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="strict_dynamic_enabled"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STRICT_DYNAMIC_ENABLED"
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STRICT_DYNAMIC_ENABLED_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
showon="contentsecuritypolicy:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="style_hashes_enabled"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STYLE_HASHES_ENABLED"
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STYLE_HASHES_ENABLED_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
showon="contentsecuritypolicy:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="frame_ancestors_self_enabled"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_FRAME_ANCESTORS_SELF_ENABLED"
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_FRAME_ANCESTORS_SELF_ENABLED_DESC"
layout="joomla.form.field.radio.switcher"
default="1"
showon="contentsecuritypolicy:1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="contentsecuritypolicy_values"
type="subform"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_VALUES"
multiple="true"
showon="contentsecuritypolicy:1"
>
<form>
<field
name="directive"
type="list"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_VALUES_DIRECTIVE"
class="col-md-4"
validate="options"
>
<option value="child-src">child-src</option>
<option value="connect-src">connect-src</option>
<option value="default-src">default-src</option>
<option value="font-src">font-src</option>
<option value="frame-src">frame-src</option>
<option value="img-src">img-src</option>
<option value="manifest-src">manifest-src</option>
<option value="media-src">media-src</option>
<option value="prefetch-src">prefetch-src</option>
<option value="object-src">object-src</option>
<option value="script-src">script-src</option>
<option value="script-src-elem">script-src-elem</option>
<option value="script-src-attr">script-src-attr</option>
<option value="style-src">style-src</option>
<option value="style-src-elem">style-src-elem</option>
<option value="style-src-attr">style-src-attr</option>
<option value="worker-src">worker-src</option>
<option value="base-uri">base-uri</option>
<option value="plugin-types">plugin-types</option>
<option value="sandbox">sandbox</option>
<option value="form-action">form-action</option>
<option value="frame-ancestors">frame-ancestors</option>
<option value="navigate-to">navigate-to</option>
<option value="report-uri">report-uri</option>
<option value="report-to">report-to</option>
<option value="block-all-mixed-content">block-all-mixed-content</option>
<option value="upgrade-insecure-requests">upgrade-insecure-requests</option>
<option value="require-sri-for">require-sri-for</option>
</field>
<field
name="value"
type="text"
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_VALUES_VALUE"
class="col-md-10"
showon="directive!:block-all-mixed-content[AND]directive!:upgrade-insecure-requests"
/>
<field
name="client"
type="radio"
label="PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT"
default="site"
class="col-md-12"
>
<option value="site">JSITE</option>
<option value="administrator">JADMINISTRATOR</option>
<option value="both">PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT_BOTH</option>
</field>
</form>
</field>
</fieldset>
</fields>
</config>
<languages>
<language tag="en-GB">language/en-GB/plg_system_httpheaders.ini</language>
<language tag="en-GB">language/en-GB/plg_system_httpheaders.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,62 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.HttpHeaders
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Checks if the plugin is enabled. If not it returns true, meaning that the
* message concerning the HTTPHeaders Plugin should be displayed.
*
* @return integer
*
* @since 4.0.0
*/
function httpheaders_postinstall_condition()
{
return !Joomla\CMS\Plugin\PluginHelper::isEnabled('system', 'httpheaders');
}
/**
* Enables the HTTPHeaders plugin
*
* @return void
*
* @since 4.0.0
*/
function httpheaders_postinstall_action()
{
// Enable the plugin
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('httpheaders'));
$db->setQuery($query);
$db->execute();
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('httpheaders'));
$db->setQuery($query);
$extensionId = $db->loadResult();
$url = 'index.php?option=com_plugins&task=plugin.edit&extension_id=' . $extensionId;
Factory::getApplication()->redirect($url);
}

View File

@ -0,0 +1,48 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.httpheaders
*
* @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\Httpheaders\Extension\Httpheaders;
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) {
$plugin = new Httpheaders(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'httpheaders'),
Factory::getApplication()
);
$plugin->setDatabase($container->get(DatabaseInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,456 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.httpheaders
*
* @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\Httpheaders\Extension;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Plugin class for HTTP Headers
*
* @since 4.0.0
*/
final class Httpheaders extends CMSPlugin implements SubscriberInterface
{
use DatabaseAwareTrait;
/**
* The generated csp nonce value
*
* @var string
* @since 4.0.0
*/
private $cspNonce;
/**
* The list of the supported HTTP headers
*
* @var array
* @since 4.0.0
*/
private $supportedHttpHeaders = [
'strict-transport-security',
'content-security-policy',
'content-security-policy-report-only',
'x-frame-options',
'referrer-policy',
'expect-ct',
'feature-policy',
'cross-origin-opener-policy',
'report-to',
'permissions-policy',
'nel',
];
/**
* The list of valid directives based on: https://www.w3.org/TR/CSP3/#csp-directives
*
* @var array
* @since 4.0.0
*/
private $validDirectives = [
'child-src',
'connect-src',
'default-src',
'font-src',
'frame-src',
'img-src',
'manifest-src',
'media-src',
'prefetch-src',
'object-src',
'script-src',
'script-src-elem',
'script-src-attr',
'style-src',
'style-src-elem',
'style-src-attr',
'worker-src',
'base-uri',
'plugin-types',
'sandbox',
'form-action',
'frame-ancestors',
'navigate-to',
'report-uri',
'report-to',
'block-all-mixed-content',
'upgrade-insecure-requests',
'require-sri-for',
];
/**
* The list of directives without a value
*
* @var array
* @since 4.0.0
*/
private $noValueDirectives = [
'block-all-mixed-content',
'upgrade-insecure-requests',
];
/**
* The list of directives supporting nonce
*
* @var array
* @since 4.0.0
*/
private $nonceDirectives = [
'script-src',
'style-src',
];
/**
* @param DispatcherInterface $dispatcher The object to observe -- event dispatcher.
* @param array $config An optional associative array of configuration settings.
* @param CMSApplicationInterface $app The app
*
* @since 4.0.0
*/
public function __construct(DispatcherInterface $dispatcher, array $config, CMSApplicationInterface $app)
{
parent::__construct($dispatcher, $config);
$this->setApplication($app);
$nonceEnabled = (int) $this->params->get('nonce_enabled', 0);
// Nonce generation when it's enabled
if ($nonceEnabled) {
$this->cspNonce = base64_encode(bin2hex(random_bytes(64)));
}
// Set the nonce, when not set we set it to NULL which is checked down the line
$this->getApplication()->set('csp_nonce', $this->cspNonce);
}
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 4.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterInitialise' => 'setHttpHeaders',
'onAfterRender' => 'applyHashesToCspRule',
];
}
/**
* The `applyHashesToCspRule` method makes sure the csp hashes are added to the csp header when enabled
*
* @param Event $event
*
* @return void
*
* @since 4.0.0
*/
public function applyHashesToCspRule(Event $event): void
{
// CSP is only relevant on html pages. Let's early exit here.
if ($this->getApplication()->getDocument()->getType() !== 'html') {
return;
}
$scriptHashesEnabled = (int) $this->params->get('script_hashes_enabled', 0);
$styleHashesEnabled = (int) $this->params->get('style_hashes_enabled', 0);
// Early exit when both options are disabled
if (!$scriptHashesEnabled && !$styleHashesEnabled) {
return;
}
$headData = $this->getApplication()->getDocument()->getHeadData();
$scriptHashes = [];
$styleHashes = [];
if ($scriptHashesEnabled) {
// Generate the hashes for the script-src
$inlineScripts = \is_array($headData['script']) ? $headData['script'] : [];
foreach ($inlineScripts as $type => $scripts) {
foreach ($scripts as $hash => $scriptContent) {
$scriptHashes[] = "'sha256-" . base64_encode(hash('sha256', $scriptContent, true)) . "'";
}
}
}
if ($styleHashesEnabled) {
// Generate the hashes for the style-src
$inlineStyles = \is_array($headData['style']) ? $headData['style'] : [];
foreach ($inlineStyles as $type => $styles) {
foreach ($styles as $hash => $styleContent) {
$styleHashes[] = "'sha256-" . base64_encode(hash('sha256', $styleContent, true)) . "'";
}
}
}
// Replace the hashes in the csp header when set.
$headers = $this->getApplication()->getHeaders();
foreach ($headers as $id => $headerConfiguration) {
if (
strtolower($headerConfiguration['name']) === 'content-security-policy'
|| strtolower($headerConfiguration['name']) === 'content-security-policy-report-only'
) {
$newHeaderValue = $headerConfiguration['value'];
if (!empty($scriptHashes)) {
$newHeaderValue = str_replace('{script-hashes}', implode(' ', $scriptHashes), $newHeaderValue);
} else {
$newHeaderValue = str_replace('{script-hashes}', '', $newHeaderValue);
}
if (!empty($styleHashes)) {
$newHeaderValue = str_replace('{style-hashes}', implode(' ', $styleHashes), $newHeaderValue);
} else {
$newHeaderValue = str_replace('{style-hashes}', '', $newHeaderValue);
}
$this->getApplication()->setHeader($headerConfiguration['name'], $newHeaderValue, true);
}
}
}
/**
* The `setHttpHeaders` method handle the setting of the configured HTTP Headers
*
* @param Event $event
*
* @return void
*
* @since 4.0.0
*/
public function setHttpHeaders(Event $event): void
{
// Set the default header when they are enabled
$this->setStaticHeaders();
// Handle CSP Header configuration
$cspEnabled = (int) $this->params->get('contentsecuritypolicy', 0);
$cspClient = (string) $this->params->get('contentsecuritypolicy_client', 'site');
// Check whether CSP is enabled and enabled by the current client
if ($cspEnabled && ($this->getApplication()->isClient($cspClient) || $cspClient === 'both')) {
$this->setCspHeader();
}
}
/**
* Set the CSP header when enabled
*
* @return void
*
* @since 4.0.0
*/
private function setCspHeader(): void
{
$cspReadOnly = (int) $this->params->get('contentsecuritypolicy_report_only', 1);
$cspHeader = $cspReadOnly === 0 ? 'content-security-policy' : 'content-security-policy-report-only';
// In custom mode we compile the header from the values configured
$cspValues = $this->params->get('contentsecuritypolicy_values', []);
$nonceEnabled = (int) $this->params->get('nonce_enabled', 0);
$scriptHashesEnabled = (int) $this->params->get('script_hashes_enabled', 0);
$strictDynamicEnabled = (int) $this->params->get('strict_dynamic_enabled', 0);
$styleHashesEnabled = (int) $this->params->get('style_hashes_enabled', 0);
$frameAncestorsSelfEnabled = (int) $this->params->get('frame_ancestors_self_enabled', 1);
$frameAncestorsSet = false;
foreach ($cspValues as $cspValue) {
// Handle the client settings foreach header
if (!$this->getApplication()->isClient($cspValue->client) && $cspValue->client != 'both') {
continue;
}
// Handle non value directives
if (\in_array($cspValue->directive, $this->noValueDirectives)) {
$newCspValues[] = trim($cspValue->directive);
continue;
}
// We can only use this if this is a valid entry
if (
\in_array($cspValue->directive, $this->validDirectives)
&& !empty($cspValue->value)
) {
if (\in_array($cspValue->directive, $this->nonceDirectives) && $nonceEnabled) {
/**
* That line is for B/C we do no longer require to add the nonce tag
* but add it once the setting is enabled so this line here is needed
* to remove the outdated tag that was required until 4.2.0
*/
$cspValue->value = str_replace('{nonce}', '', $cspValue->value);
// Append the nonce when the nonce setting is enabled
$cspValue->value = "'nonce-" . $this->cspNonce . "' " . $cspValue->value;
}
// Append the script hashes placeholder
if ($scriptHashesEnabled && strpos($cspValue->directive, 'script-src') === 0) {
$cspValue->value = '{script-hashes} ' . $cspValue->value;
}
// Append the style hashes placeholder
if ($styleHashesEnabled && strpos($cspValue->directive, 'style-src') === 0) {
$cspValue->value = '{style-hashes} ' . $cspValue->value;
}
if ($cspValue->directive === 'frame-ancestors') {
$frameAncestorsSet = true;
}
// Add strict-dynamic to the script-src directive when enabled
if (
$strictDynamicEnabled
&& $cspValue->directive === 'script-src'
&& strpos($cspValue->value, 'strict-dynamic') === false
) {
$cspValue->value = "'strict-dynamic' " . $cspValue->value;
}
$newCspValues[] = trim($cspValue->directive) . ' ' . trim($cspValue->value);
}
}
if ($frameAncestorsSelfEnabled && !$frameAncestorsSet) {
$newCspValues[] = "frame-ancestors 'self'";
}
if (empty($newCspValues)) {
return;
}
$this->getApplication()->setHeader($cspHeader, trim(implode('; ', $newCspValues)));
}
/**
* Get the configured static headers.
*
* @return array We return the array of static headers with its values.
*
* @since 4.0.0
*/
private function getStaticHeaderConfiguration(): array
{
$staticHeaderConfiguration = [];
// X-frame-options
if ($this->params->get('xframeoptions', 1) === 1) {
$staticHeaderConfiguration['x-frame-options#both'] = 'SAMEORIGIN';
}
// Referrer-policy
$referrerPolicy = (string) $this->params->get('referrerpolicy', 'strict-origin-when-cross-origin');
if ($referrerPolicy !== 'disabled') {
$staticHeaderConfiguration['referrer-policy#both'] = $referrerPolicy;
}
// Cross-Origin-Opener-Policy
$coop = (string) $this->params->get('coop', 'same-origin');
if ($coop !== 'disabled') {
$staticHeaderConfiguration['cross-origin-opener-policy#both'] = $coop;
}
// Generate the strict-transport-security header and make sure the site is SSL
if ($this->params->get('hsts', 0) === 1 && Uri::getInstance()->isSsl() === true) {
$hstsOptions = [];
$hstsOptions[] = 'max-age=' . (int) $this->params->get('hsts_maxage', 31536000);
if ($this->params->get('hsts_subdomains', 0) === 1) {
$hstsOptions[] = 'includeSubDomains';
}
if ($this->params->get('hsts_preload', 0) === 1) {
$hstsOptions[] = 'preload';
}
$staticHeaderConfiguration['strict-transport-security#both'] = implode('; ', $hstsOptions);
}
// Generate the additional headers
$additionalHttpHeaders = $this->params->get('additional_httpheader', []);
foreach ($additionalHttpHeaders as $additionalHttpHeader) {
// Make sure we have a key and a value
if (empty($additionalHttpHeader->key) || empty($additionalHttpHeader->value)) {
continue;
}
// Make sure the header is a valid and supported header
if (!\in_array(strtolower($additionalHttpHeader->key), $this->supportedHttpHeaders)) {
continue;
}
// Make sure we do not add one header twice but we support to set a different header per client.
if (
isset($staticHeaderConfiguration[$additionalHttpHeader->key . '#' . $additionalHttpHeader->client])
|| isset($staticHeaderConfiguration[$additionalHttpHeader->key . '#both'])
) {
continue;
}
// Allow the custom csp headers to use the random $cspNonce in the rules
if (\in_array(strtolower($additionalHttpHeader->key), ['content-security-policy', 'content-security-policy-report-only'])) {
$additionalHttpHeader->value = str_replace('{nonce}', "'nonce-" . $this->cspNonce . "'", $additionalHttpHeader->value);
}
$staticHeaderConfiguration[$additionalHttpHeader->key . '#' . $additionalHttpHeader->client] = $additionalHttpHeader->value;
}
return $staticHeaderConfiguration;
}
/**
* Set the static headers when enabled
*
* @return void
*
* @since 4.0.0
*/
private function setStaticHeaders(): void
{
$staticHeaderConfiguration = $this->getStaticHeaderConfiguration();
if (empty($staticHeaderConfiguration)) {
return;
}
foreach ($staticHeaderConfiguration as $headerAndClient => $value) {
$headerAndClient = explode('#', $headerAndClient);
$header = $headerAndClient[0];
$client = $headerAndClient[1] ?? 'both';
if (!$this->getApplication()->isClient($client) && $client != 'both') {
continue;
}
$this->getApplication()->setHeader($header, $value, true);
}
}
}

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_jooa11y</name>
<author>Joomla! Project</author>
<creationDate>2022-02</creationDate>
<copyright>(C) 2021 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>4.2.0</version>
<description>PLG_SYSTEM_JOOA11Y_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Jooa11y</namespace>
<files>
<folder plugin="jooa11y">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_jooa11y.ini</language>
<language tag="en-GB">language/en-GB/plg_system_jooa11y.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="showAlways"
type="radio"
label="PLG_SYSTEM_JOOA11Y_FIELD_SHOW_ALWAYS"
description="PLG_SYSTEM_JOOA11Y_FIELD_SHOW_ALWAYS_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
>
<option value="0">JOFF</option>
<option value="1">JON</option>
</field>
<field
name="checkRoot"
type="text"
label="PLG_SYSTEM_JOOA11Y_FIELD_CHECK_ROOT"
description="PLG_SYSTEM_JOOA11Y_FIELD_CHECK_ROOT_DESC"
default="main"
filter="string"
/>
<field
name="readabilityRoot"
type="text"
label="PLG_SYSTEM_JOOA11Y_FIELD_READABILITY_ROOT"
description="PLG_SYSTEM_JOOA11Y_FIELD_READABILITY_ROOT_DESC"
default="main"
filter="string"
/>
<field
name="containerIgnore"
type="text"
label="PLG_SYSTEM_JOOA11Y_FIELD_CONTAINER_IGNORE"
description="PLG_SYSTEM_JOOA11Y_FIELD_CONTAINER_IGNORE_DESC"
filter="string"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.jooa11y
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Jooa11y\Extension\Jooa11y;
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) {
$plugin = new Jooa11y(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'jooa11y')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,259 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.jooa11y
*
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Jooa11y\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Jooa11y plugin to add an accessibility checker
*
* @since 4.1.0
*/
final class Jooa11y extends CMSPlugin implements SubscriberInterface
{
/**
* Subscribe to certain events
*
* @return string[] An array of event mappings
*
* @since 4.1.0
*
* @throws Exception
*/
public static function getSubscribedEvents(): array
{
return ['onBeforeCompileHead' => 'initJooa11y'];
}
/**
* Method to check if the current user is allowed to see the debug information or not.
*
* @return boolean True if access is allowed.
*
* @since 4.1.0
*/
private function isAuthorisedDisplayChecker(): bool
{
static $result;
if (\is_bool($result)) {
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 $result;
}
}
$result = true;
return $result;
}
/**
* Add the checker.
*
* @return void
*
* @since 4.1.0
*/
public function initJooa11y()
{
if (!$this->getApplication()->isClient('site')) {
return;
}
// Check if we are in a preview modal or the plugin has enforced loading
$showJooa11y = $this->getApplication()->getInput()->get('jooa11y', $this->params->get('showAlways', 0));
// Load the checker if authorised
if (!$showJooa11y || !$this->isAuthorisedDisplayChecker()) {
return;
}
// Load translations
$this->loadLanguage();
// Get the document object.
$document = $this->getApplication()->getDocument();
// Add plugin settings from the xml
$document->addScriptOptions(
'jooa11yOptions',
[
'checkRoot' => $this->params->get('checkRoot', 'main'),
'readabilityRoot' => $this->params->get('readabilityRoot', 'main'),
'containerIgnore' => $this->params->get('containerIgnore'),
]
);
// Add the language constants
$constants = [
'PLG_SYSTEM_JOOA11Y_ALERT_CLOSE',
'PLG_SYSTEM_JOOA11Y_ALERT_TEXT',
'PLG_SYSTEM_JOOA11Y_AVG_WORD_PER_SENTENCE',
'PLG_SYSTEM_JOOA11Y_COMPLEX_WORDS',
'PLG_SYSTEM_JOOA11Y_CONTAINER_LABEL',
'PLG_SYSTEM_JOOA11Y_CONTRAST',
'PLG_SYSTEM_JOOA11Y_CONTRAST_ERROR_INPUT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_CONTRAST_ERROR_INPUT_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_CONTRAST_ERROR_MESSAGE',
'PLG_SYSTEM_JOOA11Y_CONTRAST_ERROR_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_CONTRAST_WARNING_MESSAGE',
'PLG_SYSTEM_JOOA11Y_CONTRAST_WARNING_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_DARK_MODE',
'PLG_SYSTEM_JOOA11Y_DIFFICULT_READABILITY',
'PLG_SYSTEM_JOOA11Y_EMBED_AUDIO',
'PLG_SYSTEM_JOOA11Y_EMBED_GENERAL_WARNING',
'PLG_SYSTEM_JOOA11Y_EMBED_MISSING_TITLE',
'PLG_SYSTEM_JOOA11Y_EMBED_VIDEO',
'PLG_SYSTEM_JOOA11Y_ERROR',
'PLG_SYSTEM_JOOA11Y_FAIRLY_DIFFICULT_READABILITY',
'PLG_SYSTEM_JOOA11Y_FILE_TYPE_WARNING',
'PLG_SYSTEM_JOOA11Y_FILE_TYPE_WARNING_TIP',
'PLG_SYSTEM_JOOA11Y_FORM_LABELS',
'PLG_SYSTEM_JOOA11Y_GOOD',
'PLG_SYSTEM_JOOA11Y_GOOD_READABILITY',
'PLG_SYSTEM_JOOA11Y_HEADING_EMPTY',
'PLG_SYSTEM_JOOA11Y_HEADING_EMPTY_WITH_IMAGE',
'PLG_SYSTEM_JOOA11Y_HEADING_FIRST',
'PLG_SYSTEM_JOOA11Y_HEADING_LONG',
'PLG_SYSTEM_JOOA11Y_HEADING_LONG_INFO',
'PLG_SYSTEM_JOOA11Y_HEADING_MISSING_ONE',
'PLG_SYSTEM_JOOA11Y_HEADING_NON_CONSECUTIVE_LEVEL',
'PLG_SYSTEM_JOOA11Y_HIDE_OUTLINE',
'PLG_SYSTEM_JOOA11Y_HIDE_SETTINGS',
'PLG_SYSTEM_JOOA11Y_HYPERLINK_ALT_LENGTH_MESSAGE',
'PLG_SYSTEM_JOOA11Y_HYPERLINK_ALT_LENGTH_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_IMAGE_FIGURE_DECORATIVE',
'PLG_SYSTEM_JOOA11Y_IMAGE_FIGURE_DECORATIVE_INFO',
'PLG_SYSTEM_JOOA11Y_IMAGE_FIGURE_DUPLICATE_ALT',
'PLG_SYSTEM_JOOA11Y_LABELS_ARIA_LABEL_INPUT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LABELS_ARIA_LABEL_INPUT_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LABELS_INPUT_RESET_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LABELS_INPUT_RESET_MESSAGE_TIP',
'PLG_SYSTEM_JOOA11Y_LABELS_MISSING_IMAGE_INPUT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LABELS_MISSING_LABEL_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LABELS_NO_FOR_ATTRIBUTE_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LABELS_NO_FOR_ATTRIBUTE_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LANG_CODE',
'PLG_SYSTEM_JOOA11Y_LINKS_ADVANCED',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_HAS_BAD_WORD_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_HAS_BAD_WORD_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_HAS_SUS_WORD_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_HAS_SUS_WORD_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_PLACEHOLDER_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_TOO_LONG_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_ALT_TOO_LONG_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_ANCHOR_LINK_AND_ALT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_ANCHOR_LINK_AND_ALT_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_BEST_PRACTICES',
'PLG_SYSTEM_JOOA11Y_LINK_BEST_PRACTICES_DETAILS',
'PLG_SYSTEM_JOOA11Y_LINK_DECORATIVE_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_EMPTY',
'PLG_SYSTEM_JOOA11Y_LINK_EMPTY_LINK_NO_LABEL',
'PLG_SYSTEM_JOOA11Y_LINK_HYPERLINKED_IMAGE_ARIA_HIDDEN',
'PLG_SYSTEM_JOOA11Y_LINK_IDENTICAL_NAME',
'PLG_SYSTEM_JOOA11Y_LINK_IDENTICAL_NAME_TIP',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_BAD_ALT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_BAD_ALT_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_LINK_ALT_TEXT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_LINK_ALT_TEXT_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_LINK_NULL_ALT_NO_TEXT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_PLACEHOLDER_ALT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_SUS_ALT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_IMAGE_SUS_ALT_MESSAGE_INFO',
'PLG_SYSTEM_JOOA11Y_LINK_LABEL',
'PLG_SYSTEM_JOOA11Y_LINK_LINK_HAS_ALT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_LINK_PASS_ALT',
'PLG_SYSTEM_JOOA11Y_LINK_STOPWORD',
'PLG_SYSTEM_JOOA11Y_LINK_STOPWORD_TIP',
'PLG_SYSTEM_JOOA11Y_LINK_URL',
'PLG_SYSTEM_JOOA11Y_LINK_URL_TIP',
'PLG_SYSTEM_JOOA11Y_MAIN_TOGGLE_LABEL',
'PLG_SYSTEM_JOOA11Y_MISSING_ALT_LINK_BUT_HAS_TEXT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_MISSING_ALT_LINK_MESSAGE',
'PLG_SYSTEM_JOOA11Y_MISSING_ALT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_NEW_TAB_WARNING',
'PLG_SYSTEM_JOOA11Y_NEW_TAB_WARNING_TIP',
'PLG_SYSTEM_JOOA11Y_OFF',
'PLG_SYSTEM_JOOA11Y_ON',
'PLG_SYSTEM_JOOA11Y_PAGE_OUTLINE',
'PLG_SYSTEM_JOOA11Y_PANEL_HEADING_MISSING_ONE',
'PLG_SYSTEM_JOOA11Y_PANEL_STATUS_BOTH',
'PLG_SYSTEM_JOOA11Y_PANEL_STATUS_ERRORS',
'PLG_SYSTEM_JOOA11Y_PANEL_STATUS_HIDDEN',
'PLG_SYSTEM_JOOA11Y_PANEL_STATUS_ICON',
'PLG_SYSTEM_JOOA11Y_PANEL_STATUS_NONE',
'PLG_SYSTEM_JOOA11Y_PANEL_STATUS_WARNINGS',
'PLG_SYSTEM_JOOA11Y_QA_BAD_ITALICS',
'PLG_SYSTEM_JOOA11Y_QA_BAD_LINK',
'PLG_SYSTEM_JOOA11Y_QA_BLOCKQUOTE_MESSAGE',
'PLG_SYSTEM_JOOA11Y_QA_BLOCKQUOTE_MESSAGE_TIP',
'PLG_SYSTEM_JOOA11Y_QA_DUPLICATE_ID',
'PLG_SYSTEM_JOOA11Y_QA_DUPLICATE_ID_TIP',
'PLG_SYSTEM_JOOA11Y_QA_FAKE_HEADING',
'PLG_SYSTEM_JOOA11Y_QA_FAKE_HEADING_INFO',
'PLG_SYSTEM_JOOA11Y_QA_PAGE_LANGUAGE_MESSAGE',
'PLG_SYSTEM_JOOA11Y_QA_PDF_COUNT',
'PLG_SYSTEM_JOOA11Y_QA_SHOULD_BE_LIST',
'PLG_SYSTEM_JOOA11Y_QA_SHOULD_BE_LIST_TIP',
'PLG_SYSTEM_JOOA11Y_QA_UPPERCASE_WARNING',
'PLG_SYSTEM_JOOA11Y_READABILITY',
'PLG_SYSTEM_JOOA11Y_READABILITY_NOT_ENOUGH_CONTENT_MESSAGE',
'PLG_SYSTEM_JOOA11Y_READABILITY_NO_P_OR_LI_MESSAGE',
'PLG_SYSTEM_JOOA11Y_SETTINGS',
'PLG_SYSTEM_JOOA11Y_SHORTCUT_SR',
'PLG_SYSTEM_JOOA11Y_SHORTCUT_TOOLTIP',
'PLG_SYSTEM_JOOA11Y_SHOW_OUTLINE',
'PLG_SYSTEM_JOOA11Y_SHOW_SETTINGS',
'PLG_SYSTEM_JOOA11Y_TABLES_EMPTY_HEADING',
'PLG_SYSTEM_JOOA11Y_TABLES_EMPTY_HEADING_INFO',
'PLG_SYSTEM_JOOA11Y_TABLES_MISSING_HEADINGS',
'PLG_SYSTEM_JOOA11Y_TABLES_MISSING_HEADINGS_INFO',
'PLG_SYSTEM_JOOA11Y_TABLES_SEMANTIC_HEADING',
'PLG_SYSTEM_JOOA11Y_TABLES_SEMANTIC_HEADING_INFO',
'PLG_SYSTEM_JOOA11Y_TEXT_UNDERLINE_WARNING',
'PLG_SYSTEM_JOOA11Y_TEXT_UNDERLINE_WARNING_TIP',
'PLG_SYSTEM_JOOA11Y_TOTAL_WORDS',
'PLG_SYSTEM_JOOA11Y_VERY_DIFFICULT_READABILITY',
'PLG_SYSTEM_JOOA11Y_WARNING',
];
foreach ($constants as $constant) {
Text::script($constant);
}
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa*/
$wa = $document->getWebAssetManager();
$wa->getRegistry()->addRegistryFile('media/plg_system_jooa11y/joomla.asset.json');
$wa->useScript('plg_system_jooa11y.jooa11y')
->useStyle('plg_system_jooa11y.jooa11y');
return true;
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_languagecode</name>
<author>Joomla! Project</author>
<creationDate>2011-11</creationDate>
<copyright>(C) 2011 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_SYSTEM_LANGUAGECODE_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\LanguageCode</namespace>
<files>
<folder plugin="languagecode">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_languagecode.ini</language>
<language tag="en-GB">language/en-GB/plg_system_languagecode.sys.ini</language>
</languages>
<config>
<fields name="params">
<field
name="languagecodeplugin"
type="hidden"
default="true"
/>
</fields>
</config>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.languagecode
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\LanguageCode\Extension\LanguageCode;
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) {
$plugin = new LanguageCode(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'languagecode')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,151 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.languagecode
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\LanguageCode\Extension;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Language Code plugin class.
*
* @since 2.5
*/
final class LanguageCode extends CMSPlugin
{
/**
* Plugin that changes the language code used in the <html /> tag.
*
* @return void
*
* @since 2.5
*/
public function onAfterRender()
{
// Use this plugin only in site application.
if ($this->getApplication()->isClient('site')) {
// Get the response body.
$body = $this->getApplication()->getBody();
// Get the current language code.
$code = $this->getApplication()->getDocument()->getLanguage();
// Get the new code.
$new_code = $this->params->get($code);
// Replace the old code by the new code in the <html /> tag.
if ($new_code) {
// Replace the new code in the HTML document.
$patterns = [
\chr(1) . '(<html.*\s+xml:lang=")(' . $code . ')(".*>)' . \chr(1) . 'i',
\chr(1) . '(<html.*\s+lang=")(' . $code . ')(".*>)' . \chr(1) . 'i',
];
$replace = [
'${1}' . strtolower($new_code) . '${3}',
'${1}' . strtolower($new_code) . '${3}',
];
} else {
$patterns = [];
$replace = [];
}
// Replace codes in <link hreflang="" /> attributes.
preg_match_all(\chr(1) . '(<link.*\s+hreflang=")([0-9a-z\-]*)(".*\s+rel="alternate".*>)' . \chr(1) . 'i', $body, $matches);
foreach ($matches[2] as $match) {
$new_code = $this->params->get(strtolower($match));
if ($new_code) {
$patterns[] = \chr(1) . '(<link.*\s+hreflang=")(' . $match . ')(".*\s+rel="alternate".*>)' . \chr(1) . 'i';
$replace[] = '${1}' . $new_code . '${3}';
}
}
preg_match_all(\chr(1) . '(<link.*\s+rel="alternate".*\s+hreflang=")([0-9A-Za-z\-]*)(".*>)' . \chr(1) . 'i', $body, $matches);
foreach ($matches[2] as $match) {
$new_code = $this->params->get(strtolower($match));
if ($new_code) {
$patterns[] = \chr(1) . '(<link.*\s+rel="alternate".*\s+hreflang=")(' . $match . ')(".*>)' . \chr(1) . 'i';
$replace[] = '${1}' . $new_code . '${3}';
}
}
// Replace codes in itemprop content
preg_match_all(\chr(1) . '(<meta.*\s+itemprop="inLanguage".*\s+content=")([0-9A-Za-z\-]*)(".*>)' . \chr(1) . 'i', $body, $matches);
foreach ($matches[2] as $match) {
$new_code = $this->params->get(strtolower($match));
if ($new_code) {
$patterns[] = \chr(1) . '(<meta.*\s+itemprop="inLanguage".*\s+content=")(' . $match . ')(".*>)' . \chr(1) . 'i';
$replace[] = '${1}' . $new_code . '${3}';
}
}
$this->getApplication()->setBody(preg_replace($patterns, $replace, $body));
}
}
/**
* Prepare form.
*
* @param Form $form The form to be altered.
* @param mixed $data The associated data for the form.
*
* @return boolean
*
* @since 2.5
*/
public function onContentPrepareForm(Form $form, $data)
{
// Check we are manipulating the languagecode plugin.
if ($form->getName() !== 'com_plugins.plugin' || !$form->getField('languagecodeplugin', 'params')) {
return true;
}
// Get site languages.
if ($languages = LanguageHelper::getKnownLanguages(JPATH_SITE)) {
// Inject fields into the form.
foreach ($languages as $tag => $language) {
$form->load('
<form>
<fields name="params">
<fieldset
name="languagecode"
label="PLG_SYSTEM_LANGUAGECODE_FIELDSET_LABEL"
description="PLG_SYSTEM_LANGUAGECODE_FIELDSET_DESC"
>
<field
name="' . strtolower($tag) . '"
type="text"
label="' . $tag . '"
description="' . htmlspecialchars(Text::sprintf('PLG_SYSTEM_LANGUAGECODE_FIELD_DESC', $language['name']), ENT_COMPAT, 'UTF-8') . '"
translate_description="false"
translate_label="false"
size="7"
filter="cmd"
/>
</fieldset>
</fields>
</form>');
}
}
return true;
}
}

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_languagefilter</name>
<author>Joomla! Project</author>
<creationDate>2010-07</creationDate>
<copyright>(C) 2010 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_SYSTEM_LANGUAGEFILTER_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\LanguageFilter</namespace>
<files>
<folder plugin="languagefilter">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_languagefilter.ini</language>
<language tag="en-GB">language/en-GB/plg_system_languagefilter.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="detect_browser"
type="list"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_DETECT_BROWSER_LABEL"
default="0"
filter="integer"
validate="options"
>
<option value="0">PLG_SYSTEM_LANGUAGEFILTER_SITE_LANGUAGE</option>
<option value="1">PLG_SYSTEM_LANGUAGEFILTER_BROWSER_SETTINGS</option>
</field>
<field
name="automatic_change"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_AUTOMATIC_CHANGE_LABEL"
default="1"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="item_associations"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_ITEM_ASSOCIATIONS_LABEL"
default="1"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="alternate_meta"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_ALTERNATE_META_LABEL"
default="1"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="xdefault"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_XDEFAULT_LABEL"
default="1"
filter="integer"
showon="alternate_meta:1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="xdefault_language"
type="contentlanguage"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_XDEFAULT_LANGUAGE_LABEL"
default="default"
showon="alternate_meta:1[AND]xdefault:1"
>
<option value="default">PLG_SYSTEM_LANGUAGEFILTER_OPTION_DEFAULT_LANGUAGE</option>
</field>
<field
name="remove_default_prefix"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_REMOVE_DEFAULT_PREFIX_LABEL"
default="0"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="lang_cookie"
type="list"
label="PLG_SYSTEM_LANGUAGEFILTER_FIELD_COOKIE_LABEL"
default="0"
filter="integer"
validate="options"
>
<option value="1">PLG_SYSTEM_LANGUAGEFILTER_OPTION_YEAR</option>
<option value="0">PLG_SYSTEM_LANGUAGEFILTER_OPTION_SESSION</option>
</field>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,50 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.languagefilter
*
* @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\Language\LanguageFactoryInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\SiteRouter;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\LanguageFilter\Extension\LanguageFilter;
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) {
$plugin = new LanguageFilter(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'languagefilter'),
Factory::getApplication(),
$container->get(LanguageFactoryInterface::class)
);
$plugin->setSiteRouter($container->get(SiteRouter::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,856 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.languagefilter
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\LanguageFilter\Extension;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Association\AssociationServiceInterface;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\LanguageFactoryInterface;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Router\Router;
use Joomla\CMS\Router\SiteRouterAwareTrait;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Menus\Administrator\Helper\MenusHelper;
use Joomla\Event\DispatcherInterface;
use Joomla\Filesystem\Path;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! Language Filter Plugin.
*
* @since 1.6
*/
final class LanguageFilter extends CMSPlugin
{
use SiteRouterAwareTrait;
/**
* The routing mode.
*
* @var boolean
* @since 2.5
*/
protected $mode_sef;
/**
* Available languages by sef.
*
* @var array
* @since 1.6
*/
protected $sefs;
/**
* Available languages by language codes.
*
* @var array
* @since 2.5
*/
protected $lang_codes;
/**
* The current language code.
*
* @var string
* @since 3.4.2
*/
protected $current_lang;
/**
* The default language code.
*
* @var string
* @since 2.5
*/
protected $default_lang;
/**
* The logged user language code.
*
* @var string
* @since 3.3.1
*/
private $user_lang_code;
/**
* The language factory
*
* @var LanguageFactoryInterface
*
* @since 4.4.0
*/
private $languageFactory;
/**
* Constructor.
*
* @param DispatcherInterface $dispatcher The dispatcher
* @param array $config An optional associative array of configuration settings
* @param CMSApplicationInterface $app The language factory
* @param LanguageFactoryInterface $languageFactory The language factory
*
* @since 1.6.0
*/
public function __construct(
DispatcherInterface $dispatcher,
array $config,
CMSApplicationInterface $app,
LanguageFactoryInterface $languageFactory
) {
parent::__construct($dispatcher, $config);
$this->languageFactory = $languageFactory;
$this->setApplication($app);
// Setup language data.
$this->mode_sef = $this->getApplication()->get('sef', 0);
$this->sefs = LanguageHelper::getLanguages('sef');
$this->lang_codes = LanguageHelper::getLanguages('lang_code');
$this->default_lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB');
// If language filter plugin is executed in a site page.
if ($this->getApplication()->isClient('site')) {
$levels = $this->getApplication()->getIdentity()->getAuthorisedViewLevels();
foreach ($this->sefs as $sef => $language) {
// @todo: In Joomla 2.5.4 and earlier access wasn't set. Non modified Content Languages got 0 as access value
// we also check if frontend language exists and is enabled
if (
($language->access && !\in_array($language->access, $levels))
|| (!\array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0)))
) {
unset($this->lang_codes[$language->lang_code], $this->sefs[$language->sef]);
}
}
} else {
// If language filter plugin is executed in an admin page (ex: Route site).
// Set current language to default site language, fallback to en-GB if there is no content language for the default site language.
$this->current_lang = isset($this->lang_codes[$this->default_lang]) ? $this->default_lang : 'en-GB';
foreach ($this->sefs as $sef => $language) {
if (!\array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) {
unset($this->lang_codes[$language->lang_code]);
unset($this->sefs[$language->sef]);
}
}
}
}
/**
* After initialise.
*
* @return void
*
* @since 1.6
*/
public function onAfterInitialise()
{
$router = $this->getSiteRouter();
// Attach build rules for language SEF.
$router->attachBuildRule([$this, 'preprocessBuildRule'], Router::PROCESS_BEFORE);
if ($this->mode_sef) {
$router->attachBuildRule([$this, 'buildRule'], Router::PROCESS_BEFORE);
$router->attachBuildRule([$this, 'postprocessSEFBuildRule'], Router::PROCESS_AFTER);
} else {
$router->attachBuildRule([$this, 'postprocessNonSEFBuildRule'], Router::PROCESS_AFTER);
}
// Attach parse rule.
$router->attachParseRule([$this, 'parseRule'], Router::PROCESS_BEFORE);
}
/**
* After route.
*
* @return void
*
* @since 3.4
*/
public function onAfterRoute()
{
// Add custom site name.
if ($this->getApplication()->isClient('site') && isset($this->lang_codes[$this->current_lang]) && $this->lang_codes[$this->current_lang]->sitename) {
$this->getApplication()->set('sitename', $this->lang_codes[$this->current_lang]->sitename);
}
}
/**
* Add build preprocess rule to router.
*
* @param Router &$router Router object.
* @param Uri &$uri Uri object.
*
* @return void
*
* @since 3.4
*/
public function preprocessBuildRule(&$router, &$uri)
{
$lang = $uri->getVar('lang', $this->current_lang);
if (isset($this->sefs[$lang])) {
$lang = $this->sefs[$lang]->lang_code;
}
$uri->setVar('lang', $lang);
}
/**
* Add build rule to router.
*
* @param Router &$router Router object.
* @param Uri &$uri Uri object.
*
* @return void
*
* @since 1.6
*/
public function buildRule(&$router, &$uri)
{
$lang = $uri->getVar('lang');
if (isset($this->lang_codes[$lang])) {
$sef = $this->lang_codes[$lang]->sef;
} else {
$sef = $this->lang_codes[$this->current_lang]->sef;
}
if (
!$this->params->get('remove_default_prefix', 0)
|| $lang !== $this->default_lang
|| $lang !== $this->current_lang
) {
$uri->setPath($uri->getPath() . '/' . $sef . '/');
}
}
/**
* postprocess build rule for SEF URLs
*
* @param Router &$router Router object.
* @param Uri &$uri Uri object.
*
* @return void
*
* @since 3.4
*/
public function postprocessSEFBuildRule(&$router, &$uri)
{
$uri->delVar('lang');
}
/**
* postprocess build rule for non-SEF URLs
*
* @param Router &$router Router object.
* @param Uri &$uri Uri object.
*
* @return void
*
* @since 3.4
*/
public function postprocessNonSEFBuildRule(&$router, &$uri)
{
$lang = $uri->getVar('lang');
if (isset($this->lang_codes[$lang])) {
$uri->setVar('lang', $this->lang_codes[$lang]->sef);
}
}
/**
* Add parse rule to router.
*
* @param Router &$router Router object.
* @param Uri &$uri Uri object.
*
* @return void
*
* @since 1.6
*/
public function parseRule(&$router, &$uri)
{
// Did we find the current and existing language yet?
$found = false;
// Are we in SEF mode or not?
if ($this->mode_sef) {
$path = $uri->getPath();
$parts = explode('/', $path);
$sef = StringHelper::strtolower($parts[0]);
// Do we have a URL Language Code ?
if (!isset($this->sefs[$sef])) {
// Check if remove default URL language code is set
if ($this->params->get('remove_default_prefix', 0)) {
if ($parts[0]) {
// We load a default site language page
$lang_code = $this->default_lang;
} else {
// We check for an existing language cookie
$lang_code = $this->getLanguageCookie();
}
} else {
$lang_code = $this->getLanguageCookie();
}
// No language code. Try using browser settings or default site language
if (!$lang_code && $this->params->get('detect_browser', 0) == 1) {
$lang_code = LanguageHelper::detectLanguage();
}
if (!$lang_code) {
$lang_code = $this->default_lang;
}
if ($lang_code === $this->default_lang && $this->params->get('remove_default_prefix', 0)) {
$found = true;
}
} else {
// We found our language
$found = true;
$lang_code = $this->sefs[$sef]->lang_code;
// If we found our language, but it's the default language and we don't want a prefix for that, we are on a wrong URL.
// Or we try to change the language back to the default language. We need a redirect to the proper URL for the default language.
if ($lang_code === $this->default_lang && $this->params->get('remove_default_prefix', 0)) {
// Create a cookie.
$this->setLanguageCookie($lang_code);
$found = false;
array_shift($parts);
$path = implode('/', $parts);
}
// We have found our language and the first part of our URL is the language prefix
if ($found) {
array_shift($parts);
// Empty parts array when "index.php" is the only part left.
if (\count($parts) === 1 && $parts[0] === 'index.php') {
$parts = [];
}
$uri->setPath(implode('/', $parts));
}
}
} else {
// We are not in SEF mode
$lang_code = $this->getLanguageCookie();
if (!$lang_code && $this->params->get('detect_browser', 1)) {
$lang_code = LanguageHelper::detectLanguage();
}
if (!isset($this->lang_codes[$lang_code])) {
$lang_code = $this->default_lang;
}
}
$lang = $uri->getVar('lang', $lang_code);
if (isset($this->sefs[$lang])) {
// We found our language
$found = true;
$lang_code = $this->sefs[$lang]->lang_code;
}
// We are called via POST or the nolangfilter url parameter was set. We don't care about the language
// and simply set the default language as our current language.
if (
$this->getApplication()->getInput()->getMethod() === 'POST'
|| $this->getApplication()->getInput()->get('nolangfilter', 0) == 1
|| \count($this->getApplication()->getInput()->post) > 0
|| \count($this->getApplication()->getInput()->files) > 0
) {
$found = true;
if (!isset($lang_code)) {
$lang_code = $this->getLanguageCookie();
}
if (!$lang_code && $this->params->get('detect_browser', 1)) {
$lang_code = LanguageHelper::detectLanguage();
}
if (!isset($this->lang_codes[$lang_code])) {
$lang_code = $this->default_lang;
}
}
// We have not found the language and thus need to redirect
if (!$found) {
// Lets find the default language for this user
if (!isset($lang_code) || !isset($this->lang_codes[$lang_code])) {
$lang_code = false;
if ($this->params->get('detect_browser', 1)) {
$lang_code = LanguageHelper::detectLanguage();
if (!isset($this->lang_codes[$lang_code])) {
$lang_code = false;
}
}
if (!$lang_code) {
$lang_code = $this->default_lang;
}
}
if ($this->mode_sef) {
// Use the current language sef or the default one.
if (
$lang_code !== $this->default_lang
|| !$this->params->get('remove_default_prefix', 0)
) {
$path = $this->lang_codes[$lang_code]->sef . '/' . $path;
}
$uri->setPath($path);
if (!$this->getApplication()->get('sef_rewrite')) {
$uri->setPath('index.php/' . $uri->getPath());
}
$redirectUri = $uri->base() . $uri->toString(['path', 'query', 'fragment']);
} else {
$uri->setVar('lang', $this->lang_codes[$lang_code]->sef);
$redirectUri = $uri->base() . 'index.php?' . $uri->getQuery();
}
// Set redirect HTTP code to "302 Found".
$redirectHttpCode = 302;
// If selected language is the default language redirect code is "301 Moved Permanently".
if ($lang_code === $this->default_lang) {
$redirectHttpCode = 301;
// We cannot cache this redirect in browser. 301 is cacheable by default so we need to force to not cache it in browsers.
$this->getApplication()->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true);
$this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false);
$this->getApplication()->sendHeaders();
}
// Redirect to language.
$this->getApplication()->redirect($redirectUri, $redirectHttpCode);
}
// We have found our language and now need to set the cookie and the language value in our system
$this->current_lang = $lang_code;
// Set the request var.
$this->getApplication()->getInput()->set('language', $lang_code);
$this->getApplication()->set('language', $lang_code);
$language = $this->getApplication()->getLanguage();
if ($language->getTag() !== $lang_code) {
$language_new = $this->languageFactory->createLanguage($lang_code, (bool) $this->getApplication()->get('debug_lang'));
foreach ($language->getPaths() as $extension => $files) {
if (strpos($extension, 'plg_system') !== false) {
$extension_name = substr($extension, 11);
$language_new->load($extension, JPATH_ADMINISTRATOR)
|| $language_new->load($extension, JPATH_PLUGINS . '/system/' . $extension_name);
continue;
}
$language_new->load($extension);
}
Factory::$language = $language_new;
$this->getApplication()->loadLanguage($language_new);
}
// Create a cookie.
if ($this->getLanguageCookie() !== $lang_code) {
$this->setLanguageCookie($lang_code);
}
}
/**
* Reports the privacy related capabilities for this plugin to site administrators.
*
* @return array
*
* @since 3.9.0
*/
public function onPrivacyCollectAdminCapabilities()
{
$this->loadLanguage();
return [
$this->getApplication()->getLanguage()->_('PLG_SYSTEM_LANGUAGEFILTER') => [
$this->getApplication()->getLanguage()->_('PLG_SYSTEM_LANGUAGEFILTER_PRIVACY_CAPABILITY_LANGUAGE_COOKIE'),
],
];
}
/**
* Before store user method.
*
* Method is called before user data is stored in the database.
*
* @param array $user Holds the old user data.
* @param boolean $isnew True if a new user is stored.
* @param array $new Holds the new user data.
*
* @return void
*
* @since 1.6
*/
public function onUserBeforeSave($user, $isnew, $new)
{
if (\array_key_exists('params', $user) && $this->params->get('automatic_change', 1) == 1) {
$registry = new Registry($user['params']);
$this->user_lang_code = $registry->get('language');
if (empty($this->user_lang_code)) {
$this->user_lang_code = $this->current_lang;
}
}
}
/**
* After store user method.
*
* Method is called after user data is stored in the database.
*
* @param array $user Holds the new user data.
* @param boolean $isnew True if a new user is stored.
* @param boolean $success True if user was successfully stored in the database.
* @param string $msg Message.
*
* @return void
*
* @since 1.6
*/
public function onUserAfterSave($user, $isnew, $success, $msg): void
{
if ($success && \array_key_exists('params', $user) && $this->params->get('automatic_change', 1) == 1) {
$registry = new Registry($user['params']);
$lang_code = $registry->get('language');
if (empty($lang_code)) {
$lang_code = $this->current_lang;
}
if ($lang_code === $this->user_lang_code || !isset($this->lang_codes[$lang_code])) {
if ($this->getApplication()->isClient('site')) {
$this->getApplication()->setUserState('com_users.edit.profile.redirect', null);
}
} else {
if ($this->getApplication()->isClient('site')) {
$this->getApplication()->setUserState('com_users.edit.profile.redirect', 'index.php?Itemid='
. $this->getApplication()->getMenu()->getDefault($lang_code)->id . '&lang=' . $this->lang_codes[$lang_code]->sef);
// Create a cookie.
$this->setLanguageCookie($lang_code);
}
}
}
}
/**
* Method to handle any login logic and report back to the subject.
*
* @param array $user Holds the user data.
* @param array $options Array holding options (remember, autoregister, group).
*
* @return null
*
* @since 1.5
*/
public function onUserLogin($user, $options = [])
{
if ($this->getApplication()->isClient('site')) {
$menu = $this->getApplication()->getMenu();
if ($this->params->get('automatic_change', 1)) {
$assoc = Associations::isEnabled();
$lang_code = $user['language'];
// If no language is specified for this user, we set it to the site default language
if (empty($lang_code)) {
$lang_code = $this->default_lang;
}
// The language has been deleted/disabled or the related content language does not exist/has been unpublished
// or the related home page does not exist/has been unpublished
if (
!\array_key_exists($lang_code, $this->lang_codes)
|| !\array_key_exists($lang_code, Multilanguage::getSiteHomePages())
|| !Folder::exists(JPATH_SITE . '/language/' . $lang_code)
) {
$lang_code = $this->current_lang;
}
// Try to get association from the current active menu item
$active = $menu->getActive();
$foundAssociation = false;
/**
* Looking for associations.
* If the login menu item form contains an internal URL redirection,
* This will override the automatic change to the user preferred site language.
* In that case we use the redirect as defined in the menu item.
* Otherwise we redirect, when available, to the user preferred site language.
*/
if ($active && !$active->getParams()->get('login_redirect_url')) {
if ($assoc) {
$associations = MenusHelper::getAssociations($active->id);
}
// Retrieves the Itemid from a login form.
$uri = new Uri($this->getApplication()->getUserState('users.login.form.return'));
if ($uri->getVar('Itemid')) {
// The login form contains a menu item redirection. Try to get associations from that menu item.
// If any association set to the user preferred site language, redirect to that page.
if ($assoc) {
$associations = MenusHelper::getAssociations($uri->getVar('Itemid'));
}
if (isset($associations[$lang_code]) && $menu->getItem($associations[$lang_code])) {
$associationItemid = $associations[$lang_code];
$this->getApplication()->setUserState('users.login.form.return', 'index.php?Itemid=' . $associationItemid);
$foundAssociation = true;
}
} elseif (isset($associations[$lang_code]) && $menu->getItem($associations[$lang_code])) {
/**
* The login form does not contain a menu item redirection.
* The active menu item has associations.
* We redirect to the user preferred site language associated page.
*/
$associationItemid = $associations[$lang_code];
$this->getApplication()->setUserState('users.login.form.return', 'index.php?Itemid=' . $associationItemid);
$foundAssociation = true;
} elseif ($active->home) {
// We are on a Home page, we redirect to the user preferred site language Home page.
$item = $menu->getDefault($lang_code);
if ($item && $item->language !== $active->language && $item->language !== '*') {
$this->getApplication()->setUserState('users.login.form.return', 'index.php?Itemid=' . $item->id);
$foundAssociation = true;
}
}
}
if ($foundAssociation && $lang_code !== $this->current_lang) {
// Change language.
$this->current_lang = $lang_code;
// Create a cookie.
$this->setLanguageCookie($lang_code);
// Change the language code.
$this->languageFactory->createLanguage($lang_code);
}
} else {
if ($this->getApplication()->getUserState('users.login.form.return')) {
$this->getApplication()->setUserState('users.login.form.return', Route::_($this->getApplication()->getUserState('users.login.form.return'), false));
}
}
}
}
/**
* Method to add alternative meta tags for associated menu items.
*
* @return void
*
* @since 1.7
*/
public function onAfterDispatch()
{
$doc = $this->getApplication()->getDocument();
if ($this->getApplication()->isClient('site') && $this->params->get('alternate_meta', 1) && $doc->getType() === 'html') {
$languages = $this->lang_codes;
$homes = Multilanguage::getSiteHomePages();
$menu = $this->getApplication()->getMenu();
$active = $menu->getActive();
$levels = $this->getApplication()->getIdentity()->getAuthorisedViewLevels();
$remove_default_prefix = $this->params->get('remove_default_prefix', 0);
$server = Uri::getInstance()->toString(['scheme', 'host', 'port']);
$is_home = false;
// Router can be injected when turned into a DI built plugin
$currentInternalUrl = 'index.php?' . http_build_query($this->getSiteRouter()->getVars());
if ($active) {
$active_link = Route::_($active->link . '&Itemid=' . $active->id);
$current_link = Route::_($currentInternalUrl);
// Load menu associations
if ($active_link === $current_link) {
$associations = MenusHelper::getAssociations($active->id);
}
// Check if we are on the home page
$is_home = ($active->home
&& ($active_link === $current_link || $active_link === $current_link . 'index.php' || $active_link . '/' === $current_link));
}
// Load component associations.
$option = $this->getApplication()->getInput()->get('option');
$component = $this->getApplication()->bootComponent($option);
if ($component instanceof AssociationServiceInterface) {
$cassociations = $component->getAssociationsExtension()->getAssociationsForItem();
} else {
$cName = ucfirst(substr($option, 4)) . 'HelperAssociation';
\JLoader::register($cName, Path::clean(JPATH_SITE . '/components/' . $option . '/helpers/association.php'));
if (class_exists($cName) && \is_callable([$cName, 'getAssociations'])) {
$cassociations = \call_user_func([$cName, 'getAssociations']);
}
}
// For each language...
foreach ($languages as $i => $language) {
switch (true) {
// Language without frontend UI || Language without specific home menu || Language without authorized access level
case !\array_key_exists($i, LanguageHelper::getInstalledLanguages(0)):
case !isset($homes[$i]):
case isset($language->access) && $language->access && !\in_array($language->access, $levels):
unset($languages[$i]);
break;
// Home page
case $is_home:
$language->link = Route::_('index.php?lang=' . $language->sef . '&Itemid=' . $homes[$i]->id);
break;
// Current language link
case $i === $this->current_lang:
$language->link = Route::_($currentInternalUrl);
break;
// Component association
case isset($cassociations[$i]):
$language->link = Route::_($cassociations[$i]);
break;
// Menu items association
// Heads up! "$item = $menu" here below is an assignment, *NOT* comparison
case isset($associations[$i]) && ($item = $menu->getItem($associations[$i])):
$language->link = Route::_('index.php?Itemid=' . $item->id . '&lang=' . $language->sef);
break;
// Too bad...
default:
unset($languages[$i]);
}
}
// If there are at least 2 of them, add the rel="alternate" links to the <head>
if (\count($languages) > 1) {
// Remove the sef from the default language if "Remove URL Language Code" is on
if ($remove_default_prefix && isset($languages[$this->default_lang])) {
$languages[$this->default_lang]->link
= preg_replace('|/' . $languages[$this->default_lang]->sef . '/|', '/', $languages[$this->default_lang]->link, 1);
}
foreach ($languages as $i => $language) {
$doc->addHeadLink($server . $language->link, 'alternate', 'rel', ['hreflang' => $i]);
}
// Add x-default language tag
if ($this->params->get('xdefault', 1)) {
$xdefault_language = $this->params->get('xdefault_language', $this->default_lang);
$xdefault_language = ($xdefault_language === 'default') ? $this->default_lang : $xdefault_language;
if (isset($languages[$xdefault_language])) {
// Use a custom tag because addHeadLink is limited to one URI per tag
$doc->addCustomTag('<link href="' . $server . $languages[$xdefault_language]->link . '" rel="alternate" hreflang="x-default">');
}
}
}
}
}
/**
* Set the language cookie
*
* @param string $languageCode The language code for which we want to set the cookie
*
* @return void
*
* @since 3.4.2
*/
private function setLanguageCookie($languageCode)
{
// If is set to use language cookie for a year in plugin params, save the user language in a new cookie.
if ((int) $this->params->get('lang_cookie', 0) === 1) {
// Create a cookie with one year lifetime.
$this->getApplication()->getInput()->cookie->set(
ApplicationHelper::getHash('language'),
$languageCode,
[
'expires' => time() + 365 * 86400,
'path' => $this->getApplication()->get('cookie_path', '/'),
'domain' => $this->getApplication()->get('cookie_domain', ''),
'secure' => $this->getApplication()->isHttpsForced(),
'httponly' => true,
]
);
} else {
// If not, set the user language in the session (that is already saved in a cookie).
$this->getApplication()->getSession()->set('plg_system_languagefilter.language', $languageCode);
}
}
/**
* Get the language cookie
*
* @return string
*
* @since 3.4.2
*/
private function getLanguageCookie()
{
// Is is set to use a year language cookie in plugin params, get the user language from the cookie.
if ((int) $this->params->get('lang_cookie', 0) === 1) {
$languageCode = $this->getApplication()->getInput()->cookie->get(ApplicationHelper::getHash('language'));
} else {
// Else get the user language from the session.
$languageCode = $this->getApplication()->getSession()->get('plg_system_languagefilter.language');
}
// Let's be sure we got a valid language code. Fallback to null.
if (!\array_key_exists($languageCode, $this->lang_codes)) {
$languageCode = null;
}
return $languageCode;
}
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_log</name>
<author>Joomla! Project</author>
<creationDate>2007-04</creationDate>
<copyright>(C) 2007 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_LOG_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Log</namespace>
<files>
<folder plugin="log">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_log.ini</language>
<language tag="en-GB">language/en-GB/plg_system_log.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="log_username"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_LOG_FIELD_LOG_USERNAME_LABEL"
default="0"
filter="integer"
>
<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.log
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Log\Extension\Log;
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) {
$plugin = new Log(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'log')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,72 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.log
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Log\Extension;
use Joomla\CMS\Authentication\Authentication;
use Joomla\CMS\Log\Log as Logger;
use Joomla\CMS\Plugin\CMSPlugin;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! System Logging Plugin.
*
* @since 1.5
*/
final class Log extends CMSPlugin
{
/**
* Called if user fails to be logged in.
*
* @param array $response Array of response data.
*
* @return void
*
* @since 1.5
*/
public function onUserLoginFailure($response)
{
$errorlog = [];
switch ($response['status']) {
case Authentication::STATUS_SUCCESS:
$errorlog['status'] = $response['type'] . ' CANCELED: ';
$errorlog['comment'] = $response['error_message'];
break;
case Authentication::STATUS_FAILURE:
$errorlog['status'] = $response['type'] . ' FAILURE: ';
if ($this->params->get('log_username', 0)) {
$errorlog['comment'] = $response['error_message'] . ' ("' . $response['username'] . '")';
} else {
$errorlog['comment'] = $response['error_message'];
}
break;
default:
$errorlog['status'] = $response['type'] . ' UNKNOWN ERROR: ';
$errorlog['comment'] = $response['error_message'];
break;
}
Logger::addLogger([], Logger::INFO);
try {
Logger::add($errorlog['comment'], Logger::INFO, $errorlog['status']);
} catch (\Exception $e) {
// If the log file is unwriteable during login then we should not go to the error page
return;
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_logout</name>
<author>Joomla! Project</author>
<creationDate>2009-04</creationDate>
<copyright>(C) 2009 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_SYSTEM_LOGOUT_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Logout</namespace>
<files>
<folder plugin="logout">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_logout.ini</language>
<language tag="en-GB">language/en-GB/plg_system_logout.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,44 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.logout
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Logout\Extension\Logout;
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 Logout(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'logout'),
Factory::getApplication()
);
}
);
}
};

View File

@ -0,0 +1,88 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.logout
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Logout\Extension;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\DispatcherInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Plugin class for logout redirect handling.
*
* @since 1.6
*/
final class Logout extends CMSPlugin
{
/**
* @param DispatcherInterface $dispatcher The object to observe -- event dispatcher.
* @param array $config An optional associative array of configuration settings.
* @param CMSApplicationInterface $app The object to observe -- event dispatcher.
*
* @since 1.6
*/
public function __construct(DispatcherInterface $dispatcher, array $config, CMSApplicationInterface $app)
{
parent::__construct($dispatcher, $config);
$this->setApplication($app);
// If we are on admin don't process.
if (!$this->getApplication()->isClient('site')) {
return;
}
$hash = ApplicationHelper::getHash('PlgSystemLogout');
if ($this->getApplication()->getInput()->cookie->getString($hash)) {
// Destroy the cookie.
$this->getApplication()->getInput()->cookie->set(
$hash,
'',
1,
$this->getApplication()->get('cookie_path', '/'),
$this->getApplication()->get('cookie_domain', '')
);
}
}
/**
* Method to handle any logout logic and report back to the subject.
*
* @param array $user Holds the user data.
* @param array $options Array holding options (client, ...).
*
* @return boolean Always returns true.
*
* @since 1.6
*/
public function onUserLogout($user, $options = [])
{
if ($this->getApplication()->isClient('site')) {
// Create the cookie.
$this->getApplication()->getInput()->cookie->set(
ApplicationHelper::getHash('PlgSystemLogout'),
true,
time() + 86400,
$this->getApplication()->get('cookie_path', '/'),
$this->getApplication()->get('cookie_domain', ''),
$this->getApplication()->isHttpsForced(),
true
);
}
return true;
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="privacyconsent">
<fieldset
name="privacyconsent"
label="PLG_SYSTEM_PRIVACYCONSENT_LABEL"
>
<field
name="privacy"
type="privacy"
label="PLG_SYSTEM_PRIVACYCONSENT_FIELD_LABEL"
default="0"
filter="integer"
required="true"
>
<option value="1">PLG_SYSTEM_PRIVACYCONSENT_OPTION_AGREE</option>
<option value="0">PLG_SYSTEM_PRIVACYCONSENT_OPTION_DO_NOT_AGREE</option>
</field>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_privacyconsent</name>
<author>Joomla! Project</author>
<creationDate>2018-04</creationDate>
<copyright>(C) 2018 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.9.0</version>
<description>PLG_SYSTEM_PRIVACYCONSENT_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\PrivacyConsent</namespace>
<files>
<folder>forms</folder>
<folder plugin="privacyconsent">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_privacyconsent.ini</language>
<language tag="en-GB">language/en-GB/plg_system_privacyconsent.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic" addfieldprefix="Joomla\Component\Content\Administrator\Field">
<field
name="privacy_note"
type="textarea"
label="PLG_SYSTEM_PRIVACYCONSENT_NOTE_FIELD_LABEL"
description="PLG_SYSTEM_PRIVACYCONSENT_NOTE_FIELD_DESC"
hint="PLG_SYSTEM_PRIVACYCONSENT_NOTE_FIELD_DEFAULT"
rows="7"
cols="20"
filter="html"
/>
<field
name="privacy_type"
type="list"
label="PLG_SYSTEM_PRIVACYCONSENT_FIELD_TYPE_LABEL"
default="article"
validate="options"
>
<option value="article">PLG_SYSTEM_PRIVACYCONSENT_FIELD_TYPE_ARTICLE</option>
<option value="menu_item">PLG_SYSTEM_PRIVACYCONSENT_FIELD_TYPE_MENU_ITEM</option>
</field>
<field
name="privacy_article"
type="modal_article"
label="PLG_SYSTEM_PRIVACYCONSENT_FIELD_ARTICLE_LABEL"
description="PLG_SYSTEM_PRIVACYCONSENT_FIELD_ARTICLE_DESC"
select="true"
new="true"
edit="true"
clear="true"
filter="integer"
showon="privacy_type:article"
/>
<field
addfieldprefix="Joomla\Component\Menus\Administrator\Field"
name="privacy_menu_item"
type="modal_menu"
label="PLG_SYSTEM_PRIVACYCONSENT_FIELD_MENU_ITEM_LABEL"
select="true"
new="true"
edit="true"
clear="true"
filter="integer"
showon="privacy_type:menu_item"
/>
<field
name="messageOnRedirect"
type="textarea"
label="PLG_SYSTEM_PRIVACYCONSENT_REDIRECT_MESSAGE_LABEL"
description="PLG_SYSTEM_PRIVACYCONSENT_REDIRECT_MESSAGE_DESC"
hint="PLG_SYSTEM_PRIVACYCONSENT_REDIRECT_MESSAGE_DEFAULT"
class="span12"
rows="7"
cols="20"
filter="html"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,48 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.privacyconsent
*
* @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\PrivacyConsent\Extension\PrivacyConsent;
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) {
$plugin = new PrivacyConsent(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'privacyconsent')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,445 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.privacyconsent
*
* @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\PrivacyConsent\Extension;
use Joomla\CMS\Event\Privacy\CheckPrivacyPolicyPublishedEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* An example custom privacyconsent plugin.
*
* @since 3.9.0
*/
final class PrivacyConsent extends CMSPlugin
{
use DatabaseAwareTrait;
/**
* Adds additional fields to the user editing form
*
* @param Form $form The form to be altered.
* @param mixed $data The associated data for the form.
*
* @return boolean
*
* @since 3.9.0
*/
public function onContentPrepareForm(Form $form, $data)
{
// Check we are manipulating a valid form - we only display this on user registration form and user profile form.
$name = $form->getName();
if (!\in_array($name, ['com_users.profile', 'com_users.registration'])) {
return true;
}
// Load plugin language files
$this->loadLanguage();
// We only display this if user has not consented before
if (\is_object($data)) {
$userId = $data->id ?? 0;
if ($userId > 0 && $this->isUserConsented($userId)) {
return true;
}
}
// Add the privacy policy fields to the form.
FormHelper::addFieldPrefix('Joomla\\Plugin\\System\\PrivacyConsent\\Field');
FormHelper::addFormPath(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms');
$form->loadFile('privacyconsent');
$privacyType = $this->params->get('privacy_type', 'article');
$privacyId = ($privacyType == 'menu_item') ? $this->getPrivacyItemId() : $this->getPrivacyArticleId();
$privacynote = $this->params->get('privacy_note');
// Push the privacy article ID into the privacy field.
$form->setFieldAttribute('privacy', $privacyType, $privacyId, 'privacyconsent');
$form->setFieldAttribute('privacy', 'note', $privacynote, 'privacyconsent');
}
/**
* Method is called before user data is stored in the database
*
* @param array $user Holds the old user data.
* @param boolean $isNew True if a new user is stored.
* @param array $data Holds the new user data.
*
* @return boolean
*
* @since 3.9.0
* @throws \InvalidArgumentException on missing required data.
*/
public function onUserBeforeSave($user, $isNew, $data)
{
// // Only check for front-end user creation/update profile
if ($this->getApplication()->isClient('administrator')) {
return true;
}
$userId = ArrayHelper::getValue($user, 'id', 0, 'int');
// Load plugin language files
$this->loadLanguage();
// User already consented before, no need to check it further
if ($userId > 0 && $this->isUserConsented($userId)) {
return true;
}
// Check that the privacy is checked if required ie only in registration from frontend.
$input = $this->getApplication()->getInput();
$option = $input->get('option');
$task = $input->post->get('task');
$form = $input->post->get('jform', [], 'array');
if (
$option == 'com_users' && \in_array($task, ['registration.register', 'profile.save'])
&& empty($form['privacyconsent']['privacy'])
) {
throw new \InvalidArgumentException($this->getApplication()->getLanguage()->_('PLG_SYSTEM_PRIVACYCONSENT_FIELD_ERROR'));
}
return true;
}
/**
* Saves user privacy confirmation
*
* @param array $data entered user data
* @param boolean $isNew true if this is a new user
* @param boolean $result true if saving the user worked
* @param string $error error message
*
* @return void
*
* @since 3.9.0
*/
public function onUserAfterSave($data, $isNew, $result, $error): void
{
// Only create an entry on front-end user creation/update profile
if ($this->getApplication()->isClient('administrator')) {
return;
}
// Get the user's ID
$userId = ArrayHelper::getValue($data, 'id', 0, 'int');
// If user already consented before, no need to check it further
if ($userId > 0 && $this->isUserConsented($userId)) {
return;
}
$input = $this->getApplication()->getInput();
$option = $input->get('option');
$task = $input->post->get('task');
$form = $input->post->get('jform', [], 'array');
if (
$option == 'com_users'
&& \in_array($task, ['registration.register', 'profile.save'])
&& !empty($form['privacyconsent']['privacy'])
) {
$userId = ArrayHelper::getValue($data, 'id', 0, 'int');
// Get the user's IP address
$ip = $input->server->get('REMOTE_ADDR', '', 'string');
// Get the user agent string
$userAgent = $input->server->get('HTTP_USER_AGENT', '', 'string');
// Create the user note
$userNote = (object) [
'user_id' => $userId,
'subject' => 'PLG_SYSTEM_PRIVACYCONSENT_SUBJECT',
'body' => Text::sprintf('PLG_SYSTEM_PRIVACYCONSENT_BODY', $ip, $userAgent),
'created' => Factory::getDate()->toSql(),
];
try {
$this->getDatabase()->insertObject('#__privacy_consents', $userNote);
} catch (\Exception $e) {
// Do nothing if the save fails
}
$userId = ArrayHelper::getValue($data, 'id', 0, 'int');
$message = [
'action' => 'consent',
'id' => $userId,
'title' => $data['name'],
'itemlink' => 'index.php?option=com_users&task=user.edit&id=' . $userId,
'userid' => $userId,
'username' => $data['username'],
'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $userId,
];
/** @var ActionlogModel $model */
$model = $this->getApplication()->bootComponent('com_actionlogs')->getMVCFactory()->createModel('Actionlog', 'Administrator');
$model->addLog([$message], 'PLG_SYSTEM_PRIVACYCONSENT_CONSENT', 'plg_system_privacyconsent', $userId);
}
}
/**
* Remove all user privacy consent information for the given user ID
*
* Method is called after user data is deleted from the database
*
* @param array $user Holds the user data
* @param boolean $success True if user was successfully stored in the database
* @param string $msg Message
*
* @return void
*
* @since 3.9.0
*/
public function onUserAfterDelete($user, $success, $msg): void
{
if (!$success) {
return;
}
$userId = ArrayHelper::getValue($user, 'id', 0, 'int');
if ($userId) {
// Remove user's consent
$query = $this->getDatabase()->getQuery(true)
->delete($this->getDatabase()->quoteName('#__privacy_consents'))
->where($this->getDatabase()->quoteName('user_id') . ' = :userid')
->bind(':userid', $userId, ParameterType::INTEGER);
$this->getDatabase()->setQuery($query);
$this->getDatabase()->execute();
}
}
/**
* If logged in users haven't agreed to privacy consent, redirect them to profile edit page, ask them to agree to
* privacy consent before allowing access to any other pages
*
* @return void
*
* @since 3.9.0
*/
public function onAfterRoute()
{
// Run this in frontend only
if (!$this->getApplication()->isClient('site')) {
return;
}
$userId = $this->getApplication()->getIdentity()->id;
// Check to see whether user already consented, if not, redirect to user profile page
if ($userId > 0) {
// Load plugin language files
$this->loadLanguage();
// If user consented before, no need to check it further
if ($this->isUserConsented($userId)) {
return;
}
$input = $this->getApplication()->getInput();
$option = $input->getCmd('option');
$task = $input->get('task', '');
$view = $input->getString('view', '');
$layout = $input->getString('layout', '');
$id = $input->getInt('id');
$privacyArticleId = $this->getPrivacyArticleId();
/*
* If user is already on edit profile screen or view privacy article
* or press update/apply button, or logout, do nothing to avoid infinite redirect
*/
$allowedUserTasks = [
'profile.save', 'profile.apply', 'user.logout', 'user.menulogout',
'method', 'methods', 'captive', 'callback',
];
$isAllowedUserTask = \in_array($task, $allowedUserTasks)
|| substr($task, 0, 8) === 'captive.'
|| substr($task, 0, 8) === 'methods.'
|| substr($task, 0, 7) === 'method.'
|| substr($task, 0, 9) === 'callback.';
if (
($option == 'com_users' && $isAllowedUserTask)
|| ($option == 'com_content' && $view == 'article' && $id == $privacyArticleId)
|| ($option == 'com_users' && $view == 'profile' && $layout == 'edit')
) {
return;
}
// Redirect to com_users profile edit
$this->getApplication()->enqueueMessage($this->getRedirectMessage(), 'notice');
$link = 'index.php?option=com_users&view=profile&layout=edit';
$this->getApplication()->redirect(Route::_($link, false));
}
}
/**
* Event to specify whether a privacy policy has been published.
*
* @param CheckPrivacyPolicyPublishedEvent $event The privacy policy status event.
*
* @return void
*
* @since 3.9.0
*/
public function onPrivacyCheckPrivacyPolicyPublished(CheckPrivacyPolicyPublishedEvent $event)
{
// Data, with keys "published", "editLink" and "articlePublished".
$policy = $event->getPolicyInfo();
// If another plugin has already indicated a policy is published, we won't change anything here
if ($policy['published']) {
return;
}
$articleId = (int) $this->params->get('privacy_article');
if (!$articleId) {
return;
}
// Check if the article exists in database and is published
$query = $this->getDatabase()->getQuery(true)
->select($this->getDatabase()->quoteName(['id', 'state']))
->from($this->getDatabase()->quoteName('#__content'))
->where($this->getDatabase()->quoteName('id') . ' = :id')
->bind(':id', $articleId, ParameterType::INTEGER);
$this->getDatabase()->setQuery($query);
$article = $this->getDatabase()->loadObject();
// Check if the article exists
if (!$article) {
return;
}
// Check if the article is published
if ($article->state == 1) {
$policy['articlePublished'] = true;
}
$policy['published'] = true;
$policy['editLink'] = Route::_('index.php?option=com_content&task=article.edit&id=' . $articleId);
$event->updatePolicyInfo($policy);
}
/**
* Returns the configured redirect message and falls back to the default version.
*
* @return string redirect message
*
* @since 3.9.0
*/
private function getRedirectMessage()
{
$messageOnRedirect = trim($this->params->get('messageOnRedirect', ''));
if (empty($messageOnRedirect)) {
return $this->getApplication()->getLanguage()->_('PLG_SYSTEM_PRIVACYCONSENT_REDIRECT_MESSAGE_DEFAULT');
}
return $messageOnRedirect;
}
/**
* Method to check if the given user has consented yet
*
* @param integer $userId ID of uer to check
*
* @return boolean
*
* @since 3.9.0
*/
private function isUserConsented($userId)
{
$userId = (int) $userId;
$db = $this->getDatabase();
$query = $db->getQuery(true);
$query->select('COUNT(*)')
->from($db->quoteName('#__privacy_consents'))
->where($db->quoteName('user_id') . ' = :userid')
->where($db->quoteName('subject') . ' = ' . $db->quote('PLG_SYSTEM_PRIVACYCONSENT_SUBJECT'))
->where($db->quoteName('state') . ' = 1')
->bind(':userid', $userId, ParameterType::INTEGER);
$db->setQuery($query);
return (int) $db->loadResult() > 0;
}
/**
* Get privacy article ID. If the site is a multilingual website and there is associated article for the
* current language, ID of the associated article will be returned
*
* @return integer
*
* @since 3.9.0
*/
private function getPrivacyArticleId()
{
$privacyArticleId = $this->params->get('privacy_article');
if ($privacyArticleId > 0 && Associations::isEnabled()) {
$privacyAssociated = Associations::getAssociations('com_content', '#__content', 'com_content.item', $privacyArticleId);
$currentLang = $this->getApplication()->getLanguage()->getTag();
if (isset($privacyAssociated[$currentLang])) {
$privacyArticleId = $privacyAssociated[$currentLang]->id;
}
}
return $privacyArticleId;
}
/**
* Get privacy menu item ID. If the site is a multilingual website and there is associated menu item for the
* current language, ID of the associated menu item will be returned.
*
* @return integer
*
* @since 4.0.0
*/
private function getPrivacyItemId()
{
$itemId = $this->params->get('privacy_menu_item');
if ($itemId > 0 && Associations::isEnabled()) {
$privacyAssociated = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $itemId, 'id', '', '');
$currentLang = $this->getApplication()->getLanguage()->getTag();
if (isset($privacyAssociated[$currentLang])) {
$itemId = $privacyAssociated[$currentLang]->id;
}
}
return $itemId;
}
}

View File

@ -0,0 +1,133 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.privacyconsent
*
* @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\PrivacyConsent\Field;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\RadioField;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\Component\Content\Site\Helper\RouteHelper;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Provides input for privacy
*
* @since 3.9.0
*/
class PrivacyField extends RadioField
{
/**
* The form field type.
*
* @var string
* @since 3.9.0
*/
protected $type = 'privacy';
/**
* Method to get the field input markup.
*
* @return string The field input markup.
*
* @since 3.9.0
*/
protected function getInput()
{
// Display the message before the field
echo $this->getRenderer('plugins.system.privacyconsent.message')->render($this->getLayoutData());
return parent::getInput();
}
/**
* Method to get the field label markup.
*
* @return string The field label markup.
*
* @since 3.9.0
*/
protected function getLabel()
{
if ($this->hidden) {
return '';
}
return $this->getRenderer('plugins.system.privacyconsent.label')->render($this->getLayoutData());
}
/**
* Method to get the data to be passed to the layout for rendering.
*
* @return array
*
* @since 3.9.4
*/
protected function getLayoutData()
{
$data = parent::getLayoutData();
$article = false;
$link = false;
$privacyArticle = $this->element['article'] > 0 ? (int) $this->element['article'] : 0;
if ($privacyArticle && Factory::getApplication()->isClient('site')) {
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'alias', 'catid', 'language']))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $privacyArticle, ParameterType::INTEGER);
$db->setQuery($query);
$article = $db->loadObject();
$slug = $article->alias ? ($article->id . ':' . $article->alias) : $article->id;
$article->link = RouteHelper::getArticleRoute($slug, $article->catid, $article->language);
$link = $article->link;
}
$privacyMenuItem = $this->element['menu_item'] > 0 ? (int) $this->element['menu_item'] : 0;
if ($privacyMenuItem && Factory::getApplication()->isClient('site')) {
$link = 'index.php?Itemid=' . $privacyMenuItem;
if (Multilanguage::isEnabled()) {
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'language']))
->from($db->quoteName('#__menu'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $privacyMenuItem, ParameterType::INTEGER);
$db->setQuery($query);
$menuItem = $db->loadObject();
$link .= '&lang=' . $menuItem->language;
}
}
$extraData = [
'privacynote' => !empty($this->element['note']) ? $this->element['note'] : Text::_('PLG_SYSTEM_PRIVACYCONSENT_NOTE_FIELD_DEFAULT'),
'options' => $this->getOptions(),
'value' => (string) $this->value,
'translateLabel' => $this->translateLabel,
'translateDescription' => $this->translateDescription,
'translateHint' => $this->translateHint,
'privacyArticle' => $privacyArticle,
'article' => $article,
'privacyLink' => $link,
];
return array_merge($data, $extraData);
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset>
<field
name="term"
type="text"
label="PLG_SYSTEM_REDIRECT_FIELD_EXCLUDE_URLS_TERM_LABEL"
description="PLG_SYSTEM_REDIRECT_FIELD_EXCLUDE_URLS_TERM_DESC"
required="true"
/>
<field
name="regexp"
type="checkbox"
label="PLG_SYSTEM_REDIRECT_FIELD_EXCLUDE_URLS_REGEXP_LABEL"
filter="integer"
/>
</fieldset>
</form>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_redirect</name>
<author>Joomla! Project</author>
<creationDate>2009-04</creationDate>
<copyright>(C) 2009 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_SYSTEM_REDIRECT_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Redirect</namespace>
<files>
<folder>form</folder>
<folder plugin="redirect">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_redirect.ini</language>
<language tag="en-GB">language/en-GB/plg_system_redirect.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="collect_urls"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_REDIRECT_FIELD_COLLECT_URLS_LABEL"
default="1"
filter="integer"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="includeUrl"
type="radio"
layout="joomla.form.field.radio.switcher"
label="PLG_SYSTEM_REDIRECT_FIELD_STORE_FULL_URL_LABEL"
default="1"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="exclude_urls"
type="subform"
label="PLG_SYSTEM_REDIRECT_FIELD_EXCLUDE_URLS_LABEL"
multiple="true"
formsource="plugins/system/redirect/form/excludes.xml"
layout="joomla.form.field.subform.repeatable-table"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,48 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.redirect
*
* @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\Redirect\Extension\Redirect;
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) {
$plugin = new Redirect(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'redirect')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,265 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.redirect
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Redirect\Extension;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\ErrorEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\ParameterType;
use Joomla\Event\SubscriberInterface;
use Joomla\String\StringHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Plugin class for redirect handling.
*
* @since 1.6
*/
final class Redirect extends CMSPlugin implements SubscriberInterface
{
use DatabaseAwareTrait;
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 4.0.0
*/
public static function getSubscribedEvents(): array
{
return ['onError' => 'handleError'];
}
/**
* Internal processor for all error handlers
*
* @param ErrorEvent $event The event object
*
* @return void
*
* @since 3.5
*/
public function handleError(ErrorEvent $event)
{
/** @var \Joomla\CMS\Application\CMSApplication $app */
$app = $event->getApplication();
if ($app->isClient('administrator') || ((int) $event->getError()->getCode() !== 404)) {
return;
}
// Load translations
$this->loadLanguage();
$uri = Uri::getInstance();
// These are the original URLs
$orgurl = rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'query', 'fragment']));
$orgurlRel = rawurldecode($uri->toString(['path', 'query', 'fragment']));
// The above doesn't work for sub directories, so do this
$orgurlRootRel = str_replace(Uri::root(), '', $orgurl);
// For when users have added / to the url
$orgurlRootRelSlash = str_replace(Uri::root(), '/', $orgurl);
$orgurlWithoutQuery = rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'fragment']));
$orgurlRelWithoutQuery = rawurldecode($uri->toString(['path', 'fragment']));
// These are the URLs we save and use
$url = StringHelper::strtolower(rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'query', 'fragment'])));
$urlRel = StringHelper::strtolower(rawurldecode($uri->toString(['path', 'query', 'fragment'])));
// The above doesn't work for sub directories, so do this
$urlRootRel = str_replace(Uri::root(), '', $url);
// For when users have added / to the url
$urlRootRelSlash = str_replace(Uri::root(), '/', $url);
$urlWithoutQuery = StringHelper::strtolower(rawurldecode($uri->toString(['scheme', 'host', 'port', 'path', 'fragment'])));
$urlRelWithoutQuery = StringHelper::strtolower(rawurldecode($uri->toString(['path', 'fragment'])));
$excludes = (array) $this->params->get('exclude_urls');
$skipUrl = false;
foreach ($excludes as $exclude) {
if (empty($exclude->term)) {
continue;
}
if (!empty($exclude->regexp)) {
// Only check $url, because it includes all other sub urls
if (preg_match('/' . $exclude->term . '/i', $orgurlRel)) {
$skipUrl = true;
break;
}
} else {
if (StringHelper::strpos($orgurlRel, $exclude->term) !== false) {
$skipUrl = true;
break;
}
}
}
/**
* Why is this (still) here?
* Because hackers still try urls with mosConfig_* and Url Injection with =http[s]:// and we dont want to log/redirect these requests
*/
if ($skipUrl || (strpos($url, 'mosConfig_') !== false) || (strpos($url, '=http') !== false)) {
return;
}
$query = $this->getDatabase()->getQuery(true);
$query->select('*')
->from($this->getDatabase()->quoteName('#__redirect_links'))
->whereIn(
$this->getDatabase()->quoteName('old_url'),
[
$url,
$urlRel,
$urlRootRel,
$urlRootRelSlash,
$urlWithoutQuery,
$urlRelWithoutQuery,
$orgurl,
$orgurlRel,
$orgurlRootRel,
$orgurlRootRelSlash,
$orgurlWithoutQuery,
$orgurlRelWithoutQuery,
],
ParameterType::STRING
);
$this->getDatabase()->setQuery($query);
$redirect = null;
try {
$redirects = $this->getDatabase()->loadAssocList();
} catch (\Exception $e) {
$event->setError(new \Exception($this->getApplication()->getLanguage()->_('PLG_SYSTEM_REDIRECT_ERROR_UPDATING_DATABASE'), 500, $e));
return;
}
$possibleMatches = array_unique(
[
$url,
$urlRel,
$urlRootRel,
$urlRootRelSlash,
$urlWithoutQuery,
$urlRelWithoutQuery,
$orgurl,
$orgurlRel,
$orgurlRootRel,
$orgurlRootRelSlash,
$orgurlWithoutQuery,
$orgurlRelWithoutQuery,
]
);
foreach ($possibleMatches as $match) {
if (($index = array_search($match, array_column($redirects, 'old_url'))) !== false) {
$redirect = (object) $redirects[$index];
if ((int) $redirect->published === 1) {
break;
}
}
}
// A redirect object was found and, if published, will be used
if ($redirect !== null && ((int) $redirect->published === 1)) {
if (!$redirect->header || (bool) ComponentHelper::getParams('com_redirect')->get('mode', false) === false) {
$redirect->header = 301;
}
if ($redirect->header < 400 && $redirect->header >= 300) {
$urlQuery = $uri->getQuery();
$oldUrlParts = parse_url($redirect->old_url);
$newUrl = $redirect->new_url;
if ($urlQuery !== '' && empty($oldUrlParts['query'])) {
$newUrl .= '?' . $urlQuery;
}
$dest = Uri::isInternal($newUrl) || strpos($newUrl, 'http') === false ?
Route::_($newUrl) : $newUrl;
// In case the url contains double // lets remove it
$destination = str_replace(Uri::root() . '/', Uri::root(), $dest);
// Always count redirect hits
$redirect->hits++;
try {
$this->getDatabase()->updateObject('#__redirect_links', $redirect, 'id');
} catch (\Exception $e) {
// We don't log issues for now
}
$app->redirect($destination, (int) $redirect->header);
}
$event->setError(new \RuntimeException($event->getError()->getMessage(), $redirect->header, $event->getError()));
} elseif ($redirect === null) {
// No redirect object was found so we create an entry in the redirect table
if ((bool) $this->params->get('collect_urls', 1)) {
if (!$this->params->get('includeUrl', 1)) {
$url = $urlRel;
}
$nowDate = Factory::getDate()->toSql();
$data = (object) [
'id' => 0,
'old_url' => $url,
'referer' => $app->getInput()->server->getString('HTTP_REFERER', ''),
'hits' => 1,
'published' => 0,
'created_date' => $nowDate,
'modified_date' => $nowDate,
];
try {
$this->getDatabase()->insertObject('#__redirect_links', $data, 'id');
} catch (\Exception $e) {
$event->setError(new \Exception($this->getApplication()->getLanguage()->_('PLG_SYSTEM_REDIRECT_ERROR_UPDATING_DATABASE'), 500, $e));
return;
}
}
} else {
// We have an unpublished redirect object, increment the hit counter
$redirect->hits++;
try {
$this->getDatabase()->updateObject('#__redirect_links', $redirect, ['id']);
} catch (\Exception $e) {
$event->setError(new \Exception($this->getApplication()->getLanguage()->_('PLG_SYSTEM_REDIRECT_ERROR_UPDATING_DATABASE'), 500, $e));
return;
}
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_remember</name>
<author>Joomla! Project</author>
<creationDate>2007-04</creationDate>
<copyright>(C) 2007 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_REMEMBER_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Remember</namespace>
<files>
<folder plugin="remember">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_remember.ini</language>
<language tag="en-GB">language/en-GB/plg_system_remember.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,48 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.remember
*
* @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\Remember\Extension\Remember;
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) {
$plugin = new Remember(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'remember')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,130 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.remember
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Remember\Extension;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\User\UserHelper;
use Joomla\Database\DatabaseAwareTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! System Remember Me Plugin
*
* @since 1.5
*/
final class Remember extends CMSPlugin
{
use DatabaseAwareTrait;
/**
* Remember me method to run onAfterInitialise
* Only purpose is to initialise the login authentication process if a cookie is present
*
* @return void
*
* @since 1.5
*
* @throws InvalidArgumentException
*/
public function onAfterInitialise()
{
// No remember me for admin.
if (!$this->getApplication()->isClient('site')) {
return;
}
// Check for a cookie if user is not logged in
if ($this->getApplication()->getIdentity()->guest) {
$cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent();
// Check for the cookie
if ($this->getApplication()->getInput()->cookie->get($cookieName)) {
$this->getApplication()->login(['username' => ''], ['silent' => true]);
}
}
}
/**
* Imports the authentication plugin on user logout to make sure that the cookie is destroyed.
*
* @param array $user Holds the user data.
* @param array $options Array holding options (remember, autoregister, group).
*
* @return boolean
*/
public function onUserLogout($user, $options)
{
// No remember me for admin
if (!$this->getApplication()->isClient('site')) {
return true;
}
$cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent();
// Check for the cookie
if ($this->getApplication()->getInput()->cookie->get($cookieName)) {
// Make sure authentication group is loaded to process onUserAfterLogout event
PluginHelper::importPlugin('authentication');
}
return true;
}
/**
* Method is called before user data is stored in the database
* Invalidate all existing remember-me cookies after a password change
*
* @param array $user Holds the old user data.
* @param boolean $isnew True if a new user is stored.
* @param array $data Holds the new user data.
*
* @return boolean
*
* @since 3.8.6
*/
public function onUserBeforeSave($user, $isnew, $data)
{
// Irrelevant on new users
if ($isnew) {
return true;
}
// Irrelevant, because password was not changed by user
if (empty($data['password_clear'])) {
return true;
}
// But now, we need to do something - Delete all tokens for this user!
$db = $this->getDatabase();
$query = $db->getQuery(true)
->delete($db->quoteName('#__user_keys'))
->where($db->quoteName('user_id') . ' = :userid')
->bind(':userid', $user['username']);
try {
$db->setQuery($query)->execute();
} catch (\RuntimeException $e) {
// Log an alert for the site admin
Log::add(
sprintf('Failed to delete cookie token for user %s with the following error: %s', $user['username'], $e->getMessage()),
Log::WARNING,
'security'
);
}
return true;
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_schedulerunner</name>
<author>Joomla! Project</author>
<creationDate>2021-08</creationDate>
<copyright>(C) 2021 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>4.1</version>
<description>PLG_SYSTEM_SCHEDULERUNNER_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\ScheduleRunner</namespace>
<media destination="plg_system_schedulerunner" folder="media">
<folder>js</folder>
<filename>joomla.asset.json</filename>
</media>
<files>
<folder plugin="schedulerunner">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_schedulerunner.ini</language>
<language tag="en-GB">language/en-GB/plg_system_schedulerunner.sys.ini</language>
</languages>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.schedulerunner
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\ScheduleRunner\Extension\ScheduleRunner;
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) {
$plugin = new ScheduleRunner(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'schedulerunner')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,357 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.schedulerunner
*
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\ScheduleRunner\Extension;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Table\Extension;
use Joomla\CMS\User\UserHelper;
use Joomla\Component\Scheduler\Administrator\Model\TasksModel;
use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler;
use Joomla\Component\Scheduler\Administrator\Task\Task;
use Joomla\Event\Event;
use Joomla\Event\EventInterface;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* This plugin implements listeners to support a visitor-triggered lazy-scheduling pattern.
* If `com_scheduler` is installed/enabled and its configuration allows unprotected lazy scheduling, this plugin
* injects into each response with an HTML context a JS file {@see PlgSystemScheduleRunner::injectScheduleRunner()} that
* sets up an AJAX callback to trigger the scheduler {@see PlgSystemScheduleRunner::runScheduler()}. This is achieved
* through a call to the `com_ajax` component.
* Also supports the scheduler component configuration form through auto-generation of the webcron key and injection
* of JS of usability enhancement.
*
* @since 4.1.0
*/
final class ScheduleRunner extends CMSPlugin implements SubscriberInterface
{
/**
* Length of auto-generated webcron key.
*
* @var integer
* @since 4.1.0
*/
private const WEBCRON_KEY_LENGTH = 20;
/**
* @inheritDoc
*
* @return string[]
*
* @since 4.1.0
*
* @throws \Exception
*/
public static function getSubscribedEvents(): array
{
$config = ComponentHelper::getParams('com_scheduler');
$app = Factory::getApplication();
$mapping = [];
if ($app->isClient('site') || $app->isClient('administrator')) {
$mapping['onBeforeCompileHead'] = 'injectLazyJS';
$mapping['onAjaxRunSchedulerLazy'] = 'runLazyCron';
// Only allowed in the frontend
if ($app->isClient('site')) {
if ($config->get('webcron.enabled')) {
$mapping['onAjaxRunSchedulerWebcron'] = 'runWebCron';
}
} elseif ($app->isClient('administrator')) {
$mapping['onContentPrepareForm'] = 'enhanceSchedulerConfig';
$mapping['onExtensionBeforeSave'] = 'generateWebcronKey';
$mapping['onAjaxRunSchedulerTest'] = 'runTestCron';
}
}
return $mapping;
}
/**
* Inject JavaScript to trigger the scheduler in HTML contexts.
*
* @param EventInterface $event The onBeforeCompileHead event.
*
* @return void
*
* @since 4.1.0
*/
public function injectLazyJS(EventInterface $event): void
{
// Only inject in HTML documents
if ($this->getApplication()->getDocument()->getType() !== 'html') {
return;
}
$config = ComponentHelper::getParams('com_scheduler');
if (!$config->get('lazy_scheduler.enabled', true)) {
return;
}
/** @var TasksModel $model */
$model = $this->getApplication()->bootComponent('com_scheduler')
->getMVCFactory()->createModel('Tasks', 'Administrator', ['ignore_request' => true]);
$now = Factory::getDate('now', 'UTC');
if (!$model->hasDueTasks($now)) {
return;
}
// Add configuration options
$triggerInterval = $config->get('lazy_scheduler.interval', 300);
$this->getApplication()->getDocument()->addScriptOptions('plg_system_schedulerunner', ['interval' => $triggerInterval]);
// Load and injection directive
$wa = $this->getApplication()->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('plg_system_schedulerunner');
$wa->useScript('plg_system_schedulerunner.run-schedule');
}
/**
* Acts on the LazyCron trigger from the frontend when Lazy Cron is enabled in the Scheduler component
* configuration. The lazy cron trigger is implemented in client-side JavaScript which is injected on every page
* load with an HTML context when the component configuration allows it. This method then triggers the Scheduler,
* which effectively runs the next Task in the Scheduler's task queue.
*
* @param EventInterface $e The onAjaxRunSchedulerLazy event.
*
* @return void
*
* @since 4.1.0
*
* @throws \Exception
*/
public function runLazyCron(EventInterface $e)
{
$config = ComponentHelper::getParams('com_scheduler');
if (!$config->get('lazy_scheduler.enabled', true)) {
return;
}
// Since the the request from the frontend may time out, try allowing execution after disconnect.
if (\function_exists('ignore_user_abort')) {
ignore_user_abort(true);
}
// Prevent PHP from trying to output to the user pipe. PHP may kill the script otherwise if the pipe is not accessible.
ob_start();
// Suppress all errors to avoid any output
try {
$this->runScheduler();
} catch (\Exception $e) {
}
ob_end_clean();
}
/**
* This method is responsible for the WebCron functionality of the Scheduler component.<br/>
* Acting on a `com_ajax` call, this method can work in two ways:
* 1. If no Task ID is specified, it triggers the Scheduler to run the next task in
* the task queue.
* 2. If a Task ID is specified, it fetches the task (if it exists) from the Scheduler API and executes it.<br/>
*
* URL query parameters:
* - `hash` string (required) Webcron hash (from the Scheduler component configuration).
* - `id` int (optional) ID of the task to trigger.
*
* @param Event $event The onAjaxRunSchedulerWebcron event.
*
* @return void
*
* @since 4.1.0
*
* @throws \Exception
*/
public function runWebCron(Event $event)
{
$config = ComponentHelper::getParams('com_scheduler');
$hash = $config->get('webcron.key', '');
if (!$config->get('webcron.enabled', false)) {
Log::add($this->getApplication()->getLanguage()->_('PLG_SYSTEM_SCHEDULE_RUNNER_WEBCRON_DISABLED'));
throw new \Exception($this->getApplication()->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
}
if (!\strlen($hash) || $hash !== $this->getApplication()->getInput()->get('hash')) {
throw new \Exception($this->getApplication()->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
}
$id = (int) $this->getApplication()->getInput()->getInt('id', 0);
$task = $this->runScheduler($id);
if (!empty($task) && !empty($task->getContent()['exception'])) {
throw $task->getContent()['exception'];
}
}
/**
* This method is responsible for the "test run" functionality in the Scheduler administrator backend interface.
* Acting on a `com_ajax` call, this method requires the URL to have a `id` query parameter (corresponding to an
* existing Task ID).
*
* @param Event $event The onAjaxRunScheduler event.
*
* @return void
*
* @since 4.1.0
*
* @throws \Exception
*/
public function runTestCron(Event $event)
{
if (!Session::checkToken('GET')) {
return;
}
$id = (int) $this->getApplication()->getInput()->getInt('id');
$allowConcurrent = $this->getApplication()->getInput()->getBool('allowConcurrent', false);
$user = $this->getApplication()->getIdentity();
if (empty($id) || !$user->authorise('core.testrun', 'com_scheduler.task.' . $id)) {
throw new \Exception($this->getApplication()->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
}
/**
* ?: About allow simultaneous, how do we detect if it failed because of pre-existing lock?
*
* We will allow CLI exclusive tasks to be fetched and executed, it's left to routines to do a runtime check
* if they want to refuse normal operation.
*/
$task = (new Scheduler())->getTask(
[
'id' => $id,
'allowDisabled' => true,
'bypassScheduling' => true,
'allowConcurrent' => $allowConcurrent,
]
);
if ($task) {
$task->run();
$event->addArgument('result', $task->getContent());
} else {
/**
* Placeholder result, but the idea is if we failed to fetch the task, it's likely because another task was
* already running. This is a fair assumption if this test run was triggered through the administrator backend,
* so we know the task probably exists and is either enabled/disabled (not trashed).
*/
// @todo language constant + review if this is done right.
$event->addArgument('result', ['message' => 'could not acquire lock on task. retry or allow concurrency.']);
}
}
/**
* Run the scheduler, allowing execution of a single due task.
* Does not bypass task scheduling, meaning that even if an ID is passed the task is only
* triggered if it is due.
*
* @param integer $id The optional ID of the task to run
*
* @return ?Task
*
* @since 4.1.0
* @throws \RuntimeException
*/
private function runScheduler(int $id = 0): ?Task
{
return (new Scheduler())->runTask(['id' => $id]);
}
/**
* Enhance the scheduler config form by dynamically populating or removing display fields.
*
* @param Model\PrepareFormEvent $event The onContentPrepareForm event.
*
* @return void
*
* @since 4.1.0
* @throws \UnexpectedValueException|\RuntimeException
*
* @todo Move to another plugin?
*/
public function enhanceSchedulerConfig(Model\PrepareFormEvent $event): void
{
$form = $event->getForm();
$data = $event->getData();
if (
$form->getName() !== 'com_config.component'
|| $this->getApplication()->getInput()->get('component') !== 'com_scheduler'
) {
return;
}
if (!empty($data['webcron']['key'])) {
$form->removeField('generate_key_on_save', 'webcron');
$relative = 'index.php?option=com_ajax&plugin=RunSchedulerWebcron&group=system&format=json&hash=' . $data['webcron']['key'];
$link = Route::link('site', $relative, false, Route::TLS_IGNORE, true);
$form->setValue('base_link', 'webcron', $link);
} else {
$form->removeField('base_link', 'webcron');
$form->removeField('reset_key', 'webcron');
}
}
/**
* Auto-generate a key/hash for the webcron functionality.
* This method acts on table save, when a hash doesn't already exist or a reset is required.
* @todo Move to another plugin?
*
* @param EventInterface $event The onExtensionBeforeSave event.
*
* @return void
*
* @since 4.1.0
*/
public function generateWebcronKey(EventInterface $event): void
{
/** @var Extension $table */
[$context, $table] = array_values($event->getArguments());
if ($context !== 'com_config.component' || $table->name !== 'com_scheduler') {
return;
}
$params = new Registry($table->params ?? '');
if (
empty($params->get('webcron.key'))
|| $params->get('webcron.reset_key') === 1
) {
$params->set('webcron.key', UserHelper::genRandomPassword(self::WEBCRON_KEY_LENGTH));
}
$params->remove('webcron.base_link');
$params->remove('webcron.reset_key');
$table->params = $params->toString();
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="schema">
<fieldset
name="schema"
label="PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_LABEL"
>
<field
name="schemainfo"
type="note"
class="alert alert-info d-block w-100"
description="PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_DESCRIPTION"
/>
<field
name="schemaType"
type="list"
label="PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_TYPE_LABEL"
default="None"
validate="options"
>
<option value="None">JNONE</option>
</field>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_schemaorg</name>
<author>Joomla! Project</author>
<creationDate>2023-07</creationDate>
<copyright>(C) 2023 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>5.0.0</version>
<description>PLG_SYSTEM_SCHEMAORG_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Schemaorg</namespace>
<files>
<folder plugin="schemaorg">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_schemaorg.ini</language>
<language tag="en-GB">language/en-GB/plg_system_schemaorg.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="baseType"
type="list"
label="PLG_SYSTEM_SCHEMAORG_BASETYPE_LABEL"
description="PLG_SYSTEM_SCHEMAORG_BASETYPE_DESCRIPTION"
default="organization"
validate="options"
required="true"
>
<option value="organization">PLG_SYSTEM_SCHEMAORG_BASETYPE_OPTION_ORGANIZATION</option>
<option value="person">PLG_SYSTEM_SCHEMAORG_BASETYPE_OPTION_PERSON</option>
</field>
<field
name="user"
type="user"
label="PLG_SYSTEM_SCHEMAORG_USER_LABEL"
showon="baseType:person"
default="0"
/>
<field
name="name"
type="text"
label="PLG_SYSTEM_SCHEMAORG_NAME_LABEL"
showon="baseType:organization[OR]user:0"
/>
<field
name="image"
type="media"
label="PLG_SYSTEM_SCHEMAORG_IMAGE_LABEL"
/>
<field
name="socialmedia"
type="subform"
label="PLG_SYSTEM_SCHEMAORG_SOCIALMEDIA_LABEL"
multiple="true"
>
<form>
<field
name="url"
type="url"
label="PLG_SYSTEM_SCHEMAORG_SOCIALMEDIA_URL_LABEL"
required="true"
hint="https://"
validate="url"
/>
</form>
</field>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,53 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.schemaorg
*
* @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\CMS\User\UserFactoryInterface;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Schemaorg\Extension\Schemaorg;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 5.0.0
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new Schemaorg(
$dispatcher,
(array) PluginHelper::getPlugin('system', 'schemaorg')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase(Factory::getContainer()->get(DatabaseInterface::class));
$plugin->setUserFactory($container->get(UserFactoryInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,533 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.schemaorg
*
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Schemaorg\Extension;
use Joomla\CMS\Event\Model;
use Joomla\CMS\Event\Plugin\System\Schemaorg\BeforeCompileHeadEvent;
use Joomla\CMS\Event\Plugin\System\Schemaorg\PrepareDataEvent;
use Joomla\CMS\Event\Plugin\System\Schemaorg\PrepareFormEvent;
use Joomla\CMS\Event\Plugin\System\Schemaorg\PrepareSaveEvent;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Schemaorg\SchemaorgPrepareDateTrait;
use Joomla\CMS\Schemaorg\SchemaorgPrepareImageTrait;
use Joomla\CMS\Schemaorg\SchemaorgServiceInterface;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\ParameterType;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Schemaorg System Plugin
*
* @since 5.0.0
*/
final class Schemaorg extends CMSPlugin implements SubscriberInterface
{
use DatabaseAwareTrait;
use SchemaorgPrepareImageTrait;
use SchemaorgPrepareDateTrait;
use UserFactoryAwareTrait;
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 5.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onBeforeCompileHead' => 'onBeforeCompileHead',
'onContentPrepareData' => 'onContentPrepareData',
'onContentPrepareForm' => 'onContentPrepareForm',
'onContentAfterSave' => 'onContentAfterSave',
];
}
/**
* Runs on content preparation
*
* @param Model\PrepareDataEvent $event The event
*
* @since 5.0.0
*
*/
public function onContentPrepareData(Model\PrepareDataEvent $event)
{
$context = $event->getContext();
$data = $event->getData();
$app = $this->getApplication();
if ($app->isClient('site') || !$this->isSupported($context)) {
return;
}
$data = (object) $data;
$itemId = $data->id ?? 0;
// Check if the form already has some data
if ($itemId > 0) {
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__schemaorg'))
->where($db->quoteName('itemId') . '= :itemId')
->bind(':itemId', $itemId, ParameterType::INTEGER)
->where($db->quoteName('context') . '= :context')
->bind(':context', $context, ParameterType::STRING);
$results = $db->setQuery($query)->loadAssoc();
if (empty($results)) {
return;
}
$schemaType = $results['schemaType'];
$data->schema['schemaType'] = $schemaType;
$schema = new Registry($results['schema']);
$data->schema[$schemaType] = $schema->toArray();
}
$dispatcher = $this->getDispatcher();
$event = new PrepareDataEvent('onSchemaPrepareData', [
'subject' => $data,
'context' => $context,
]);
PluginHelper::importPlugin('schemaorg', null, true, $dispatcher);
$dispatcher->dispatch('onSchemaPrepareData', $event);
}
/**
* The form event.
*
* @param Model\PrepareFormEvent $event The event
*
* @since 5.0.0
*/
public function onContentPrepareForm(Model\PrepareFormEvent $event)
{
$form = $event->getForm();
$context = $form->getName();
$app = $this->getApplication();
if (!$app->isClient('administrator') || !$this->isSupported($context)) {
return;
}
// Load plugin language files.
$this->loadLanguage();
// Load the form fields
$form->loadFile(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms/schemaorg.xml');
// The user should configure the plugin first
if (!$this->params->get('baseType')) {
$form->removeField('schemaType', 'schema');
$plugin = PluginHelper::getPlugin('system', 'schemaorg');
$user = $this->getApplication()->getIdentity();
$infoText = Text::_('PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_DESCRIPTION_NOT_CONFIGURATED');
// If edit permission are available, offer a link
if ($user->authorise('core.edit', 'com_plugins')) {
$infoText = Text::sprintf('PLG_SYSTEM_SCHEMAORG_FIELD_SCHEMA_DESCRIPTION_NOT_CONFIGURATED_ADMIN', (int) $plugin->id);
}
$form->setFieldAttribute('schemainfo', 'description', $infoText, 'schema');
return;
}
$dispatcher = $this->getDispatcher();
$event = new PrepareFormEvent('onSchemaPrepareForm', [
'subject' => $form,
]);
PluginHelper::importPlugin('schemaorg', null, true, $dispatcher);
$dispatcher->dispatch('onSchemaPrepareForm', $event);
}
/**
* Saves form field data in the database
*
* @param Model\AfterSaveEvent $event
*
* @return void
*
* @since 5.0.0
*/
public function onContentAfterSave(Model\AfterSaveEvent $event)
{
$context = $event->getContext();
$table = $event->getItem();
$isNew = $event->getIsNew();
$data = $event->getData();
$app = $this->getApplication();
$db = $this->getDatabase();
if (!$app->isClient('administrator') || !$this->isSupported($context)) {
return;
}
$itemId = (int) $table->id;
if (empty($data['schema']) || empty($data['schema']['schemaType']) || $data['schema']['schemaType'] === 'None') {
$query = $db->getQuery(true);
$query->delete($db->quoteName('#__schemaorg'))
->where($db->quoteName('itemId') . '= :itemId')
->bind(':itemId', $itemId, ParameterType::INTEGER)
->where($db->quoteName('context') . '= :context')
->bind(':context', $context, ParameterType::STRING);
$db->setQuery($query)->execute();
return;
}
$query = $db->getQuery(true);
$query->select('*')
->from($db->quoteName('#__schemaorg'))
->where($db->quoteName('itemId') . '= :itemId')
->bind(':itemId', $itemId, ParameterType::INTEGER)
->where($db->quoteName('context') . '= :context')
->bind(':context', $context, ParameterType::STRING);
$entry = $db->setQuery($query)->loadObject();
if (empty($entry->id)) {
$entry = new \stdClass();
}
$entry->itemId = (int) $table->getId();
$entry->context = $context;
if (isset($data['schema']['schemaType'])) {
$entry->schemaType = $data['schema']['schemaType'];
if (isset($data['schema'][$entry->schemaType])) {
$entry->schema = (new Registry($data['schema'][$entry->schemaType]))->toString();
}
}
$dispatcher = $this->getDispatcher();
$event = new PrepareSaveEvent('onSchemaPrepareSave', [
'subject' => $entry,
'context' => $context,
'item' => $table,
'isNew' => $isNew,
'schema' => $data['schema'],
]);
PluginHelper::importPlugin('schemaorg', null, true, $dispatcher);
$dispatcher->dispatch('onSchemaPrepareSave', $event);
if (!isset($entry->schemaType)) {
return;
}
if (!empty($entry->id)) {
$db->updateObject('#__schemaorg', $entry, 'id');
} else {
$db->insertObject('#__schemaorg', $entry, 'id');
}
}
/**
* This event is triggered before the framework creates the Head section of the Document
*
* @return void
*
* @since 5.0.0
*/
public function onBeforeCompileHead(): void
{
$app = $this->getApplication();
$baseType = $this->params->get('baseType', 'organization');
$itemId = (int) $app->getInput()->getInt('id');
$option = $app->getInput()->get('option');
$view = $app->getInput()->get('view');
$context = $option . '.' . $view;
// We need the plugin configured at least once to add structured data
if (!$app->isClient('site') || !\in_array($baseType, ['organization', 'person']) || !$this->isSupported($context)) {
return;
}
$domain = Uri::root();
$isPerson = $baseType === 'person';
$schema = new Registry();
$baseSchema = [];
$baseSchema['@context'] = 'https://schema.org';
$baseSchema['@graph'] = [];
// Add base tag Person/Organization
$baseId = $domain . '#/schema/' . ucfirst($baseType) . '/base';
$siteSchema = [];
$siteSchema['@type'] = ucfirst($baseType);
$siteSchema['@id'] = $baseId;
$name = $this->params->get('name', $app->get('sitename'));
if ($isPerson && $this->params->get('user') > 0) {
$user = $this->getUserFactory()->loadUserById($this->params->get('user'));
$name = $user ? $user->name : '';
}
if ($name) {
$siteSchema['name'] = $name;
}
$siteSchema['url'] = $domain;
// Image
$image = $this->params->get('image') ? HTMLHelper::_('cleanimageUrl', $this->params->get('image')) : false;
if ($image !== false) {
$siteSchema['logo'] = [
'@type' => 'ImageObject',
'@id' => $domain . '#/schema/ImageObject/logo',
'url' => $image->url,
'contentUrl' => $image->url,
'width' => $image->attributes['width'] ?? 0,
'height' => $image->attributes['height'] ?? 0,
];
$siteSchema['image'] = ['@id' => $siteSchema['logo']['@id']];
}
// Social media accounts
$socialMedia = (array) $this->params->get('socialmedia', []);
if (!empty($socialMedia)) {
$siteSchema['sameAs'] = [];
}
foreach ($socialMedia as $social) {
$siteSchema['sameAs'][] = $social->url;
}
$baseSchema['@graph'][] = $siteSchema;
// Add WebSite
$webSiteId = $domain . '#/schema/WebSite/base';
$webSiteSchema = [];
$webSiteSchema['@type'] = 'WebSite';
$webSiteSchema['@id'] = $webSiteId;
$webSiteSchema['url'] = $domain;
$webSiteSchema['name'] = $app->get('sitename');
$webSiteSchema['publisher'] = ['@id' => $baseId];
// We support Finder actions
$finder = ModuleHelper::getModule('mod_finder');
if (!empty($finder->id)) {
$webSiteSchema['potentialAction'] = [
'@type' => 'SearchAction',
'target' => Route::_('index.php?option=com_finder&view=search&q={search_term_string}', true, Route::TLS_IGNORE, true),
'query-input' => 'required name=search_term_string',
];
}
$baseSchema['@graph'][] = $webSiteSchema;
// Add WebPage
$webPageId = $domain . '#/schema/WebPage/base';
$webPageSchema = [];
$webPageSchema['@type'] = 'WebPage';
$webPageSchema['@id'] = $webPageId;
$webPageSchema['url'] = htmlspecialchars(Uri::getInstance()->toString());
$webPageSchema['name'] = $app->getDocument()->getTitle();
$webPageSchema['description'] = $app->getDocument()->getDescription();
$webPageSchema['isPartOf'] = ['@id' => $webSiteId];
$webPageSchema['about'] = ['@id' => $baseId];
$webPageSchema['inLanguage'] = $app->getLanguage()->getTag();
// We support Breadcrumb linking
$breadcrumbs = ModuleHelper::getModule('mod_breadcrumbs');
if (!empty($breadcrumbs->id)) {
$webPageSchema['breadcrumb'] = ['@id' => $domain . '#/schema/BreadcrumbList/' . (int) $breadcrumbs->id];
}
$baseSchema['@graph'][] = $webPageSchema;
if ($itemId > 0) {
// Load the table data from the database
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__schemaorg'))
->where($db->quoteName('itemId') . ' = :itemId')
->bind(':itemId', $itemId, ParameterType::INTEGER)
->where($db->quoteName('context') . ' = :context')
->bind(':context', $context, ParameterType::STRING);
$result = $db->setQuery($query)->loadObject();
if ($result) {
$localSchema = new Registry($result->schema);
$localSchema->set('@id', $domain . '#/schema/' . str_replace('.', '/', $context) . '/' . (int) $result->itemId);
$localSchema->set('isPartOf', ['@id' => $webPageId]);
$itemSchema = $localSchema->toArray();
$baseSchema['@graph'][] = $itemSchema;
}
}
$schema->loadArray($baseSchema);
$dispatcher = $this->getDispatcher();
$event = new BeforeCompileHeadEvent('onSchemaBeforeCompileHead', [
'subject' => $schema,
'context' => $context . '.' . $itemId,
]);
PluginHelper::importPlugin('schemaorg', null, true, $dispatcher);
$dispatcher->dispatch('onSchemaBeforeCompileHead', $event);
$data = $schema->get('@graph');
foreach ($data as $key => $entry) {
$data[$key] = $this->cleanupSchema($entry);
}
$schema->set('@graph', $data);
$prettyPrint = JDEBUG ? JSON_PRETTY_PRINT : 0;
$schemaString = $schema->toString('JSON', ['bitmask' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | $prettyPrint]);
if ($schemaString !== '{}') {
$wa = $this->getApplication()->getDocument()->getWebAssetManager();
$wa->addInlineScript($schemaString, ['name' => 'inline.schemaorg'], ['type' => 'application/ld+json']);
}
}
/**
* Clean the schema and remove empty fields
*
* @param array $schema
*
* @return array
*
* @since 5.0.0
*/
private function cleanupSchema($schema)
{
$result = [];
foreach ($schema as $key => $value) {
if (\is_array($value)) {
// Subtypes need special handling
if (!empty($value['@type'])) {
if ($value['@type'] === 'ImageObject') {
if (!empty($value['url'])) {
$value['url'] = $this->prepareImage($value['url']);
}
if (empty($value['url'])) {
$value = [];
}
} elseif ($value['@type'] === 'Date') {
if (!empty($value['value'])) {
$value['value'] = $this->prepareDate($value['value']);
}
if (empty($value['value'])) {
$value = [];
}
}
// Go into the array
$value = $this->cleanupSchema($value);
// We don't save when the array contains only the @type
if (empty($value) || \count($value) <= 1) {
$value = null;
}
} elseif ($key == 'genericField') {
foreach ($value as $field) {
$result[$field['genericTitle']] = $field['genericValue'];
}
continue;
}
}
// No data, no play
if (empty($value)) {
continue;
}
$result[$key] = $value;
}
return $result;
}
/**
* Check if the current plugin should execute schemaorg related activities
*
* @param string $context
*
* @return boolean
*
* @since 5.0.0
*/
protected function isSupported($context)
{
// We need at least the extension + view for loading the table fields
if (!str_contains($context, '.')) {
return false;
}
$parts = explode('.', $context, 2);
$component = $this->getApplication()->bootComponent($parts[0]);
return $component instanceof SchemaorgServiceInterface;
}
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_sef</name>
<author>Joomla! Project</author>
<creationDate>2007-12</creationDate>
<copyright>(C) 2007 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_SEF_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Sef</namespace>
<files>
<folder plugin="sef">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_sef.ini</language>
<language tag="en-GB">language/en-GB/plg_system_sef.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="domain"
type="url"
label="PLG_SEF_DOMAIN_LABEL"
description="PLG_SEF_DOMAIN_DESCRIPTION"
hint="https://www.example.com"
filter="url"
validate="url"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.sef
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Sef\Extension\Sef;
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) {
$plugin = new Sef(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'sef')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,218 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.sef
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Sef\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla! SEF Plugin.
*
* @since 1.5
*/
final class Sef extends CMSPlugin
{
/**
* Add the canonical uri to the head.
*
* @return void
*
* @since 3.5
*/
public function onAfterDispatch()
{
$doc = $this->getApplication()->getDocument();
if (!$this->getApplication()->isClient('site') || $doc->getType() !== 'html') {
return;
}
$sefDomain = $this->params->get('domain', false);
// Don't add a canonical html tag if no alternative domain has added in SEF plugin domain field.
if (empty($sefDomain)) {
return;
}
// Check if a canonical html tag already exists (for instance, added by a component).
$canonical = '';
foreach ($doc->_links as $linkUrl => $link) {
if (isset($link['relation']) && $link['relation'] === 'canonical') {
$canonical = $linkUrl;
break;
}
}
// If a canonical html tag already exists get the canonical and change it to use the SEF plugin domain field.
if (!empty($canonical)) {
// Remove current canonical link.
unset($doc->_links[$canonical]);
// Set the current canonical link but use the SEF system plugin domain field.
$canonical = $sefDomain . Uri::getInstance($canonical)->toString(['path', 'query', 'fragment']);
} else {
// If a canonical html doesn't exists already add a canonical html tag using the SEF plugin domain field.
$canonical = $sefDomain . Uri::getInstance()->toString(['path', 'query', 'fragment']);
}
// Add the canonical link.
$doc->addHeadLink(htmlspecialchars($canonical), 'canonical');
}
/**
* Convert the site URL to fit to the HTTP request.
*
* @return void
*/
public function onAfterRender()
{
if (!$this->getApplication()->isClient('site')) {
return;
}
// Replace src links.
$base = Uri::base(true) . '/';
$buffer = $this->getApplication()->getBody();
// For feeds we need to search for the URL with domain.
$prefix = $this->getApplication()->getDocument()->getType() === 'feed' ? Uri::root() : '';
// Replace index.php URI by SEF URI.
if (strpos($buffer, 'href="' . $prefix . 'index.php?') !== false) {
preg_match_all('#href="' . $prefix . 'index.php\?([^"]+)"#m', $buffer, $matches);
foreach ($matches[1] as $urlQueryString) {
$buffer = str_replace(
'href="' . $prefix . 'index.php?' . $urlQueryString . '"',
'href="' . trim($prefix, '/') . Route::_('index.php?' . $urlQueryString) . '"',
$buffer
);
}
$this->checkBuffer($buffer);
}
// Check for all unknown protocols (a protocol must contain at least one alphanumeric character followed by a ":").
$protocols = '[a-zA-Z0-9\-]+:';
$attributes = ['href=', 'src=', 'poster='];
foreach ($attributes as $attribute) {
if (strpos($buffer, $attribute) !== false) {
$regex = '#\s' . $attribute . '"(?!/|' . $protocols . '|\#|\')([^"]*)"#m';
$buffer = preg_replace($regex, ' ' . $attribute . '"' . $base . '$1"', $buffer);
$this->checkBuffer($buffer);
}
}
if (strpos($buffer, 'srcset=') !== false) {
$regex = '#\s+srcset="([^"]+)"#m';
$buffer = preg_replace_callback(
$regex,
function ($match) use ($base, $protocols) {
preg_match_all('#(?:[^\s]+)\s*(?:[\d\.]+[wx])?(?:\,\s*)?#i', $match[1], $matches);
foreach ($matches[0] as &$src) {
$src = preg_replace('#^(?!/|' . $protocols . '|\#|\')(.+)#', $base . '$1', $src);
}
return ' srcset="' . implode($matches[0]) . '"';
},
$buffer
);
$this->checkBuffer($buffer);
}
// Replace all unknown protocols in javascript window open events.
if (strpos($buffer, 'window.open(') !== false) {
$regex = '#onclick="window.open\(\'(?!/|' . $protocols . '|\#)([^/]+[^\']*?\')#m';
$buffer = preg_replace($regex, 'onclick="window.open(\'' . $base . '$1', $buffer);
$this->checkBuffer($buffer);
}
// Replace all unknown protocols in onmouseover and onmouseout attributes.
$attributes = ['onmouseover=', 'onmouseout='];
foreach ($attributes as $attribute) {
if (strpos($buffer, $attribute) !== false) {
$regex = '#' . $attribute . '"this.src=([\']+)(?!/|' . $protocols . '|\#|\')([^"]+)"#m';
$buffer = preg_replace($regex, $attribute . '"this.src=$1' . $base . '$2"', $buffer);
$this->checkBuffer($buffer);
}
}
// Replace all unknown protocols in CSS background image.
if (strpos($buffer, 'style=') !== false) {
$regex_url = '\s*url\s*\(([\'\"]|\&\#0?3[49];)?(?!/|\&\#0?3[49];|' . $protocols . '|\#)([^\)\'\"]+)([\'\"]|\&\#0?3[49];)?\)';
$regex = '#style=\s*([\'\"])(.*):' . $regex_url . '#m';
$buffer = preg_replace($regex, 'style=$1$2: url($3' . $base . '$4$5)', $buffer);
$this->checkBuffer($buffer);
}
// Replace all unknown protocols in OBJECT param tag.
if (strpos($buffer, '<param') !== false) {
// OBJECT <param name="xx", value="yy"> -- fix it only inside the <param> tag.
$regex = '#(<param\s+)name\s*=\s*"(movie|src|url)"[^>]\s*value\s*=\s*"(?!/|' . $protocols . '|\#|\')([^"]*)"#m';
$buffer = preg_replace($regex, '$1name="$2" value="' . $base . '$3"', $buffer);
$this->checkBuffer($buffer);
// OBJECT <param value="xx", name="yy"> -- fix it only inside the <param> tag.
$regex = '#(<param\s+[^>]*)value\s*=\s*"(?!/|' . $protocols . '|\#|\')([^"]*)"\s*name\s*=\s*"(movie|src|url)"#m';
$buffer = preg_replace($regex, '<param value="' . $base . '$2" name="$3"', $buffer);
$this->checkBuffer($buffer);
}
// Replace all unknown protocols in OBJECT tag.
if (strpos($buffer, '<object') !== false) {
$regex = '#(<object\s+[^>]*)data\s*=\s*"(?!/|' . $protocols . '|\#|\')([^"]*)"#m';
$buffer = preg_replace($regex, '$1data="' . $base . '$2"', $buffer);
$this->checkBuffer($buffer);
}
// Use the replaced HTML body.
$this->getApplication()->setBody($buffer);
}
/**
* Check the buffer.
*
* @param string $buffer Buffer to be checked.
*
* @return void
*/
private function checkBuffer($buffer)
{
if ($buffer === null) {
switch (preg_last_error()) {
case PREG_BACKTRACK_LIMIT_ERROR:
$message = 'PHP regular expression limit reached (pcre.backtrack_limit)';
break;
case PREG_RECURSION_LIMIT_ERROR:
$message = 'PHP regular expression limit reached (pcre.recursion_limit)';
break;
case PREG_BAD_UTF8_ERROR:
$message = 'Bad UTF8 passed to PCRE function';
break;
default:
$message = 'Unknown PCRE error calling PCRE function';
}
throw new \RuntimeException($message);
}
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.shortcut
*
* @copyright (C) 2022 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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Shortcut\Extension\Shortcut;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.2.0
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new Shortcut(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'shortcut')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_shortcut</name>
<author>Joomla! Project</author>
<creationDate>2022-06</creationDate>
<copyright>(C) 2022 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>4.2.0</version>
<description>PLG_SYSTEM_SHORTCUT_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Shortcut</namespace>
<media destination="plg_system_shortcut" folder="media">
<folder>js</folder>
</media>
<files>
<folder plugin="shortcut">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_shortcut.ini</language>
<language tag="en-GB">language/en-GB/plg_system_shortcut.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="timeout"
type="number"
label="PLG_SYSTEM_SHORTCUT_TIMEOUT_LABEL"
description="PLG_SYSTEM_SHORTCUT_TIMEOUT_DESC"
required="true"
start="1"
step="1"
default="2000"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,136 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.shortcut
*
* @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\Shortcut\Extension;
use Joomla\CMS\Event\GenericEvent;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Shortcut plugin to add accessible keyboard shortcuts to the administrator templates.
*
* @since 4.2.0
*/
final class Shortcut extends CMSPlugin implements SubscriberInterface
{
/**
* Returns an array of events this subscriber will listen to.
*
* The array keys are event names and the value can be:
*
* - The method name to call (priority defaults to 0)
* - An array composed of the method name to call and the priority
*
* For instance:
*
* * array('eventName' => 'methodName')
* * array('eventName' => array('methodName', $priority))
*
* @return array
*
* @since 4.2.0
*/
public static function getSubscribedEvents(): array
{
return [
'onBeforeCompileHead' => 'initialize',
'onLoadShortcuts' => 'addShortcuts',
];
}
/**
* Add the javascript for the shortcuts
*
* @return void
*
* @since 4.2.0
*/
public function initialize()
{
if (!$this->getApplication()->isClient('administrator')) {
return;
}
// Load translations
$this->loadLanguage();
$context = $this->getApplication()->getInput()->get('option') . '.' . $this->getApplication()->getInput()->get('view');
$shortcuts = [];
$event = new GenericEvent(
'onLoadShortcuts',
[
'context' => $context,
'shortcuts' => $shortcuts,
]
);
$this->getDispatcher()->dispatch('onLoadShortcuts', $event);
$shortcuts = $event->getArgument('shortcuts');
Text::script('PLG_SYSTEM_SHORTCUT_OVERVIEW_HINT');
Text::script('PLG_SYSTEM_SHORTCUT_OVERVIEW_TITLE');
Text::script('PLG_SYSTEM_SHORTCUT_OVERVIEW_DESC');
Text::script('PLG_SYSTEM_SHORTCUT_THEN');
Text::script('JCLOSE');
$document = $this->getApplication()->getDocument();
$wa = $document->getWebAssetManager();
$wa->useScript('bootstrap.modal');
$wa->registerAndUseScript('script', 'plg_system_shortcut/shortcut.min.js', ['dependencies' => ['hotkeysjs']]);
$timeout = $this->params->get('timeout', 2000);
$document->addScriptOptions('plg_system_shortcut.shortcuts', $shortcuts);
$document->addScriptOptions('plg_system_shortcut.timeout', $timeout);
}
/**
* Add default shortcuts to the document
*
* @param Event $event The event
*
* @return void
*
* @since 4.2.0
*/
public function addShortcuts(Event $event)
{
$shortcuts = $event->getArgument('shortcuts', []);
$shortcuts = array_merge(
$shortcuts,
[
'applyKey' => (object) ['selector' => 'joomla-toolbar-button .button-apply', 'shortcut' => 'A', 'title' => $this->getApplication()->getLanguage()->_('JAPPLY')],
'saveKey' => (object) ['selector' => 'joomla-toolbar-button .button-save', 'shortcut' => 'S', 'title' => $this->getApplication()->getLanguage()->_('JTOOLBAR_SAVE')],
'cancelKey' => (object) ['selector' => 'joomla-toolbar-button .button-cancel', 'shortcut' => 'Q', 'title' => $this->getApplication()->getLanguage()->_('JCANCEL')],
'newKey' => (object) ['selector' => 'joomla-toolbar-button .button-new', 'shortcut' => 'N', 'title' => $this->getApplication()->getLanguage()->_('JTOOLBAR_NEW')],
'searchKey' => (object) ['selector' => 'input[placeholder=' . $this->getApplication()->getLanguage()->_('JSEARCH_FILTER') . ']', 'shortcut' => 'F', 'title' => $this->getApplication()->getLanguage()->_('JSEARCH_FILTER')],
'optionKey' => (object) ['selector' => 'joomla-toolbar-button .button-options', 'shortcut' => 'O', 'title' => $this->getApplication()->getLanguage()->_('JOPTIONS')],
'helpKey' => (object) ['selector' => 'joomla-toolbar-button .button-help', 'shortcut' => 'H', 'title' => $this->getApplication()->getLanguage()->_('JHELP')],
'toggleMenu' => (object) ['selector' => '#menu-collapse', 'shortcut' => 'M', 'title' => $this->getApplication()->getLanguage()->_('JTOGGLE_SIDEBAR_MENU')],
'dashboard' => (object) ['selector' => (string) new Uri(Route::_('index.php?')), 'shortcut' => 'D', 'title' => $this->getApplication()->getLanguage()->_('JHOMEDASHBOARD')],
]
);
$event->setArgument('shortcuts', $shortcuts);
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.skipto
*
* @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\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Skipto\Extension\Skipto;
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) {
$plugin = new Skipto(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'skipto')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_skipto</name>
<author>Joomla! Project</author>
<creationDate>2020-02</creationDate>
<copyright>(C) 2019 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>4.0.0</version>
<description>PLG_SYSTEM_SKIPTO_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Skipto</namespace>
<files>
<folder plugin="skipto">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_skipto.ini</language>
<language tag="en-GB">language/en-GB/plg_system_skipto.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="section"
type="list"
label="PLG_SYSTEM_SKIPTO_SECTION"
default="administrator"
validate="options"
>
<option value="site">PLG_SYSTEM_SKIPTO_SECTION_SITE</option>
<option value="administrator">PLG_SYSTEM_SKIPTO_SECTION_ADMIN</option>
<option value="both">PLG_SYSTEM_SKIPTO_SECTION_BOTH</option>
</field>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,102 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.skipto
*
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Skipto\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Skipto plugin to add accessible keyboard navigation to the site and administrator templates.
*
* @since 4.0.0
*/
final class Skipto extends CMSPlugin
{
/**
* Add the skipto navigation menu.
*
* @return void
*
* @since 4.0.0
*/
public function onAfterDispatch()
{
$section = $this->params->get('section', 'administrator');
if ($section !== 'both' && $this->getApplication()->isClient($section) !== true) {
return;
}
// Get the document object.
$document = $this->getApplication()->getDocument();
if ($document->getType() !== 'html') {
return;
}
// Are we in a modal?
if ($this->getApplication()->getInput()->get('tmpl', '', 'cmd') === 'component') {
return;
}
// Load language file.
$this->loadLanguage();
// Add plugin settings and strings for translations in JavaScript.
$document->addScriptOptions(
'skipto-settings',
[
'settings' => [
'skipTo' => [
// Feature switches
'enableActions' => false,
'enableHeadingLevelShortcuts' => false,
// Customization of button and menu
'accesskey' => '9',
'displayOption' => 'popup',
// Button labels and messages
'buttonLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_TITLE'),
'buttonTooltipAccesskey' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_ACCESS_KEY'),
// Menu labels and messages
'landmarkGroupLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK'),
'headingGroupLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_HEADING'),
'mofnGroupLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_HEADING_MOFN'),
'headingLevelLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_HEADING_LEVEL'),
'mainLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_MAIN'),
'searchLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_SEARCH'),
'navLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_NAV'),
'regionLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_REGION'),
'asideLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_ASIDE'),
'footerLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_FOOTER'),
'headerLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_HEADER'),
'formLabel' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_FORM'),
'msgNoLandmarksFound' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_LANDMARK_NONE'),
'msgNoHeadingsFound' => $this->getApplication()->getLanguage()->_('PLG_SYSTEM_SKIPTO_HEADING_NONE'),
// Selectors for landmark and headings sections
'headings' => 'h1, h2, h3',
'landmarks' => 'main, nav, search, aside, header, footer, form',
],
],
]
);
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $document->getWebAssetManager();
$wa->useScript('skipto');
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2016 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\Factory;
use Joomla\CMS\Language\Text;
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseScript('plg_system_stats.stats', 'plg_system_stats/stats.js', [], ['defer' => true], ['core']);
extract($displayData);
/**
* Layout variables
* -----------------
* @var string $autocomplete Autocomplete attribute for the field.
* @var boolean $autofocus Is autofocus enabled?
* @var string $class Classes for the input.
* @var string $description Description of the field.
* @var boolean $disabled Is this field disabled?
* @var string $group Group the field belongs to. <fields> section in form XML.
* @var boolean $hidden Is this field hidden in the form?
* @var string $hint Placeholder for the field.
* @var string $id DOM id of the field.
* @var string $label Label of the field.
* @var string $labelclass Classes to apply to the label.
* @var boolean $multiple Does this field support multiple values?
* @var string $name Name of the input field.
* @var string $onchange Onchange attribute for the field.
* @var string $onclick Onclick attribute for the field.
* @var string $pattern Pattern (Reg Ex) of value of the form field.
* @var boolean $readonly Is this field read only?
* @var boolean $repeat Allows extensions to duplicate elements.
* @var boolean $required Is this field required?
* @var integer $size Size attribute of the input.
* @var boolean $spellcheck Spellcheck state for the form field.
* @var string $validate Validation rules to apply.
* @var string $value Value attribute of the field.
* @var array $options Options available for this field.
* @var array $statsData Statistics that will be sent to the stats server
*/
?>
<?php if (count($statsData)) : ?>
<a href="#" id="js-pstats-data-details-toggler"><?php echo Text::_('PLG_SYSTEM_STATS_MSG_WHAT_DATA_WILL_BE_SENT'); ?></a>
<?php echo $field->render('stats', compact('statsData')); ?>
<?php endif; ?>

View File

@ -0,0 +1,49 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2016 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\Language\Text;
extract($displayData);
/**
* Layout variables
* -----------------
* @var string $autocomplete Autocomplete attribute for the field.
* @var boolean $autofocus Is autofocus enabled?
* @var string $class Classes for the input.
* @var string $description Description of the field.
* @var boolean $disabled Is this field disabled?
* @var string $group Group the field belongs to. <fields> section in form XML.
* @var boolean $hidden Is this field hidden in the form?
* @var string $hint Placeholder for the field.
* @var string $id DOM id of the field.
* @var string $label Label of the field.
* @var string $labelclass Classes to apply to the label.
* @var boolean $multiple Does this field support multiple values?
* @var string $name Name of the input field.
* @var string $onchange Onchange attribute for the field.
* @var string $onclick Onclick attribute for the field.
* @var string $pattern Pattern (Reg Ex) of value of the form field.
* @var boolean $readonly Is this field read only?
* @var boolean $repeat Allows extensions to duplicate elements.
* @var boolean $required Is this field required?
* @var integer $size Size attribute of the input.
* @var boolean $spellcheck Spellcheck state for the form field.
* @var string $validate Validation rules to apply.
* @var string $value Value attribute of the field.
* @var array $options Options available for this field.
*/
?>
<input type="hidden" name="<?php echo $name; ?>" id="<?php echo $id; ?>" value="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>">
<button class="btn btn-secondary" type="button" id="js-pstats-reset-uid">
<span class="icon-sync"></span> <?php echo Text::_('PLG_SYSTEM_STATS_RESET_UNIQUE_ID'); ?>
</button>

View File

@ -0,0 +1,47 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2015 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\Language\Text;
use Joomla\Registry\Registry;
extract($displayData);
/**
* Layout variables
* -----------------
* @var PlgSystemStats $plugin Plugin rendering this layout
* @var Registry $pluginParams Plugin parameters
* @var array $statsData Array containing the data that will be sent to the stats server
*/
?>
<joomla-alert type="info" dismiss class="js-pstats-alert hidden" role="alertdialog" close-text="<?php echo Text::_('JCLOSE'); ?>" aria-labelledby="alert-stats-heading">
<div class="alert-heading" id="alert-stats-heading"><?php echo Text::_('PLG_SYSTEM_STATS_LABEL_MESSAGE_TITLE'); ?></div>
<div>
<div class="alert-message">
<p>
<?php echo Text::_('PLG_SYSTEM_STATS_MSG_JOOMLA_WANTS_TO_SEND_DATA'); ?>
</p>
<p>
<a href="#" class="js-pstats-btn-details alert-link"><?php echo Text::_('PLG_SYSTEM_STATS_MSG_WHAT_DATA_WILL_BE_SENT'); ?></a>
</p>
<?php
echo $plugin->render('stats', compact('statsData'));
?>
<p class="fw-bold"><?php echo Text::_('PLG_SYSTEM_STATS_MSG_ALLOW_SENDING_DATA'); ?></p>
<p class="actions">
<button type="button" class="btn btn-primary js-pstats-btn-allow-never"><?php echo Text::_('PLG_SYSTEM_STATS_BTN_NEVER_SEND'); ?></button>
<button type="button" class="btn btn-primary js-pstats-btn-allow-always"><?php echo Text::_('PLG_SYSTEM_STATS_BTN_SEND_ALWAYS'); ?></button>
</p>
</div>
</div>
</joomla-alert>

View File

@ -0,0 +1,47 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2016 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\Language\Text;
extract($displayData);
/**
* Layout variables
* -----------------
* @var array $statsData Array containing the data that will be sent to the stats server
*/
$versionFields = ['php_version', 'db_version', 'cms_version'];
?>
<table class="table mb-3 d-none" id="js-pstats-data-details">
<caption class="visually-hidden">
<?php echo Text::_('PLG_SYSTEM_STATS_STATISTICS'); ?>
</caption>
<thead>
<tr>
<th scope="col" class="w-15">
<?php echo Text::_('PLG_SYSTEM_STATS_SETTING'); ?>
</th>
<th scope="col">
<?php echo Text::_('PLG_SYSTEM_STATS_VALUE'); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($statsData as $key => $value) : ?>
<tr>
<th scope="row"><?php echo Text::_('PLG_SYSTEM_STATS_LABEL_' . strtoupper($key)); ?></th>
<td><?php echo in_array($key, $versionFields) ? (preg_match('/\d+(?:\.\d+)+/', $value, $matches) ? $matches[0] : $value) : $value; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>

View File

@ -0,0 +1,48 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @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\Stats\Extension\Stats;
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) {
$plugin = new Stats(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'stats')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,620 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Stats\Extension;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\User\UserHelper;
use Joomla\Database\DatabaseAwareTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
// Uncomment the following line to enable debug mode for testing purposes. Note: statistics will be sent on every page load
// define('PLG_SYSTEM_STATS_DEBUG', 1);
/**
* Statistics system plugin. This sends anonymous data back to the Joomla! Project about the
* PHP, SQL, Joomla and OS versions
*
* @since 3.5
*/
final class Stats extends CMSPlugin
{
use DatabaseAwareTrait;
/**
* Indicates sending statistics is always allowed.
*
* @var integer
*
* @since 3.5
*/
public const MODE_ALLOW_ALWAYS = 1;
/**
* Indicates sending statistics is never allowed.
*
* @var integer
*
* @since 3.5
*/
public const MODE_ALLOW_NEVER = 3;
/**
* URL to send the statistics.
*
* @var string
*
* @since 3.5
*/
protected $serverUrl = 'https://developer.joomla.org/stats/submit';
/**
* Unique identifier for this site
*
* @var string
*
* @since 3.5
*/
protected $uniqueId;
/**
* Listener for the `onAfterInitialise` event
*
* @return void
*
* @since 3.5
*/
public function onAfterInitialise()
{
if (!$this->getApplication()->isClient('administrator') || !$this->isAllowedUser()) {
return;
}
if ($this->isCaptiveMFA()) {
return;
}
if (!$this->isDebugEnabled() && !$this->isUpdateRequired()) {
return;
}
if ($this->getApplication()->getInput()->getVar('tmpl') === 'component') {
return;
}
// Load plugin language files only when needed (ex: they are not needed in site client).
$this->loadLanguage();
}
/**
* Listener for the `onAfterDispatch` event
*
* @return void
*
* @since 4.0.0
*/
public function onAfterDispatch()
{
if (!$this->getApplication()->isClient('administrator') || !$this->isAllowedUser()) {
return;
}
if ($this->isCaptiveMFA()) {
return;
}
if (!$this->isDebugEnabled() && !$this->isUpdateRequired()) {
return;
}
if ($this->getApplication()->getInput()->getVar('tmpl') === 'component') {
return;
}
if ($this->getApplication()->getDocument()->getType() !== 'html') {
return;
}
$this->getApplication()->getDocument()->getWebAssetManager()
->registerAndUseScript('plg_system_stats.message', 'plg_system_stats/stats-message.js', [], ['defer' => true], ['core']);
}
/**
* User selected to always send data
*
* @return void
*
* @since 3.5
*
* @throws \Exception If user is not allowed.
* @throws \RuntimeException If there is an error saving the params or sending the data.
*/
public function onAjaxSendAlways()
{
if (!$this->isAllowedUser() || !$this->isAjaxRequest()) {
throw new \Exception($this->getApplication()->getLanguage()->_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
}
$this->params->set('mode', static::MODE_ALLOW_ALWAYS);
if (!$this->saveParams()) {
throw new \RuntimeException('Unable to save plugin settings', 500);
}
echo json_encode(['sent' => (int) $this->sendStats()]);
}
/**
* User selected to never send data.
*
* @return void
*
* @since 3.5
*
* @throws \Exception If user is not allowed.
* @throws \RuntimeException If there is an error saving the params.
*/
public function onAjaxSendNever()
{
if (!$this->isAllowedUser() || !$this->isAjaxRequest()) {
throw new \Exception($this->getApplication()->getLanguage()->_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
}
$this->params->set('mode', static::MODE_ALLOW_NEVER);
if (!$this->saveParams()) {
throw new \RuntimeException('Unable to save plugin settings', 500);
}
if (!$this->disablePlugin()) {
throw new \RuntimeException('Unable to disable the statistics plugin', 500);
}
echo json_encode(['sent' => 0]);
}
/**
* Send the stats to the server.
* On first load | on demand mode it will show a message asking users to select mode.
*
* @return void
*
* @since 3.5
*
* @throws \Exception If user is not allowed.
* @throws \RuntimeException If there is an error saving the params, disabling the plugin or sending the data.
*/
public function onAjaxSendStats()
{
if (!$this->isAllowedUser() || !$this->isAjaxRequest()) {
throw new \Exception($this->getApplication()->getLanguage()->_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
}
// User has not selected the mode. Show message.
if ((int) $this->params->get('mode') !== static::MODE_ALLOW_ALWAYS) {
$data = [
'sent' => 0,
'html' => $this->getRenderer('message')->render($this->getLayoutData()),
];
echo json_encode($data);
return;
}
if (!$this->saveParams()) {
throw new \RuntimeException('Unable to save plugin settings', 500);
}
echo json_encode(['sent' => (int) $this->sendStats()]);
}
/**
* Get the data through events
*
* @param string $context Context where this will be called from
*
* @return array
*
* @since 3.5
*/
public function onGetStatsData($context)
{
return $this->getStatsData();
}
/**
* Debug a layout of this plugin
*
* @param string $layoutId Layout identifier
* @param array $data Optional data for the layout
*
* @return string
*
* @since 3.5
*/
public function debug($layoutId, $data = [])
{
$data = array_merge($this->getLayoutData(), $data);
return $this->getRenderer($layoutId)->debug($data);
}
/**
* Get the data for the layout
*
* @return array
*
* @since 3.5
*/
private function getLayoutData()
{
return [
'plugin' => $this,
'pluginParams' => $this->params,
'statsData' => $this->getStatsData(),
];
}
/**
* Get the layout paths
*
* @return array
*
* @since 3.5
*/
private function getLayoutPaths()
{
$template = $this->getApplication()->getTemplate();
return [
JPATH_ADMINISTRATOR . '/templates/' . $template . '/html/layouts/plugins/' . $this->_type . '/' . $this->_name,
JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/layouts',
];
}
/**
* Get the plugin renderer
*
* @param string $layoutId Layout identifier
*
* @return \Joomla\CMS\Layout\LayoutInterface
*
* @since 3.5
*/
private function getRenderer($layoutId = 'default')
{
$renderer = new FileLayout($layoutId);
$renderer->setIncludePaths($this->getLayoutPaths());
return $renderer;
}
/**
* Get the data that will be sent to the stats server.
*
* @return array
*
* @since 3.5
*/
private function getStatsData()
{
$data = [
'unique_id' => $this->getUniqueId(),
'php_version' => PHP_VERSION,
'db_type' => $this->getDatabase()->name,
'db_version' => $this->getDatabase()->getVersion(),
'cms_version' => JVERSION,
'server_os' => php_uname('s') . ' ' . php_uname('r'),
];
// Check if we have a MariaDB version string and extract the proper version from it
if (preg_match('/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i', $data['db_version'], $versionParts)) {
$data['db_version'] = $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
}
return $data;
}
/**
* Get the unique id. Generates one if none is set.
*
* @return integer
*
* @since 3.5
*/
private function getUniqueId()
{
if (null === $this->uniqueId) {
$this->uniqueId = $this->params->get('unique_id', hash('sha1', UserHelper::genRandomPassword(28) . time()));
}
return $this->uniqueId;
}
/**
* Check if current user is allowed to send the data
*
* @return boolean
*
* @since 3.5
*/
private function isAllowedUser()
{
return $this->getApplication()->getIdentity() && $this->getApplication()->getIdentity()->authorise('core.admin');
}
/**
* Check if the debug is enabled
*
* @return boolean
*
* @since 3.5
*/
private function isDebugEnabled()
{
return \defined('PLG_SYSTEM_STATS_DEBUG');
}
/**
* Check if last_run + interval > now
*
* @return boolean
*
* @since 3.5
*/
private function isUpdateRequired()
{
$last = (int) $this->params->get('lastrun', 0);
$interval = (int) $this->params->get('interval', 12);
$mode = (int) $this->params->get('mode', 0);
if ($mode === static::MODE_ALLOW_NEVER) {
return false;
}
// Never updated or debug enabled
if (!$last || $this->isDebugEnabled()) {
return true;
}
return abs(time() - $last) > $interval * 3600;
}
/**
* Check valid AJAX request
*
* @return boolean
*
* @since 3.5
*/
private function isAjaxRequest()
{
return strtolower($this->getApplication()->getInput()->server->get('HTTP_X_REQUESTED_WITH', '')) === 'xmlhttprequest';
}
/**
* Render a layout of this plugin
*
* @param string $layoutId Layout identifier
* @param array $data Optional data for the layout
*
* @return string
*
* @since 3.5
*/
public function render($layoutId, $data = [])
{
$data = array_merge($this->getLayoutData(), $data);
return $this->getRenderer($layoutId)->render($data);
}
/**
* Save the plugin parameters
*
* @return boolean
*
* @since 3.5
*/
private function saveParams()
{
// Update params
$this->params->set('lastrun', time());
$this->params->set('unique_id', $this->getUniqueId());
$interval = (int) $this->params->get('interval', 12);
$this->params->set('interval', $interval ?: 12);
$paramsJson = $this->params->toString('JSON');
$db = $this->getDatabase();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = :params')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('stats'))
->bind(':params', $paramsJson);
try {
// Lock the tables to prevent multiple plugin executions causing a race condition
$db->lockTable('#__extensions');
} catch (\Exception $e) {
// If we can't lock the tables it's too risky to continue execution
return false;
}
try {
// Update the plugin parameters
$result = $db->setQuery($query)->execute();
$this->clearCacheGroups(['com_plugins']);
} catch (\Exception $exc) {
// If we failed to execute
$db->unlockTables();
$result = false;
}
try {
// Unlock the tables after writing
$db->unlockTables();
} catch (\Exception $e) {
// If we can't lock the tables assume we have somehow failed
$result = false;
}
return $result;
}
/**
* Send the stats to the stats server
*
* @return boolean
*
* @since 3.5
*
* @throws \RuntimeException If there is an error sending the data and debug mode enabled.
*/
private function sendStats()
{
$error = false;
try {
// Don't let the request take longer than 2 seconds to avoid page timeout issues
$response = HttpFactory::getHttp()->post($this->serverUrl, $this->getStatsData(), [], 2);
if (!$response) {
$error = 'Could not send site statistics to remote server: No response';
} elseif ($response->code !== 200) {
$data = json_decode($response->body);
$error = 'Could not send site statistics to remote server: ' . $data->message;
}
} catch (\UnexpectedValueException $e) {
// There was an error sending stats. Should we do anything?
$error = 'Could not send site statistics to remote server: ' . $e->getMessage();
} catch (\RuntimeException $e) {
// There was an error connecting to the server or in the post request
$error = 'Could not connect to statistics server: ' . $e->getMessage();
} catch (\Exception $e) {
// An unexpected error in processing; don't let this failure kill the site
$error = 'Unexpected error connecting to statistics server: ' . $e->getMessage();
}
if ($error !== false) {
// Log any errors if logging enabled.
Log::add($error, Log::WARNING, 'jerror');
// If Stats debug mode enabled, or Global Debug mode enabled, show error to the user.
if ($this->isDebugEnabled() || $this->getApplication()->get('debug')) {
throw new \RuntimeException($error, 500);
}
return false;
}
return true;
}
/**
* Clears cache groups. We use it to clear the plugins cache after we update the last run timestamp.
*
* @param array $clearGroups The cache groups to clean
*
* @return void
*
* @since 3.5
*/
private function clearCacheGroups(array $clearGroups)
{
foreach ($clearGroups as $group) {
try {
$options = [
'defaultgroup' => $group,
'cachebase' => $this->getApplication()->get('cache_path', JPATH_CACHE),
];
$cache = Cache::getInstance('callback', $options);
$cache->clean();
} catch (\Exception $e) {
// Ignore it
}
}
}
/**
* Disable this plugin, if user selects once or never, to stop Joomla loading the plugin on every page load and
* therefore regaining a tiny bit of performance
*
* @since 4.0.0
*
* @return boolean
*/
private function disablePlugin()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 0')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('stats'));
try {
// Lock the tables to prevent multiple plugin executions causing a race condition
$db->lockTable('#__extensions');
} catch (\Exception $e) {
// If we can't lock the tables it's too risky to continue execution
return false;
}
try {
// Update the plugin parameters
$result = $db->setQuery($query)->execute();
$this->clearCacheGroups(['com_plugins']);
} catch (\Exception $exc) {
// If we failed to execute
$db->unlockTables();
$result = false;
}
try {
// Unlock the tables after writing
$db->unlockTables();
} catch (\Exception $e) {
// If we can't lock the tables assume we have somehow failed
$result = false;
}
return $result;
}
/**
* Are we in a Multi-factor Authentication page?
*
* @return bool
* @since 4.2.1
*/
private function isCaptiveMFA(): bool
{
return method_exists($this->getApplication(), 'isMultiFactorAuthenticationPage')
&& $this->getApplication()->isMultiFactorAuthenticationPage(true);
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Stats\Field;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Base field for the Stats Plugin.
*
* @since 3.5
*/
abstract class AbstractStatsField extends FormField
{
/**
* Get the layouts paths
*
* @return array
*
* @since 3.5
*/
protected function getLayoutPaths()
{
$template = Factory::getApplication()->getTemplate();
return [
JPATH_ADMINISTRATOR . '/templates/' . $template . '/html/layouts/plugins/system/stats',
JPATH_PLUGINS . '/system/stats/layouts',
JPATH_SITE . '/layouts',
];
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Stats\Field;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Unique ID Field class for the Stats Plugin.
*
* @since 3.5
*/
class DataField extends AbstractStatsField
{
/**
* The form field type.
*
* @var string
* @since 3.5
*/
protected $type = 'Data';
/**
* Name of the layout being used to render the field
*
* @var string
* @since 3.5
*/
protected $layout = 'field.data';
/**
* Method to get the data to be passed to the layout for rendering.
*
* @return array
*
* @since 3.5
*/
protected function getLayoutData()
{
$data = parent::getLayoutData();
PluginHelper::importPlugin('system', 'stats');
$result = Factory::getApplication()->triggerEvent('onGetStatsData', ['stats.field.data']);
$data['statsData'] = $result ? reset($result) : [];
return $data;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.stats
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\Stats\Field;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Unique ID Field class for the Stats Plugin.
*
* @since 3.5
*/
class UniqueidField extends AbstractStatsField
{
/**
* The form field type.
*
* @var string
* @since 3.5
*/
protected $type = 'Uniqueid';
/**
* Name of the layout being used to render the field
*
* @var string
* @since 3.5
*/
protected $layout = 'field.uniqueid';
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_stats</name>
<author>Joomla! Project</author>
<creationDate>2013-11</creationDate>
<copyright>(C) 2013 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.5.0</version>
<description>PLG_SYSTEM_STATS_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\Stats</namespace>
<files>
<folder>layouts</folder>
<folder plugin="stats">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_stats.ini</language>
<language tag="en-GB">language/en-GB/plg_system_stats.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic" addfieldprefix="Joomla\Plugin\System\Stats\Field">
<field
name="data"
type="data"
label=""
/>
<field
name="unique_id"
type="uniqueid"
label="PLG_SYSTEM_STATS_UNIQUE_ID_LABEL"
/>
<field
name="interval"
type="number"
label="PLG_SYSTEM_STATS_INTERVAL_LABEL"
filter="integer"
default="12"
/>
<field
name="mode"
type="list"
label="PLG_SYSTEM_STATS_MODE_LABEL"
default="1"
validate="options"
>
<option value="2">PLG_SYSTEM_STATS_MODE_OPTION_ON_DEMAND</option>
<option value="1">PLG_SYSTEM_STATS_MODE_OPTION_ALWAYS_SEND</option>
<option value="3">PLG_SYSTEM_STATS_MODE_OPTION_NEVER_SEND</option>
</field>
<field
name="lastrun"
type="hidden"
default="0"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="params">
<fields name="notifications">
<fieldset name="notifications">
<field
name="success_mail"
type="radio"
label="PLG_SYSTEM_TASK_NOTIFICATION_LABEL_SUCCESS_MAIL_TOGGLE"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="failure_mail"
type="radio"
label="PLG_SYSTEM_TASK_NOTIFICATION_LABEL_FAILURE_MAIL_TOGGLE"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="fatal_failure_mail"
type="radio"
label="PLG_SYSTEM_TASK_NOTIFICATION_LABEL_FATAL_FAILURE_MAIL_TOGGLE"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
<field
name="orphan_mail"
type="radio"
label="PLG_SYSTEM_TASK_NOTIFICATION_LABEL_ORPHANED_TASK_MAIL_TOGGLE"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JDISABLED</option>
<option value="1">JENABLED</option>
</field>
</fieldset>
</fields>
</fields>
</form>

View File

@ -0,0 +1,50 @@
<?php
/**
* @package Joomla.Plugin
* @subpackage System.tasknotification
*
* @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\CMS\User\UserFactoryInterface;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\TaskNotification\Extension\TaskNotification;
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) {
$plugin = new TaskNotification(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'tasknotification')
);
$plugin->setApplication(Factory::getApplication());
$plugin->setDatabase($container->get(DatabaseInterface::class));
$plugin->setUserFactory($container->get(UserFactoryInterface::class));
return $plugin;
}
);
}
};

View File

@ -0,0 +1,317 @@
<?php
/**
* @package Joomla.Plugins
* @subpackage System.tasknotification
*
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\System\TaskNotification\Extension;
use Joomla\CMS\Event\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Mail\MailTemplate;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Scheduler\Administrator\Task\Status;
use Joomla\Component\Scheduler\Administrator\Task\Task;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
use Joomla\Filesystem\Path;
use PHPMailer\PHPMailer\Exception as MailerException;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* This plugin implements email notification functionality for Tasks configured through the Scheduler component.
* Notification configuration is supported on a per-task basis, which can be set-up through the Task item form, made
* possible by injecting the notification fields into the item form with a `onContentPrepareForm` listener.<br/>
*
* Notifications can be set-up on: task success, failure, fatal failure (task running too long or crashing the request),
* or on _orphaned_ task routines (missing parent plugin - either uninstalled, disabled or no longer offering a routine
* with the same ID).
*
* @since 4.1.0
*/
final class TaskNotification extends CMSPlugin implements SubscriberInterface
{
use DatabaseAwareTrait;
use UserFactoryAwareTrait;
/**
* The task notification form. This form is merged into the task item form by {@see
* injectTaskNotificationFieldset()}.
*
* @var string
* @since 4.1.0
*/
private const TASK_NOTIFICATION_FORM = 'task_notification';
/**
* @inheritDoc
*
* @return array
*
* @since 4.1.0
*/
public static function getSubscribedEvents(): array
{
return [
'onContentPrepareForm' => 'injectTaskNotificationFieldset',
'onTaskExecuteSuccess' => 'notifySuccess',
'onTaskExecuteFailure' => 'notifyFailure',
'onTaskRoutineNotFound' => 'notifyOrphan',
'onTaskRecoverFailure' => 'notifyFatalRecovery',
];
}
/**
* Inject fields to support configuration of post-execution notifications into the task item form.
*
* @param Model\PrepareFormEvent $event The onContentPrepareForm event.
*
* @return boolean True if successful.
*
* @since 4.1.0
*/
public function injectTaskNotificationFieldset(Model\PrepareFormEvent $event): bool
{
$form = $event->getForm();
if ($form->getName() !== 'com_scheduler.task') {
return true;
}
// Load translations
$this->loadLanguage();
$formFile = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms/' . self::TASK_NOTIFICATION_FORM . '.xml';
try {
$formFile = Path::check($formFile);
} catch (\Exception $e) {
// Log?
return false;
}
$formFile = Path::clean($formFile);
if (!is_file($formFile)) {
return false;
}
return $form->loadFile($formFile);
}
/**
* Send out email notifications on Task execution failure if task configuration allows it.
*
* @param Event $event The onTaskExecuteFailure event.
*
* @return void
*
* @since 4.1.0
* @throws \Exception
*/
public function notifyFailure(Event $event): void
{
/** @var Task $task */
$task = $event->getArgument('subject');
if (!(int) $task->get('params.notifications.failure_mail', 1)) {
return;
}
// Load translations
$this->loadLanguage();
// @todo safety checks, multiple files [?]
$outFile = $event->getArgument('subject')->snapshot['output_file'] ?? '';
$data = $this->getDataFromTask($event->getArgument('subject'));
$this->sendMail('plg_system_tasknotification.failure_mail', $data, $outFile);
}
/**
* Send out email notifications on orphaned task if task configuration allows.<br/>
* A task is `orphaned` if the task's parent plugin has been removed/disabled, or no longer offers a task
* with the same routine ID.
*
* @param Event $event The onTaskRoutineNotFound event.
*
* @return void
*
* @since 4.1.0
* @throws \Exception
*/
public function notifyOrphan(Event $event): void
{
/** @var Task $task */
$task = $event->getArgument('subject');
if (!(int) $task->get('params.notifications.orphan_mail', 1)) {
return;
}
// Load translations
$this->loadLanguage();
$data = $this->getDataFromTask($event->getArgument('subject'));
$this->sendMail('plg_system_tasknotification.orphan_mail', $data);
}
/**
* Send out email notifications on Task execution success if task configuration allows.
*
* @param Event $event The onTaskExecuteSuccess event.
*
* @return void
*
* @since 4.1.0
* @throws \Exception
*/
public function notifySuccess(Event $event): void
{
/** @var Task $task */
$task = $event->getArgument('subject');
if (!(int) $task->get('params.notifications.success_mail', 0)) {
return;
}
// Load translations
$this->loadLanguage();
// @todo safety checks, multiple files [?]
$outFile = $event->getArgument('subject')->snapshot['output_file'] ?? '';
$data = $this->getDataFromTask($event->getArgument('subject'));
$this->sendMail('plg_system_tasknotification.success_mail', $data, $outFile);
}
/**
* Send out email notifications on fatal recovery of task execution if task configuration allows.<br/>
* Fatal recovery indicated that the task either crashed the parent process or its execution lasted longer
* than the global task timeout (this is configurable through the Scheduler component configuration).
* In the latter case, the global task timeout should be adjusted so that this false positive can be avoided.
* This stands as a limitation of the Scheduler's current task execution implementation, which doesn't involve
* keeping track of the parent PHP process which could enable keeping track of the task's status.
*
* @param Event $event The onTaskRecoverFailure event.
*
* @return void
*
* @since 4.1.0
* @throws \Exception
*/
public function notifyFatalRecovery(Event $event): void
{
/** @var Task $task */
$task = $event->getArgument('subject');
if (!(int) $task->get('params.notifications.fatal_failure_mail', 1)) {
return;
}
// Load translations
$this->loadLanguage();
$data = $this->getDataFromTask($event->getArgument('subject'));
$this->sendMail('plg_system_tasknotification.fatal_recovery_mail', $data);
}
/**
* @param Task $task A task object
*
* @return array An array of data to bind to a mail template.
*
* @since 4.1.0
*/
private function getDataFromTask(Task $task): array
{
$lockOrExecTime = Factory::getDate($task->get('locked') ?? $task->get('last_execution'))->format($this->getApplication()->getLanguage()->_('DATE_FORMAT_LC2'));
return [
'TASK_ID' => $task->get('id'),
'TASK_TITLE' => $task->get('title'),
'EXIT_CODE' => $task->getContent()['status'] ?? Status::NO_EXIT,
'EXEC_DATE_TIME' => $lockOrExecTime,
'TASK_OUTPUT' => $task->getContent()['output_body'] ?? '',
];
}
/**
* @param string $template The mail template.
* @param array $data The data to bind to the mail template.
* @param string $attachment The attachment to send with the mail (@todo multiple)
*
* @return void
*
* @since 4.1.0
* @throws \Exception
*/
private function sendMail(string $template, array $data, string $attachment = ''): void
{
$app = $this->getApplication();
$db = $this->getDatabase();
// Get all users who are not blocked and have opted in for system mails.
$query = $db->getQuery(true);
$query->select($db->quoteName(['name', 'email', 'sendEmail', 'id']))
->from($db->quoteName('#__users'))
->where($db->quoteName('sendEmail') . ' = 1')
->where($db->quoteName('block') . ' = 0');
$db->setQuery($query);
try {
$users = $db->loadObjectList();
} catch (\RuntimeException $e) {
return;
}
if ($users === null) {
Log::add($this->getApplication()->getLanguage()->_('PLG_SYSTEM_TASK_NOTIFICATION_USER_FETCH_FAIL'), Log::ERROR);
return;
}
$mailSent = false;
// Mail all matching users who also have the `core.manage` privilege for com_scheduler.
foreach ($users as $user) {
$user = $this->getUserFactory()->loadUserById($user->id);
if ($user->authorise('core.manage', 'com_scheduler')) {
try {
$mailer = new MailTemplate($template, $app->getLanguage()->getTag());
$mailer->addTemplateData($data);
$mailer->addRecipient($user->email);
if (
!empty($attachment)
&& is_file($attachment)
) {
// @todo we allow multiple files [?]
$attachName = pathinfo($attachment, PATHINFO_BASENAME);
$mailer->addAttachment($attachName, $attachment);
}
$mailer->send();
$mailSent = true;
} catch (MailerException $exception) {
Log::add($this->getApplication()->getLanguage()->_('PLG_SYSTEM_TASK_NOTIFICATION_NOTIFY_SEND_EMAIL_FAIL'), Log::ERROR);
}
}
}
if (!$mailSent) {
Log::add($this->getApplication()->getLanguage()->_('PLG_SYSTEM_TASK_NOTIFICATION_NO_MAIL_SENT'), Log::WARNING);
}
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_task_notification</name>
<author>Joomla! Project</author>
<creationDate>2021-09</creationDate>
<copyright>(C) 2021 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>4.1</version>
<description>PLG_SYSTEM_TASK_NOTIFICATION_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\TaskNotification</namespace>
<files>
<folder>forms</folder>
<folder plugin="tasknotification">services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_tasknotification.ini</language>
<language tag="en-GB">language/en-GB/plg_system_tasknotification.sys.ini</language>
</languages>
</extension>

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More