225 lines
8.6 KiB
PHP
225 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Core\Authentication;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
|
use TYPO3\CMS\Core\Authentication\Event\AfterGroupsResolvedEvent;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* A provider for resolving fe_groups / be_groups, including nested sub groups.
|
|
*
|
|
* When fetching subgroups, the current group (parent group) is handed in recursive.
|
|
* Duplicates are suppressed: If a subgroup is including in multiple parent groups,
|
|
* it will be resolved only once.
|
|
*
|
|
* @internal this is not part of TYPO3 Core API.
|
|
*/
|
|
#[Autoconfigure(public: true)]
|
|
readonly class GroupResolver
|
|
{
|
|
private const string SOURCE_FIELD = 'usergroup';
|
|
private const string RECURSIVE_SOURCE_FIELD = 'subgroup';
|
|
|
|
public function __construct(
|
|
private EventDispatcherInterface $eventDispatcher,
|
|
private ConnectionPool $connectionPool,
|
|
) {}
|
|
|
|
/**
|
|
* Fetch all group records for a given user recursive.
|
|
*
|
|
* Note order is important: A user with main groups "1,2", where 1 has sub group 3,
|
|
* results in "3,1,2" as record list array - sub groups are listed before the group
|
|
* that includes the sub group.
|
|
*
|
|
* @param array $userRecord Used for context in PSR-14 event
|
|
* @param string $sourceTable The database table to look up: be_groups / fe_groups depending on context
|
|
* @return array List of group records. Note the ordering note above.
|
|
*/
|
|
public function resolveGroupsForUser(array $userRecord, string $sourceTable): array
|
|
{
|
|
$originalGroupIds = GeneralUtility::intExplode(',', (string)($userRecord[self::SOURCE_FIELD] ?? ''), true);
|
|
$resolvedGroups = $this->fetchGroupsRecursive($sourceTable, $originalGroupIds);
|
|
$event = $this->eventDispatcher->dispatch(new AfterGroupsResolvedEvent($sourceTable, $resolvedGroups, $originalGroupIds, $userRecord));
|
|
return $event->getGroups();
|
|
}
|
|
|
|
/**
|
|
* This works the other way around: Find all users that belong to some groups. Because groups are nested,
|
|
* we need to find all groups and subgroups first, because maybe a user is only part of a higher group,
|
|
* instead of a "All editors" group.
|
|
*
|
|
* @param int[] $groupIds a list of IDs of groups
|
|
* @param string $sourceTable e.g. be_groups or fe_groups
|
|
* @param string $userSourceTable e.g. be_users or fe_users
|
|
* @return array full user records
|
|
*/
|
|
public function findAllUsersInGroups(array $groupIds, string $sourceTable, string $userSourceTable): array
|
|
{
|
|
// Ensure the given groups exist
|
|
$mainGroups = $this->fetchRowsFromDatabase($sourceTable, $groupIds);
|
|
$groupIds = array_map(intval(...), array_column($mainGroups, 'uid'));
|
|
if (empty($groupIds)) {
|
|
return [];
|
|
}
|
|
$parentGroupIds = $this->fetchParentGroupsRecursive($sourceTable, $groupIds, $groupIds);
|
|
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($userSourceTable);
|
|
$queryBuilder
|
|
->select('*')
|
|
->from($userSourceTable);
|
|
|
|
$constraints = [];
|
|
foreach ($groupIds as $groupUid) {
|
|
$constraints[] = $queryBuilder->expr()->inSet(self::SOURCE_FIELD, (string)$groupUid);
|
|
}
|
|
foreach ($parentGroupIds as $groupUid) {
|
|
$constraints[] = $queryBuilder->expr()->inSet(self::SOURCE_FIELD, (string)$groupUid);
|
|
}
|
|
|
|
$users = $queryBuilder
|
|
->where(
|
|
$queryBuilder->expr()->or(...$constraints)
|
|
)
|
|
->executeQuery()
|
|
->fetchAllAssociative();
|
|
return !empty($users) ? $users : [];
|
|
}
|
|
|
|
/**
|
|
* Load a list of group uids, and take into account if groups have been loaded before.
|
|
*
|
|
* @param int[] $groupIds
|
|
*/
|
|
protected function fetchGroupsRecursive(string $sourceTable, array $groupIds, array $processedGroupIds = []): array
|
|
{
|
|
if (empty($groupIds)) {
|
|
return [];
|
|
}
|
|
$foundGroups = $this->fetchRowsFromDatabase($sourceTable, $groupIds);
|
|
$validGroups = [];
|
|
foreach ($groupIds as $groupId) {
|
|
// Database did not find the record
|
|
if (!is_array($foundGroups[$groupId] ?? null)) {
|
|
continue;
|
|
}
|
|
// Record was already processed, continue to avoid adding this group again
|
|
if (in_array($groupId, $processedGroupIds, true)) {
|
|
continue;
|
|
}
|
|
// Add sub groups first
|
|
$subgroupIds = GeneralUtility::intExplode(',', (string)($foundGroups[$groupId][self::RECURSIVE_SOURCE_FIELD] ?? ''), true);
|
|
if (!empty($subgroupIds)) {
|
|
$subgroups = $this->fetchGroupsRecursive($sourceTable, $subgroupIds, array_merge($processedGroupIds, [$groupId]));
|
|
$validGroups = array_merge($validGroups, $subgroups);
|
|
}
|
|
// Add main group after sub groups have been added
|
|
$validGroups[] = $foundGroups[$groupId];
|
|
}
|
|
return $validGroups;
|
|
}
|
|
|
|
/**
|
|
* Does the database query. Does not care about ordering, this is done by caller.
|
|
*
|
|
* @return array Full records with record uid as key
|
|
*/
|
|
protected function fetchRowsFromDatabase(string $sourceTable, array $groupIds): array
|
|
{
|
|
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($sourceTable);
|
|
$result = $queryBuilder
|
|
->select('*')
|
|
->from($sourceTable)
|
|
->where(
|
|
$queryBuilder->expr()->in(
|
|
'uid',
|
|
$queryBuilder->createNamedParameter(
|
|
$groupIds,
|
|
Connection::PARAM_INT_ARRAY
|
|
)
|
|
)
|
|
)
|
|
->executeQuery();
|
|
$groups = [];
|
|
while ($row = $result->fetchAssociative()) {
|
|
$groups[(int)$row['uid']] = $row;
|
|
}
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Load a list of group uids, and take into account if groups have been loaded before as part of recursive detection.
|
|
*
|
|
* @param int[] $groupIds a list of groups to find THEIR ancestors
|
|
* @param array $processedGroupIds helper function to avoid recursive detection
|
|
* @return array a list of parent groups and thus, grand grand parent groups as well
|
|
*/
|
|
protected function fetchParentGroupsRecursive(string $sourceTable, array $groupIds, array $processedGroupIds = []): array
|
|
{
|
|
if (empty($groupIds)) {
|
|
return [];
|
|
}
|
|
$parentGroups = $this->fetchParentGroupsFromDatabase($sourceTable, $groupIds);
|
|
$validParentGroupIds = [];
|
|
foreach ($parentGroups as $parentGroup) {
|
|
$parentGroupId = (int)$parentGroup['uid'];
|
|
// Record was already processed, continue to avoid adding this group again
|
|
if (in_array($parentGroupId, $processedGroupIds, true)) {
|
|
continue;
|
|
}
|
|
$processedGroupIds[] = $parentGroupId;
|
|
$validParentGroupIds[] = $parentGroupId;
|
|
}
|
|
|
|
$grandParentGroups = $this->fetchParentGroupsRecursive($sourceTable, $validParentGroupIds, $processedGroupIds);
|
|
return array_merge($validParentGroupIds, $grandParentGroups);
|
|
}
|
|
|
|
/**
|
|
* Find all groups that have a FIND_IN_SET(subgroups, [$subgroupIds]) => the parent groups
|
|
* via one SQL query.
|
|
*/
|
|
protected function fetchParentGroupsFromDatabase(string $sourceTable, array $subgroupIds): array
|
|
{
|
|
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($sourceTable);
|
|
$queryBuilder
|
|
->select('*')
|
|
->from($sourceTable);
|
|
|
|
$constraints = [];
|
|
foreach ($subgroupIds as $subgroupId) {
|
|
$constraints[] = $queryBuilder->expr()->inSet(self::RECURSIVE_SOURCE_FIELD, (string)$subgroupId);
|
|
}
|
|
|
|
$result = $queryBuilder
|
|
->where(
|
|
$queryBuilder->expr()->or(...$constraints)
|
|
)
|
|
->executeQuery();
|
|
|
|
$groups = [];
|
|
while ($row = $result->fetchAssociative()) {
|
|
$groups[(int)$row['uid']] = $row;
|
|
}
|
|
return $groups;
|
|
}
|
|
}
|