first commit
This commit is contained in:
48
plugins/system/accessibility/accessibility.xml
Normal file
48
plugins/system/accessibility/accessibility.xml
Normal 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>
|
||||
46
plugins/system/accessibility/services/provider.php
Normal file
46
plugins/system/accessibility/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
114
plugins/system/accessibility/src/Extension/Accessibility.php
Normal file
114
plugins/system/accessibility/src/Extension/Accessibility.php
Normal 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']
|
||||
);
|
||||
}
|
||||
}
|
||||
22
plugins/system/actionlogs/actionlogs.xml
Normal file
22
plugins/system/actionlogs/actionlogs.xml
Normal 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>
|
||||
29
plugins/system/actionlogs/forms/actionlogs.xml
Normal file
29
plugins/system/actionlogs/forms/actionlogs.xml
Normal 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>
|
||||
13
plugins/system/actionlogs/forms/information.xml
Normal file
13
plugins/system/actionlogs/forms/information.xml
Normal 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>
|
||||
50
plugins/system/actionlogs/services/provider.php
Normal file
50
plugins/system/actionlogs/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
350
plugins/system/actionlogs/src/Extension/ActionLogs.php
Normal file
350
plugins/system/actionlogs/src/Extension/ActionLogs.php
Normal 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
59
plugins/system/cache/cache.xml
vendored
Normal 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>
|
||||
52
plugins/system/cache/services/provider.php
vendored
Normal file
52
plugins/system/cache/services/provider.php
vendored
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
382
plugins/system/cache/src/Extension/Cache.php
vendored
Normal file
382
plugins/system/cache/src/Extension/Cache.php
vendored
Normal 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 user–specific 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 auto–login 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 short–circuit 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());
|
||||
}
|
||||
}
|
||||
262
plugins/system/debug/debug.xml
Normal file
262
plugins/system/debug/debug.xml
Normal 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>
|
||||
46
plugins/system/debug/services/provider.php
Normal file
46
plugins/system/debug/services/provider.php
Normal 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)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
113
plugins/system/debug/src/AbstractDataCollector.php
Normal file
113
plugins/system/debug/src/AbstractDataCollector.php
Normal 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);
|
||||
}
|
||||
}
|
||||
217
plugins/system/debug/src/DataCollector/InfoCollector.php
Normal file
217
plugins/system/debug/src/DataCollector/InfoCollector.php
Normal 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'] ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
151
plugins/system/debug/src/DataCollector/MemoryCollector.php
Normal file
151
plugins/system/debug/src/DataCollector/MemoryCollector.php
Normal 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'",
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
342
plugins/system/debug/src/DataCollector/ProfileCollector.php
Normal file
342
plugins/system/debug/src/DataCollector/ProfileCollector.php
Normal 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' => '{}',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
258
plugins/system/debug/src/DataCollector/QueryCollector.php
Normal file
258
plugins/system/debug/src/DataCollector/QueryCollector.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
125
plugins/system/debug/src/DataCollector/SessionCollector.php
Normal file
125
plugins/system/debug/src/DataCollector/SessionCollector.php
Normal 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' => '[]',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
62
plugins/system/debug/src/DataCollector/UserCollector.php
Normal file
62
plugins/system/debug/src/DataCollector/UserCollector.php
Normal 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;
|
||||
}
|
||||
}
|
||||
93
plugins/system/debug/src/DataFormatter.php
Normal file
93
plugins/system/debug/src/DataFormatter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
712
plugins/system/debug/src/Extension/Debug.php
Normal file
712
plugins/system/debug/src/Extension/Debug.php
Normal 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);
|
||||
}
|
||||
}
|
||||
133
plugins/system/debug/src/JavascriptRenderer.php
Normal file
133
plugins/system/debug/src/JavascriptRenderer.php
Normal 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";
|
||||
}
|
||||
}
|
||||
134
plugins/system/debug/src/JoomlaHttpDriver.php
Normal file
134
plugins/system/debug/src/JoomlaHttpDriver.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
192
plugins/system/debug/src/Storage/FileStorage.php
Normal file
192
plugins/system/debug/src/Storage/FileStorage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
21
plugins/system/fields/fields.xml
Normal file
21
plugins/system/fields/fields.xml
Normal 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>
|
||||
48
plugins/system/fields/services/provider.php
Normal file
48
plugins/system/fields/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
483
plugins/system/fields/src/Extension/Fields.php
Normal file
483
plugins/system/fields/src/Extension/Fields.php
Normal 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;
|
||||
}
|
||||
}
|
||||
21
plugins/system/guidedtours/guidedtours.xml
Normal file
21
plugins/system/guidedtours/guidedtours.xml
Normal 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>
|
||||
55
plugins/system/guidedtours/services/provider.php
Normal file
55
plugins/system/guidedtours/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
260
plugins/system/guidedtours/src/Extension/GuidedTours.php
Normal file
260
plugins/system/guidedtours/src/Extension/GuidedTours.php
Normal 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;
|
||||
}
|
||||
}
|
||||
21
plugins/system/highlight/highlight.xml
Normal file
21
plugins/system/highlight/highlight.xml
Normal 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>
|
||||
46
plugins/system/highlight/services/provider.php
Normal file
46
plugins/system/highlight/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
127
plugins/system/highlight/src/Extension/Highlight.php
Normal file
127
plugins/system/highlight/src/Extension/Highlight.php
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
328
plugins/system/httpheaders/httpheaders.xml
Normal file
328
plugins/system/httpheaders/httpheaders.xml
Normal 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>
|
||||
62
plugins/system/httpheaders/postinstall/introduction.php
Normal file
62
plugins/system/httpheaders/postinstall/introduction.php
Normal 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);
|
||||
}
|
||||
48
plugins/system/httpheaders/services/provider.php
Normal file
48
plugins/system/httpheaders/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
456
plugins/system/httpheaders/src/Extension/Httpheaders.php
Normal file
456
plugins/system/httpheaders/src/Extension/Httpheaders.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
plugins/system/jooa11y/jooa11y.xml
Normal file
62
plugins/system/jooa11y/jooa11y.xml
Normal 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>
|
||||
46
plugins/system/jooa11y/services/provider.php
Normal file
46
plugins/system/jooa11y/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
259
plugins/system/jooa11y/src/Extension/Jooa11y.php
Normal file
259
plugins/system/jooa11y/src/Extension/Jooa11y.php
Normal 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;
|
||||
}
|
||||
}
|
||||
30
plugins/system/languagecode/languagecode.xml
Normal file
30
plugins/system/languagecode/languagecode.xml
Normal 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>
|
||||
46
plugins/system/languagecode/services/provider.php
Normal file
46
plugins/system/languagecode/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
151
plugins/system/languagecode/src/Extension/LanguageCode.php
Normal file
151
plugins/system/languagecode/src/Extension/LanguageCode.php
Normal 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;
|
||||
}
|
||||
}
|
||||
121
plugins/system/languagefilter/languagefilter.xml
Normal file
121
plugins/system/languagefilter/languagefilter.xml
Normal 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>
|
||||
50
plugins/system/languagefilter/services/provider.php
Normal file
50
plugins/system/languagefilter/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
856
plugins/system/languagefilter/src/Extension/LanguageFilter.php
Normal file
856
plugins/system/languagefilter/src/Extension/LanguageFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
38
plugins/system/log/log.xml
Normal file
38
plugins/system/log/log.xml
Normal 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>
|
||||
46
plugins/system/log/services/provider.php
Normal file
46
plugins/system/log/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
72
plugins/system/log/src/Extension/Log.php
Normal file
72
plugins/system/log/src/Extension/Log.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
plugins/system/logout/logout.xml
Normal file
21
plugins/system/logout/logout.xml
Normal 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>
|
||||
44
plugins/system/logout/services/provider.php
Normal file
44
plugins/system/logout/services/provider.php
Normal 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()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
88
plugins/system/logout/src/Extension/Logout.php
Normal file
88
plugins/system/logout/src/Extension/Logout.php
Normal 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;
|
||||
}
|
||||
}
|
||||
21
plugins/system/privacyconsent/forms/privacyconsent.xml
Normal file
21
plugins/system/privacyconsent/forms/privacyconsent.xml
Normal 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>
|
||||
83
plugins/system/privacyconsent/privacyconsent.xml
Normal file
83
plugins/system/privacyconsent/privacyconsent.xml
Normal 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>
|
||||
48
plugins/system/privacyconsent/services/provider.php
Normal file
48
plugins/system/privacyconsent/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
445
plugins/system/privacyconsent/src/Extension/PrivacyConsent.php
Normal file
445
plugins/system/privacyconsent/src/Extension/PrivacyConsent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
133
plugins/system/privacyconsent/src/Field/PrivacyField.php
Normal file
133
plugins/system/privacyconsent/src/Field/PrivacyField.php
Normal 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);
|
||||
}
|
||||
}
|
||||
18
plugins/system/redirect/form/excludes.xml
Normal file
18
plugins/system/redirect/form/excludes.xml
Normal 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>
|
||||
58
plugins/system/redirect/redirect.xml
Normal file
58
plugins/system/redirect/redirect.xml
Normal 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>
|
||||
48
plugins/system/redirect/services/provider.php
Normal file
48
plugins/system/redirect/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
265
plugins/system/redirect/src/Extension/Redirect.php
Normal file
265
plugins/system/redirect/src/Extension/Redirect.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
plugins/system/remember/remember.xml
Normal file
21
plugins/system/remember/remember.xml
Normal 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>
|
||||
48
plugins/system/remember/services/provider.php
Normal file
48
plugins/system/remember/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
130
plugins/system/remember/src/Extension/Remember.php
Normal file
130
plugins/system/remember/src/Extension/Remember.php
Normal 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;
|
||||
}
|
||||
}
|
||||
25
plugins/system/schedulerunner/schedulerunner.xml
Normal file
25
plugins/system/schedulerunner/schedulerunner.xml
Normal 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>
|
||||
46
plugins/system/schedulerunner/services/provider.php
Normal file
46
plugins/system/schedulerunner/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
357
plugins/system/schedulerunner/src/Extension/ScheduleRunner.php
Normal file
357
plugins/system/schedulerunner/src/Extension/ScheduleRunner.php
Normal 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();
|
||||
}
|
||||
}
|
||||
26
plugins/system/schemaorg/forms/schemaorg.xml
Executable file
26
plugins/system/schemaorg/forms/schemaorg.xml
Executable 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>
|
||||
77
plugins/system/schemaorg/schemaorg.xml
Executable file
77
plugins/system/schemaorg/schemaorg.xml
Executable 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>
|
||||
53
plugins/system/schemaorg/services/provider.php
Normal file
53
plugins/system/schemaorg/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
533
plugins/system/schemaorg/src/Extension/Schemaorg.php
Normal file
533
plugins/system/schemaorg/src/Extension/Schemaorg.php
Normal 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;
|
||||
}
|
||||
}
|
||||
36
plugins/system/sef/sef.xml
Normal file
36
plugins/system/sef/sef.xml
Normal 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>
|
||||
46
plugins/system/sef/services/provider.php
Normal file
46
plugins/system/sef/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
218
plugins/system/sef/src/Extension/Sef.php
Normal file
218
plugins/system/sef/src/Extension/Sef.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
plugins/system/shortcut/services/provider.php
Normal file
46
plugins/system/shortcut/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
40
plugins/system/shortcut/shortcut.xml
Normal file
40
plugins/system/shortcut/shortcut.xml
Normal 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>
|
||||
136
plugins/system/shortcut/src/Extension/Shortcut.php
Normal file
136
plugins/system/shortcut/src/Extension/Shortcut.php
Normal 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);
|
||||
}
|
||||
}
|
||||
46
plugins/system/skipto/services/provider.php
Normal file
46
plugins/system/skipto/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
38
plugins/system/skipto/skipto.xml
Normal file
38
plugins/system/skipto/skipto.xml
Normal 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>
|
||||
102
plugins/system/skipto/src/Extension/Skipto.php
Normal file
102
plugins/system/skipto/src/Extension/Skipto.php
Normal 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');
|
||||
}
|
||||
}
|
||||
55
plugins/system/stats/layouts/field/data.php
Normal file
55
plugins/system/stats/layouts/field/data.php
Normal 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; ?>
|
||||
49
plugins/system/stats/layouts/field/uniqueid.php
Normal file
49
plugins/system/stats/layouts/field/uniqueid.php
Normal 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>
|
||||
47
plugins/system/stats/layouts/message.php
Normal file
47
plugins/system/stats/layouts/message.php
Normal 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>
|
||||
47
plugins/system/stats/layouts/stats.php
Normal file
47
plugins/system/stats/layouts/stats.php
Normal 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>
|
||||
48
plugins/system/stats/services/provider.php
Normal file
48
plugins/system/stats/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
620
plugins/system/stats/src/Extension/Stats.php
Normal file
620
plugins/system/stats/src/Extension/Stats.php
Normal 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);
|
||||
}
|
||||
}
|
||||
44
plugins/system/stats/src/Field/AbstractStatsField.php
Normal file
44
plugins/system/stats/src/Field/AbstractStatsField.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
62
plugins/system/stats/src/Field/DataField.php
Normal file
62
plugins/system/stats/src/Field/DataField.php
Normal 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;
|
||||
}
|
||||
}
|
||||
39
plugins/system/stats/src/Field/UniqueidField.php
Normal file
39
plugins/system/stats/src/Field/UniqueidField.php
Normal 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';
|
||||
}
|
||||
65
plugins/system/stats/stats.xml
Normal file
65
plugins/system/stats/stats.xml
Normal 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>
|
||||
49
plugins/system/tasknotification/forms/task_notification.xml
Normal file
49
plugins/system/tasknotification/forms/task_notification.xml
Normal 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>
|
||||
50
plugins/system/tasknotification/services/provider.php
Normal file
50
plugins/system/tasknotification/services/provider.php
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
plugins/system/tasknotification/tasknotification.xml
Normal file
22
plugins/system/tasknotification/tasknotification.xml
Normal 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>
|
||||
1
plugins/system/webauthn/fido.jwt
Normal file
1
plugins/system/webauthn/fido.jwt
Normal file
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
Reference in New Issue
Block a user