Update 2024-09-05 01:21:23

This commit is contained in:
Robot 2024-09-05 01:21:47 +02:00
parent b894dfc8af
commit 92c4b2881e
Signed by untrusted user: Robot
GPG Key ID: 14DECD44E7E1BB95
13 changed files with 264 additions and 64 deletions

View File

@ -15,6 +15,7 @@ class UsersSubform << (F,LightGreen) >> #RoyalBlue {
# Items $items
# string $table
# array $user
# array $activeUsers
+ __construct(Items $items, ?string $table = null)
+ table(string $table) : self
+ get(string $linkValue, string $linkKey, ...) : ?array
@ -26,8 +27,9 @@ class UsersSubform << (F,LightGreen) >> #RoyalBlue {
- getUserDetails(array $item) : void
- converter(array $items, array $keySet, ...) : array
- process(mixed $items, string $indexKey, ...) : array
- setUserDetails(array $item) : int
- loadUser(array $item) : ?User
- getActiveUsers(string $linkKey, string $linkValue) : array
- setUserDetails(array $item, array $activeUsers) : int
- loadUser(array $item, array $activeUsers) : ?User
- extractUserDetails(array $item, ?User $user) : array
- assignUserGroups($details, ?User $user, ...) : void
- saveUserDetails(array $details, int $userId) : int
@ -139,7 +141,14 @@ note right of UsersSubform::process
string $linkValue
end note
note left of UsersSubform::setUserDetails
note left of UsersSubform::getActiveUsers
Get current active Users Linked to this entity
since: 5.0.2
return: array
end note
note right of UsersSubform::setUserDetails
Handles setting user details and saving them.
This function retrieves the user by ID, sets the user details,
and adds appropriate user groups before saving the user.
@ -148,21 +157,21 @@ and adds appropriate user groups before saving the user.
return: int
end note
note right of UsersSubform::loadUser
note left of UsersSubform::loadUser
Load the user based on the user ID from the item array.
since: 5.0.2
return: ?User
end note
note left of UsersSubform::extractUserDetails
note right of UsersSubform::extractUserDetails
Extract user details from the item array and prepare them for saving.
since: 5.0.2
return: array
end note
note right of UsersSubform::assignUserGroups
note left of UsersSubform::assignUserGroups
Assigns user groups based on existing groups and entity type.
since: 5.0.2
@ -174,7 +183,7 @@ note right of UsersSubform::assignUserGroups
array $item
end note
note left of UsersSubform::saveUserDetails
note right of UsersSubform::saveUserDetails
Save the user details using UserHelper and handle exceptions.
since: 5.0.2

View File

