first commit
This commit is contained in:
65
plugins/user/token/forms/token.xml
Normal file
65
plugins/user/token/forms/token.xml
Normal file
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form>
|
||||
<fields name="joomlatoken" addfieldprefix="Joomla\Plugin\User\Token\Field">
|
||||
<fieldset
|
||||
name="joomlatoken"
|
||||
label="PLG_USER_TOKEN_GROUP_LABEL"
|
||||
description="PLG_USER_TOKEN_GROUP_DESC">
|
||||
|
||||
<field
|
||||
name="saveme"
|
||||
type="note"
|
||||
description="PLG_USER_TOKEN_SAVEME_DESC"
|
||||
class="alert alert-warning"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="notokenforotherpeople"
|
||||
type="note"
|
||||
description="PLG_USER_TOKEN_NOTOKENFOROTHERPEOPLE_DESC"
|
||||
class="alert alert-warning"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="savemeforotherpeople"
|
||||
type="note"
|
||||
description="PLG_USER_TOKEN_SAVEMEFOROTHERPEOPLE_DESC"
|
||||
class="alert alert-warning"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="token"
|
||||
type="joomlatoken"
|
||||
label="PLG_USER_TOKEN_TOKEN_LABEL"
|
||||
description="PLG_USER_TOKEN_TOKEN_DESC"
|
||||
default=""
|
||||
algo="sha256"
|
||||
readonly="true"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="enabled"
|
||||
type="radio"
|
||||
label="PLG_USER_TOKEN_ENABLED_LABEL"
|
||||
description="PLG_USER_TOKEN_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="1"
|
||||
>
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="reset"
|
||||
type="radio"
|
||||
label="PLG_USER_TOKEN_RESET_LABEL"
|
||||
description="PLG_USER_TOKEN_RESET_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
>
|
||||
<option value="0">JNO</option>
|
||||
<option value="1">JYES</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</form>
|
||||
50
plugins/user/token/services/provider.php
Normal file
50
plugins/user/token/services/provider.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage User.token
|
||||
*
|
||||
* @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\User\Token\Extension\Token;
|
||||
|
||||
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 Token(
|
||||
$container->get(DispatcherInterface::class),
|
||||
(array) PluginHelper::getPlugin('user', 'token')
|
||||
);
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
$plugin->setDatabase($container->get(DatabaseInterface::class));
|
||||
$plugin->setUserFactory($container->get(UserFactoryInterface::class));
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
605
plugins/user/token/src/Extension/Token.php
Normal file
605
plugins/user/token/src/Extension/Token.php
Normal file
@ -0,0 +1,605 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage User.token
|
||||
*
|
||||
* @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\User\Token\Extension;
|
||||
|
||||
use Joomla\CMS\Crypt\Crypt;
|
||||
use Joomla\CMS\Form\Form;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\User\UserFactoryAwareTrait;
|
||||
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 terms and conditions plugin.
|
||||
*
|
||||
* @since 3.9.0
|
||||
*/
|
||||
final class Token extends CMSPlugin
|
||||
{
|
||||
use DatabaseAwareTrait;
|
||||
use UserFactoryAwareTrait;
|
||||
|
||||
/**
|
||||
* Joomla XML form contexts where we should inject our token management user interface.
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $allowedContexts = [
|
||||
'com_users.profile',
|
||||
'com_users.user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The prefix of the user profile keys, without the dot.
|
||||
*
|
||||
* @var string
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $profileKeyPrefix = 'joomlatoken';
|
||||
|
||||
/**
|
||||
* Token length, in bytes.
|
||||
*
|
||||
* @var integer
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $tokenLength = 32;
|
||||
|
||||
/**
|
||||
* Inject the Joomla token management panel's data into the User Profile.
|
||||
*
|
||||
* This method is called whenever Joomla is preparing the data for an XML form for display.
|
||||
*
|
||||
* @param string $context Form context, passed by Joomla
|
||||
* @param mixed $data Form data
|
||||
*
|
||||
* @return boolean
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function onContentPrepareData(string $context, &$data): bool
|
||||
{
|
||||
// Only do something if the api-authentication plugin with the same name is published
|
||||
if (!PluginHelper::isEnabled('api-authentication', $this->_name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check we are manipulating a valid form.
|
||||
if (!\in_array($context, $this->allowedContexts)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// $data must be an object
|
||||
if (!\is_object($data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We expect the numeric user ID in $data->id
|
||||
if (!isset($data->id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the user ID
|
||||
$userId = \intval($data->id);
|
||||
|
||||
// Make sure we have a positive integer user ID
|
||||
if ($userId <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->isInAllowedUserGroup($userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load plugin language files
|
||||
$this->loadLanguage();
|
||||
|
||||
$data->{$this->profileKeyPrefix} = [];
|
||||
|
||||
// Load the profile data from the database.
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('profile_key'),
|
||||
$db->quoteName('profile_value'),
|
||||
])
|
||||
->from($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('user_id') . ' = :userId')
|
||||
->where($db->quoteName('profile_key') . ' LIKE :profileKey')
|
||||
->order($db->quoteName('ordering'));
|
||||
|
||||
$profileKey = $this->profileKeyPrefix . '.%';
|
||||
$query->bind(':userId', $userId, ParameterType::INTEGER);
|
||||
$query->bind(':profileKey', $profileKey, ParameterType::STRING);
|
||||
|
||||
$results = $db->setQuery($query)->loadRowList();
|
||||
|
||||
foreach ($results as $v) {
|
||||
$k = str_replace($this->profileKeyPrefix . '.', '', $v[0]);
|
||||
|
||||
$data->{$this->profileKeyPrefix}[$k] = $v[1];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// We suppress any database error. It means we get no token saved by default.
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the data for display in the user profile view page in the frontend.
|
||||
*
|
||||
* It's important to note that we deliberately not register HTMLHelper methods to do the
|
||||
* same (unlike e.g. the actionlogs system plugin) because the names of our fields are too
|
||||
* generic and we run the risk of creating naming clashes. Instead, we manipulate the data
|
||||
* directly.
|
||||
*/
|
||||
if (($context === 'com_users.profile') && ($this->getApplication()->getInput()->get('layout') !== 'edit')) {
|
||||
$pluginData = $data->{$this->profileKeyPrefix} ?? [];
|
||||
$enabled = $pluginData['enabled'] ?? false;
|
||||
$token = $pluginData['token'] ?? '';
|
||||
|
||||
$pluginData['enabled'] = $this->getApplication()->getLanguage()->_('JDISABLED');
|
||||
$pluginData['token'] = '';
|
||||
|
||||
if ($enabled) {
|
||||
$algo = $this->getAlgorithmFromFormFile();
|
||||
$pluginData['enabled'] = $this->getApplication()->getLanguage()->_('JENABLED');
|
||||
$pluginData['token'] = $this->getTokenForDisplay($userId, $token, $algo);
|
||||
}
|
||||
|
||||
$data->{$this->profileKeyPrefix} = $pluginData;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs whenever Joomla is preparing a form object.
|
||||
*
|
||||
* @param Form $form The form to be altered.
|
||||
* @param mixed $data The associated data for the form.
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @throws \Exception When $form is not a valid form object
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function onContentPrepareForm(Form $form, $data): bool
|
||||
{
|
||||
// Only do something if the api-authentication plugin with the same name is published
|
||||
if (!PluginHelper::isEnabled('api-authentication', $this->_name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check we are manipulating a valid form.
|
||||
if (!\in_array($form->getName(), $this->allowedContexts)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Check if the user belongs to an allowed user group
|
||||
$userId = (\is_object($data) && isset($data->id)) ? $data->id : 0;
|
||||
|
||||
if (!empty($userId) && !$this->isInAllowedUserGroup($userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load plugin language files
|
||||
$this->loadLanguage();
|
||||
|
||||
// Add the registration fields to the form.
|
||||
Form::addFormPath(JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms');
|
||||
$form->loadFile('token', false);
|
||||
|
||||
// No token: no reset
|
||||
$userTokenSeed = $this->getTokenSeedForUser($userId);
|
||||
$currentUser = $this->getApplication()->getIdentity();
|
||||
|
||||
if (empty($userTokenSeed)) {
|
||||
$form->removeField('notokenforotherpeople', 'joomlatoken');
|
||||
$form->removeField('reset', 'joomlatoken');
|
||||
$form->removeField('token', 'joomlatoken');
|
||||
$form->removeField('enabled', 'joomlatoken');
|
||||
} else {
|
||||
$form->removeField('saveme', 'joomlatoken');
|
||||
}
|
||||
|
||||
if ($userId != $currentUser->id) {
|
||||
$form->removeField('token', 'joomlatoken');
|
||||
} else {
|
||||
$form->removeField('notokenforotherpeople', 'joomlatoken');
|
||||
}
|
||||
|
||||
if (($userId != $currentUser->id) && empty($userTokenSeed)) {
|
||||
$form->removeField('saveme', 'joomlatoken');
|
||||
} else {
|
||||
$form->removeField('savemeforotherpeople', 'joomlatoken');
|
||||
}
|
||||
|
||||
// Remove the Reset field when displaying the user profile form
|
||||
if (($form->getName() === 'com_users.profile') && ($this->getApplication()->getInput()->get('layout') !== 'edit')) {
|
||||
$form->removeField('reset', 'joomlatoken');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the Joomla token in the user profile field
|
||||
*
|
||||
* @param mixed $data The incoming form data
|
||||
* @param bool $isNew Is this a new user?
|
||||
* @param bool $result Has Joomla successfully saved the user?
|
||||
* @param string $error Error string
|
||||
*
|
||||
* @return void
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function onUserAfterSave($data, bool $isNew, bool $result, ?string $error): void
|
||||
{
|
||||
if (!\is_array($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = ArrayHelper::getValue($data, 'id', 0, 'int');
|
||||
|
||||
if ($userId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$result) {
|
||||
return;
|
||||
}
|
||||
|
||||
$noToken = false;
|
||||
|
||||
// No Joomla token data. Set the $noToken flag which results in a new token being generated.
|
||||
if (!isset($data[$this->profileKeyPrefix])) {
|
||||
/**
|
||||
* Is the user being saved programmatically, without passing the user profile
|
||||
* information? In this case I do not want to accidentally try to generate a new token!
|
||||
*
|
||||
* We determine that by examining whether the Joomla token field exists. If it does but
|
||||
* it wasn't passed when saving the user I know it's a programmatic user save and I have
|
||||
* to ignore it.
|
||||
*/
|
||||
if ($this->hasTokenProfileFields($userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$noToken = true;
|
||||
$data[$this->profileKeyPrefix] = [];
|
||||
}
|
||||
|
||||
if (isset($data[$this->profileKeyPrefix]['reset'])) {
|
||||
$reset = $data[$this->profileKeyPrefix]['reset'] == 1;
|
||||
unset($data[$this->profileKeyPrefix]['reset']);
|
||||
|
||||
if ($reset) {
|
||||
$noToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We may have a token already saved. Let's check, shall we?
|
||||
if (!$noToken) {
|
||||
$noToken = true;
|
||||
$existingToken = $this->getTokenSeedForUser($userId);
|
||||
|
||||
if (!empty($existingToken)) {
|
||||
$noToken = false;
|
||||
$data[$this->profileKeyPrefix]['token'] = $existingToken;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no token or this is a new user generate a new token.
|
||||
if ($noToken || $isNew) {
|
||||
if (
|
||||
isset($data[$this->profileKeyPrefix]['token'])
|
||||
&& empty($data[$this->profileKeyPrefix]['token'])
|
||||
) {
|
||||
unset($data[$this->profileKeyPrefix]['token']);
|
||||
}
|
||||
|
||||
$default = $this->getDefaultProfileFieldValues();
|
||||
$data[$this->profileKeyPrefix] = array_merge($default, $data[$this->profileKeyPrefix]);
|
||||
}
|
||||
|
||||
// Remove existing Joomla Token user profile values
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('user_id') . ' = :userId')
|
||||
->where($db->quoteName('profile_key') . ' LIKE :profileKey');
|
||||
|
||||
$profileKey = $this->profileKeyPrefix . '.%';
|
||||
$query->bind(':userId', $userId, ParameterType::INTEGER);
|
||||
$query->bind(':profileKey', $profileKey, ParameterType::STRING);
|
||||
|
||||
$db->setQuery($query)->execute();
|
||||
|
||||
// If the user is not in the allowed user group don't save any new token information.
|
||||
if (!$this->isInAllowedUserGroup($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the new Joomla Token user profile values
|
||||
$order = 1;
|
||||
$query = $db->getQuery(true)
|
||||
->insert($db->quoteName('#__user_profiles'))
|
||||
->columns([
|
||||
$db->quoteName('user_id'),
|
||||
$db->quoteName('profile_key'),
|
||||
$db->quoteName('profile_value'),
|
||||
$db->quoteName('ordering'),
|
||||
]);
|
||||
|
||||
foreach ($data[$this->profileKeyPrefix] as $k => $v) {
|
||||
$query->values($userId . ', '
|
||||
. $db->quote($this->profileKeyPrefix . '.' . $k)
|
||||
. ', ' . $db->quote($v)
|
||||
. ', ' . ($order++));
|
||||
}
|
||||
|
||||
$db->setQuery($query)->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the Joomla token when the user account is deleted from the database.
|
||||
*
|
||||
* This event is called after the 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
|
||||
*
|
||||
* @throws \Exception
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function onUserAfterDelete(array $user, bool $success, string $msg): void
|
||||
{
|
||||
if (!$success) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = ArrayHelper::getValue($user, 'id', 0, 'int');
|
||||
|
||||
if ($userId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('user_id') . ' = :userId')
|
||||
->where($db->quoteName('profile_key') . ' LIKE :profileKey');
|
||||
|
||||
$profileKey = $this->profileKeyPrefix . '.%';
|
||||
$query->bind(':userId', $userId, ParameterType::INTEGER);
|
||||
$query->bind(':profileKey', $profileKey, ParameterType::STRING);
|
||||
|
||||
$db->setQuery($query)->execute();
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the default profile field values.
|
||||
*
|
||||
* This is used when saving the form data of a user (new or existing) without a token already
|
||||
* set.
|
||||
*
|
||||
* @return array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function getDefaultProfileFieldValues(): array
|
||||
{
|
||||
return [
|
||||
'token' => base64_encode(Crypt::genRandomBytes($this->tokenLength)),
|
||||
'enabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the token seed string for the given user ID.
|
||||
*
|
||||
* @param int $userId The numeric user ID to return the token seed string for.
|
||||
*
|
||||
* @return string|null Null if there is no token configured or the user doesn't exist.
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function getTokenSeedForUser(int $userId): ?string
|
||||
{
|
||||
try {
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('profile_value'))
|
||||
->from($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('profile_key') . ' = :profileKey')
|
||||
->where($db->quoteName('user_id') . ' = :userId');
|
||||
|
||||
$profileKey = $this->profileKeyPrefix . '.token';
|
||||
$query->bind(':profileKey', $profileKey, ParameterType::STRING);
|
||||
$query->bind(':userId', $userId, ParameterType::INTEGER);
|
||||
|
||||
return $db->setQuery($query)->loadResult();
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured user groups which are allowed to have access to tokens.
|
||||
*
|
||||
* @return int[]
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function getAllowedUserGroups(): array
|
||||
{
|
||||
$userGroups = $this->params->get('allowedUserGroups', [8]);
|
||||
|
||||
if (empty($userGroups)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!\is_array($userGroups)) {
|
||||
$userGroups = [$userGroups];
|
||||
}
|
||||
|
||||
return $userGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the user with the given ID in the allowed User Groups with access to tokens?
|
||||
*
|
||||
* @param int $userId The user ID to check
|
||||
*
|
||||
* @return boolean False when doesn't belong to allowed user groups, user not found, or guest
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function isInAllowedUserGroup($userId)
|
||||
{
|
||||
$allowedUserGroups = $this->getAllowedUserGroups();
|
||||
|
||||
$user = $this->getUserFactory()->loadUserById($userId);
|
||||
|
||||
if ($user->id != $userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user->guest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No specifically allowed user groups: allow ALL user groups.
|
||||
if (empty($allowedUserGroups)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$groups = $user->getAuthorisedGroups();
|
||||
$intersection = array_intersect($groups, $allowedUserGroups);
|
||||
|
||||
return !empty($intersection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token formatted suitably for the user to copy.
|
||||
*
|
||||
* @param integer $userId The user id for token
|
||||
* @param string $tokenSeed The token seed data stored in the database
|
||||
* @param string $algorithm The hashing algorithm to use for the token (default: sha256)
|
||||
*
|
||||
* @return string
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function getTokenForDisplay(
|
||||
int $userId,
|
||||
string $tokenSeed,
|
||||
string $algorithm = 'sha256'
|
||||
): string {
|
||||
if (empty($tokenSeed)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
$siteSecret = $this->getApplication()->get('secret');
|
||||
} catch (\Exception $e) {
|
||||
$siteSecret = '';
|
||||
}
|
||||
|
||||
// NO site secret? You monster!
|
||||
if (empty($siteSecret)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rawToken = base64_decode($tokenSeed);
|
||||
$tokenHash = hash_hmac($algorithm, $rawToken, $siteSecret);
|
||||
$message = base64_encode("$algorithm:$userId:$tokenHash");
|
||||
|
||||
if ($userId !== $this->getApplication()->getIdentity()->id) {
|
||||
$message = '';
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token algorithm as defined in the form file
|
||||
*
|
||||
* We use a simple RegEx match instead of loading the form for better performance.
|
||||
*
|
||||
* @return string The configured algorithm, 'sha256' as a fallback if none is found.
|
||||
*/
|
||||
private function getAlgorithmFromFormFile(): string
|
||||
{
|
||||
$algo = 'sha256';
|
||||
|
||||
$file = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/forms/token.xml';
|
||||
$contents = @file_get_contents($file);
|
||||
|
||||
if ($contents === false) {
|
||||
return $algo;
|
||||
}
|
||||
|
||||
if (preg_match('/\s*algo=\s*"\s*([a-z0-9]+)\s*"/i', $contents, $matches) !== 1) {
|
||||
return $algo;
|
||||
}
|
||||
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the user have the Joomla Token profile fields?
|
||||
*
|
||||
* @param int|null $userId The user we're interested in
|
||||
*
|
||||
* @return bool True if the user has Joomla Token profile fields
|
||||
*/
|
||||
private function hasTokenProfileFields(?int $userId): bool
|
||||
{
|
||||
if (\is_null($userId) || ($userId <= 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$q = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__user_profiles'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->where($db->quoteName('profile_key') . ' = ' . $db->quote($this->profileKeyPrefix . '.token'));
|
||||
|
||||
try {
|
||||
$numRows = $db->setQuery($q)->loadResult() ?? 0;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $numRows > 0;
|
||||
}
|
||||
}
|
||||
142
plugins/user/token/src/Field/JoomlatokenField.php
Normal file
142
plugins/user/token/src/Field/JoomlatokenField.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage User.token
|
||||
*
|
||||
* @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\User\Token\Field;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\Field\TextField;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Joomlatoken field class
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class JoomlatokenField extends TextField
|
||||
{
|
||||
/**
|
||||
* Name of the layout being used to render the field
|
||||
*
|
||||
* @var string
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected $layout = 'plugins.user.token.token';
|
||||
|
||||
/**
|
||||
* Method to attach a Form object to the field.
|
||||
*
|
||||
* @param \SimpleXMLElement $element The SimpleXMLElement object representing the `<field>`
|
||||
* tag for the form field object.
|
||||
* @param mixed $value The form field value to validate.
|
||||
* @param string $group The field name group control value. This acts as an
|
||||
* array container for the field. For example if the
|
||||
* field has name="foo" and the group value is set to
|
||||
* "bar" then the full field name would end up being
|
||||
* "bar[foo]".
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @see FormField::setup()
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function setup(\SimpleXMLElement $element, $value, $group = null)
|
||||
{
|
||||
$ret = parent::setup($element, $value, $group);
|
||||
|
||||
/**
|
||||
* Security and privacy precaution: do not display the token field when the user being
|
||||
* edited is not the same as the logged in user. Tokens are conceptually a combination of
|
||||
* a username and password, therefore they should be treated in the same mode of
|
||||
* confidentiality and privacy as passwords i.e. you can reset them for other users but NOT
|
||||
* be able to see them, thus preventing impersonation attacks by a malicious administrator.
|
||||
*/
|
||||
$userId = $this->form->getData()->get('id');
|
||||
|
||||
if ($userId != $this->getCurrentUser()->id) {
|
||||
$this->hidden = true;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the field input markup.
|
||||
*
|
||||
* @return string The field input markup.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected function getInput()
|
||||
{
|
||||
// Do not display the token field when the user being edited is not the same as the logged in user
|
||||
if ($this->hidden) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return parent::getInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the token formatted suitably for the user to copy.
|
||||
*
|
||||
* @param string $tokenSeed The token seed data stored in the database
|
||||
*
|
||||
* @return string
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function getTokenForDisplay(string $tokenSeed): string
|
||||
{
|
||||
if (empty($tokenSeed)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$algorithm = $this->getAttribute('algo', 'sha256');
|
||||
|
||||
try {
|
||||
$siteSecret = Factory::getApplication()->get('secret');
|
||||
} catch (\Exception $e) {
|
||||
$siteSecret = '';
|
||||
}
|
||||
|
||||
// NO site secret? You monster!
|
||||
if (empty($siteSecret)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rawToken = base64_decode($tokenSeed);
|
||||
$tokenHash = hash_hmac($algorithm, $rawToken, $siteSecret);
|
||||
$userId = $this->form->getData()->get('id');
|
||||
$message = base64_encode("$algorithm:$userId:$tokenHash");
|
||||
|
||||
if ($userId != $this->getCurrentUser()->id) {
|
||||
$message = '';
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data for the layout
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
protected function getLayoutData()
|
||||
{
|
||||
$data = parent::getLayoutData();
|
||||
$data['value'] = $this->getTokenForDisplay($this->value);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
38
plugins/user/token/token.xml
Normal file
38
plugins/user/token/token.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="user" method="upgrade">
|
||||
<name>plg_user_token</name>
|
||||
<author>Joomla! Project</author>
|
||||
<creationDate>2019-11</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>3.9.0</version>
|
||||
<description>PLG_USER_TOKEN_XML_DESCRIPTION</description>
|
||||
<namespace path="src">Joomla\Plugin\User\Token</namespace>
|
||||
<files>
|
||||
<folder>forms</folder>
|
||||
<folder plugin="token">services</folder>
|
||||
<folder>src</folder>
|
||||
</files>
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_user_token.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_user_token.sys.ini</language>
|
||||
</languages>
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic" addfieldprefix="Joomla\Component\Content\Administrator\Field">
|
||||
<field
|
||||
name="allowedUserGroups"
|
||||
type="UserGroupList"
|
||||
label="PLG_USER_TOKEN_ALLOWEDUSERGROUPS_LABEL"
|
||||
description="PLG_USER_TOKEN_ALLOWEDUSERGROUPS_DESC"
|
||||
layout="joomla.form.field.list-fancy-select"
|
||||
multiple="true"
|
||||
checksuperusergroup="0"
|
||||
default="8"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
Reference in New Issue
Block a user