@ -61,6 +61,14 @@ final class UsersSubform implements GuidInterface, SubformInterface
*/
protected array $user;
/**
* The active users
*
* @var array
* @since 5.0.2
*/
protected array $activeUsers = [];
/**
* Constructor.
*
@ -336,7 +344,7 @@ final class UsersSubform implements GuidInterface, SubformInterface
private function process($items, string $indexKey, string $linkKey, string $linkValue): array
{
$items = is_array($items) ? $items : [];
foreach ($items as &$item)
foreach ($items as $n => &$item)
{
$value = $item[$indexKey] ?? '';
switch ($indexKey) {
@ -357,29 +365,69 @@ final class UsersSubform implements GuidInterface, SubformInterface
// No action for other keys if empty
break;
}
// set LINK
$item[$linkKey] = $linkValue;
// create/update user
$item['user_id'] = $this->setUserDetails($item);
$item['user_id'] = $this->setUserDetails(
$item,
$this->getActiveUsers(
$linkKey,
$linkValue
)
);
// remove empty row (means no user linked)
if ($item['user_id'] == 0)
{
unset($items[$n]);
}
}
return array_values($items);
}
/**
* Get current active Users Linked to this entity
*
* @param string $linkKey The link key on which the items where linked in the child table.
* @param string $linkValue The value of the link key in child table.
*
* @return array The IDs of all active users.
* @since 5.0.2
*/
private function getActiveUsers(string $linkKey, string $linkValue): array
{
if (isset($this->activeUsers[$linkKey . $linkValue]))
{
return $this->activeUsers[$linkKey . $linkValue];
}
if (($users = $this->items->table($this->getTable())->values([$linkValue], $linkKey, 'user_id')) !== null)
{
$this->activeUsers[$linkKey . $linkValue] = $users;
return $users;
}
return [];
}
/**
* Handles setting user details and saving them.
*
* This function retrieves the user by ID, sets the user details,
* and adds appropriate user groups before saving the user.
*
* @param array $item The user details passed by reference.
* @param array $item The user details passed by reference.
* @param array $activeUsers The current active user linked to this entity.
*
* @return int The ID of the saved user, or 0 on failure.
* @since 5.0.2
*/
private function setUserDetails(array &$item): int
private function setUserDetails(array &$item, array $activeUsers): int
{
$user = $this->loadUser($item);
$user = $this->loadUser($item, $activeUsers);
$details = $this->extractUserDetails($item, $user);
$this->assignUserGroups($details, $user, $item);
@ -389,18 +437,25 @@ final class UsersSubform implements GuidInterface, SubformInterface
/**
* Load the user based on the user ID from the item array.
*
* @param array $item The array containing user details.
* @param array $item The array containing user details.
* @param array $activeUsers The current active user linked to this entity.
*
* @return User|null The user object if found, null otherwise.
* @since 5.0.2
*/
private function loadUser(array $item): ?User
private function loadUser(array $item, array $activeUsers): ?User
{
if (!isset($item['user_id']) || !is_numeric($item['user_id']) || $item['user_id'] <= 0)
{
return null;
}
// only allow update to linked users
if (!in_array($item['user_id'], $activeUsers))
{
return null;
}
$user = UserHelper::getUserById((int) $item['user_id']);
if ($user && $user->id == $item['user_id'])

View File

@ -29,6 +29,14 @@
*/
protected array $user;
/**
* The active users
*
* @var array
* @since 5.0.2
*/
protected array $activeUsers = [];
/**
* Constructor.
*
@ -304,7 +312,7 @@
private function process($items, string $indexKey, string $linkKey, string $linkValue): array
{
$items = is_array($items) ? $items : [];
foreach ($items as &$item)
foreach ($items as $n => &$item)
{
$value = $item[$indexKey] ?? '';
switch ($indexKey) {
@ -325,29 +333,69 @@
// No action for other keys if empty
break;
}
// set LINK
$item[$linkKey] = $linkValue;
// create/update user
$item['user_id'] = $this->setUserDetails($item);
$item['user_id'] = $this->setUserDetails(
$item,
$this->getActiveUsers(
$linkKey,
$linkValue
)
);
// remove empty row (means no user linked)
if ($item['user_id'] == 0)
{
unset($items[$n]);
}
}
return array_values($items);
}
/**
* Get current active Users Linked to this entity
*
* @param string $linkKey The link key on which the items where linked in the child table.
* @param string $linkValue The value of the link key in child table.
*
* @return array The IDs of all active users.
* @since 5.0.2
*/
private function getActiveUsers(string $linkKey, string $linkValue): array
{
if (isset($this->activeUsers[$linkKey . $linkValue]))
{
return $this->activeUsers[$linkKey . $linkValue];
}
if (($users = $this->items->table($this->getTable())->values([$linkValue], $linkKey, 'user_id')) !== null)
{
$this->activeUsers[$linkKey . $linkValue] = $users;
return $users;
}
return [];
}
/**
* Handles setting user details and saving them.
*
* This function retrieves the user by ID, sets the user details,
* and adds appropriate user groups before saving the user.
*
* @param array $item The user details passed by reference.
* @param array $item The user details passed by reference.
* @param array $activeUsers The current active user linked to this entity.
*
* @return int The ID of the saved user, or 0 on failure.
* @since 5.0.2
*/
private function setUserDetails(array &$item): int
private function setUserDetails(array &$item, array $activeUsers): int
{
$user = $this->loadUser($item);
$user = $this->loadUser($item, $activeUsers);
$details = $this->extractUserDetails($item, $user);
$this->assignUserGroups($details, $user, $item);
@ -357,18 +405,25 @@
/**
* Load the user based on the user ID from the item array.
*
* @param array $item The array containing user details.
* @param array $item The array containing user details.
* @param array $activeUsers The current active user linked to this entity.
*
* @return User|null The user object if found, null otherwise.
* @since 5.0.2
*/
private function loadUser(array $item): ?User
private function loadUser(array $item, array $activeUsers): ?User
{
if (!isset($item['user_id']) || !is_numeric($item['user_id']) || $item['user_id'] <= 0)
{
return null;
}
// only allow update to linked users
if (!in_array($item['user_id'], $activeUsers))
{
return null;
}
$user = UserHelper::getUserById((int) $item['user_id']);
if ($user && $user->id == $item['user_id'])

View File

@ -19,7 +19,7 @@ abstract Model #Orange {
+ __construct(Table $table, ?string $tableName = null, ...)
+ table(string $table) : self
+ {abstract} value(mixed $value, string $field, ...) : mixed
+ values(?array $items = null, string $field, ...) : ?array
+ values(?array $items, string $field, ...) : ?array
+ item(?object $item, ?string $table = null) : ?object
+ items(?array $items = null, ?string $table = null) : ?array
+ row(?array $item, ?string $table = null) : ?array
@ -73,7 +73,7 @@ Example: $this->items(Array, 'value_key', 'table_name');
return: ?array
arguments:
?array $items = null
?array $items
string $field
?string $table = null
end note

View File

@ -118,7 +118,7 @@ abstract class Model implements ModelInterface
* @return array|null
* @since 3.2.2
*/
public function values(?array $items = null, string $field, ?string $table = null): ?array
public function values(?array $items, string $field, ?string $table = null): ?array
{
// check if this is a valid table
if (ArrayHelper::check($items))

View File

@ -91,7 +91,7 @@
* @return array|null
* @since 3.2.2
*/
public function values(?array $items = null, string $field, ?string $table = null): ?array
public function values(?array $items, string $field, ?string $table = null): ?array
{
// check if this is a valid table
if (ArrayHelper::check($items))

View File

@ -22,6 +22,7 @@ abstract UserHelper #Orange {
# {static} prepareUserData(array $credentials, int $mode) : array
- {static} adminRegister(BaseDatabaseModel $model, array $data) : int
- {static} handlePostRegistration(int $userId, int $autologin, ...) : int
- {static} setFormPathForUserClass(int $mode) : void
}
note right of UserHelper::save
@ -111,6 +112,15 @@ note left of UserHelper::handlePostRegistration
array $credentials
end note
note right of UserHelper::setFormPathForUserClass
Address bug on \Joomla\CMS\MVC\Model\FormBehaviorTrait Line 76
The use of JPATH_COMPONENT cause it to load the
active component forms and fields, which breaks the registration model.
since: 5.0.3
return: void
end note
@enduml
```

View File

@ -13,6 +13,7 @@ namespace VDM\Joomla\Componentbuilder\Utilities;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserHelper as JoomlaUserHelper;
@ -59,29 +60,32 @@ abstract class UserHelper
// Ensure the 'username' key exists in the credentials array, set to an empty string if not provided.
$username = $credentials['username'] ?? $credentials['email'];
// Check for an existing user by email or username.
$existingEmailUserId = static::getUserIdByEmail($credentials['email']);
$existingUserId = static::getUserIdByUsername($username);
// If either the email or username already exists, handle the update logic.
if ($existingEmailUserId !== null || $existingUserId !== null || (isset($credentials['id']) && $credentials['id'] > 0))
// If the user's ID is set and valid, handle the update logic.
if (!empty($credentials['id']) && $credentials['id'] > 0)
{
// Prevent updating other users or reusing an email by different users.
$userId = $credentials['id'];
$email = $credentials['email'];
// Fetch existing user by email and username.
$existingEmailUserId = static::getUserIdByEmail($email);
$existingUsernameId = static::getUserIdByUsername($username);
// Validate that we aren't attempting to update other users or reuse another user's email/username.
if (
(
isset($credentials['id']) &&
(
($existingEmailUserId !== null && $existingEmailUserId != $credentials['id']) ||
($existingUserId !== null && $existingUserId != $credentials['id'])
($existingEmailUserId && $existingEmailUserId != $userId) ||
($existingUsernameId && $existingUsernameId != $userId) ||
($existingEmailUserId && $existingUsernameId && $existingEmailUserId != $existingUsernameId)
) {
throw new NoUserIdFoundException(
Text::sprintf(
'User ID mismatch detected when trying to save %s (%s) credentials.',
$username,
$email
)
) || ($existingUserId !== null && $existingEmailUserId !== null && $existingEmailUserId != $existingUserId)
)
{
throw new NoUserIdFoundException(Text::sprintf('COM_COMPONENTBUILDER_USER_ID_MISMATCH_DETECTED_WHEN_TRYING_TO_SAVE_S_S_CREDENTIALS', $username, $credentials['email']));
);
}
// Update the existing user.
$credentials['id'] = $credentials['id'] ?? $existingEmailUserId ?? $existingUserId;
return static::update($credentials);
}
@ -147,6 +151,9 @@ abstract class UserHelper
// Prepare user data
$data = static::prepareUserData($credentials, $mode);
// Set form path (bug fix for Joomla)
static::setFormPathForUserClass($mode);
// Handle user creation
$userId = $mode === 1 ? $model->register($data) : static::adminRegister($model, $data);
@ -161,6 +168,17 @@ abstract class UserHelper
}
}
if (!$userId)
{
$current_user = Factory::getApplication()->getIdentity();
// only allow those with access to Users to ignore errors
if ($current_user->authorise('core.manage', 'com_users'))
{
$userId = static::getUserIdByUsername($credentials['username']);
}
}
if (is_numeric($userId) && $userId > 0)
{
// Handle post-registration processes
@ -428,5 +446,23 @@ abstract class UserHelper
return $userId;
}
/**
* Address bug on \Joomla\CMS\MVC\Model\FormBehaviorTrait Line 76
* The use of JPATH_COMPONENT cause it to load the
* active component forms and fields, which breaks the registration model.
*
* @param int $mode
*
* @since 5.0.3
*/
private static function setFormPathForUserClass(int $mode): void
{
if ($mode == 1) // 1 = use of the Registration Model
{
// Get the form.
Form::addFormPath(JPATH_ROOT . '/components/com_users/forms');
}
}
}

View File

@ -26,29 +26,32 @@
// Ensure the 'username' key exists in the credentials array, set to an empty string if not provided.
$username = $credentials['username'] ?? $credentials['email'];
// Check for an existing user by email or username.
$existingEmailUserId = static::getUserIdByEmail($credentials['email']);
$existingUserId = static::getUserIdByUsername($username);
// If either the email or username already exists, handle the update logic.
if ($existingEmailUserId !== null || $existingUserId !== null || (isset($credentials['id']) && $credentials['id'] > 0))
// If the user's ID is set and valid, handle the update logic.
if (!empty($credentials['id']) && $credentials['id'] > 0)
{
// Prevent updating other users or reusing an email by different users.
$userId = $credentials['id'];
$email = $credentials['email'];
// Fetch existing user by email and username.
$existingEmailUserId = static::getUserIdByEmail($email);
$existingUsernameId = static::getUserIdByUsername($username);
// Validate that we aren't attempting to update other users or reuse another user's email/username.
if (
(
isset($credentials['id']) &&
(
($existingEmailUserId !== null && $existingEmailUserId != $credentials['id']) ||
($existingUserId !== null && $existingUserId != $credentials['id'])
($existingEmailUserId && $existingEmailUserId != $userId) ||
($existingUsernameId && $existingUsernameId != $userId) ||
($existingEmailUserId && $existingUsernameId && $existingEmailUserId != $existingUsernameId)
) {
throw new NoUserIdFoundException(
Text::sprintf(
'User ID mismatch detected when trying to save %s (%s) credentials.',
$username,
$email
)
) || ($existingUserId !== null && $existingEmailUserId !== null && $existingEmailUserId != $existingUserId)
)
{
throw new NoUserIdFoundException(Text::sprintf('User ID mismatch detected when trying to save %s (%s) credentials.', $username, $credentials['email']));
);
}
// Update the existing user.
$credentials['id'] = $credentials['id'] ?? $existingEmailUserId ?? $existingUserId;
return static::update($credentials);
}
@ -114,6 +117,9 @@
// Prepare user data
$data = static::prepareUserData($credentials, $mode);
// Set form path (bug fix for Joomla)
static::setFormPathForUserClass($mode);
// Handle user creation
$userId = $mode === 1 ? $model->register($data) : static::adminRegister($model, $data);
@ -128,6 +134,17 @@
}
}
if (!$userId)
{
$current_user = Factory::getApplication()->getIdentity();
// only allow those with access to Users to ignore errors
if ($current_user->authorise('core.manage', 'com_users'))
{
$userId = static::getUserIdByUsername($credentials['username']);
}
}
if (is_numeric($userId) && $userId > 0)
{
// Handle post-registration processes
@ -395,3 +412,21 @@
return $userId;
}
/**
* Address bug on \Joomla\CMS\MVC\Model\FormBehaviorTrait Line 76
* The use of JPATH_COMPONENT cause it to load the
* active component forms and fields, which breaks the registration model.
*
* @param int $mode
*
* @since 5.0.3
*/
private static function setFormPathForUserClass(int $mode): void
{
if ($mode == 1) // 1 = use of the Registration Model
{
// Get the form.
Form::addFormPath(JPATH_ROOT . '/components/com_users/forms');
}
}

View File

@ -31,6 +31,6 @@
"namespace": "[[[NamespacePrefix]]]\\Joomla\\[[[ComponentNamespace]]].Utilities.UserHelper",
"description": "Create & Update User [Save]\r\n\r\n@since 5.0.2",
"licensing_template": "\/**\r\n * @package Joomla.Component.Builder\r\n *\r\n * @created 3rd September, 2020\r\n * @author Llewellyn van der Merwe <https:\/\/dev.vdm.io>\r\n * @git Joomla Component Builder <https:\/\/git.vdm.dev\/joomla\/Component-Builder>\r\n * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.\r\n * @license GNU General Public License version 2 or later; see LICENSE.txt\r\n *\/\r\n",
"head": "use Joomla\\CMS\\Factory;\r\nuse Joomla\\CMS\\Language\\Text;\r\nuse Joomla\\CMS\\User\\User;\r\nuse Joomla\\CMS\\User\\UserHelper as JoomlaUserHelper;\r\nuse Joomla\\CMS\\MVC\\Model\\BaseDatabaseModel;",
"head": "use Joomla\\CMS\\Factory;\r\nuse Joomla\\CMS\\Form\\Form;\r\nuse Joomla\\CMS\\Language\\Text;\r\nuse Joomla\\CMS\\User\\User;\r\nuse Joomla\\CMS\\User\\UserHelper as JoomlaUserHelper;\r\nuse Joomla\\CMS\\MVC\\Model\\BaseDatabaseModel;",
"composer": ""
}

View File

@ -14,7 +14,7 @@
interface ModelInterface #Lavender {
+ table(string $table) : self
+ value(mixed $value, string $field, ...) : mixed
+ values(?array $items = null, string $field, ...) : ?array
+ values(?array $items, string $field, ...) : ?array
+ item(?object $item, ?string $table = null) : ?object
+ items(?array $items = null, ?string $table = null) : ?array
+ row(?array $item, ?string $table = null) : ?array
@ -52,7 +52,7 @@ Example: $this->items(Array, 'value_key', 'table_name');
return: ?array
arguments:
?array $items = null
?array $items
string $field
?string $table = null
end note

View File

@ -53,7 +53,7 @@ interface ModelInterface
* @return array|null
* @since 3.2.0
*/
public function values(?array $items = null, string $field, ?string $table = null): ?array;
public function values(?array $items, string $field, ?string $table = null): ?array;
/**
* Model the values of an item

View File

@ -32,7 +32,7 @@
* @return array|null
* @since 3.2.0
*/
public function values(?array $items = null, string $field, ?string $table = null): ?array;
public function values(?array $items, string $field, ?string $table = null): ?array;
/**
* Model the values of an item