Passed
Push — master ( d4cbdc...769bad )
by
unknown
13:22
created

GroupResolver::resolveGroupsForUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Authentication;
19
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use TYPO3\CMS\Core\Authentication\Event\AfterGroupsResolvedEvent;
22
use TYPO3\CMS\Core\Database\Connection;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26
/**
27
 * A provider for resolving fe_groups / be_groups, including nested sub groups.
28
 *
29
 * When fetching subgroups, the current group (parent group) is handed in recursive.
30
 * Duplicates are suppressed: If a sub group is including in multiple parent groups,
31
 * it will be resolved only once.
32
 *
33
 * @internal this is not part of TYPO3 Core API.
34
 */
35
class GroupResolver
36
{
37
    protected EventDispatcherInterface $eventDispatcher;
38
    protected string $sourceTable = '';
39
    protected string $sourceField = 'usergroup';
40
    protected string $recursiveSourceField = 'subgroup';
41
42
    public function __construct(EventDispatcherInterface $eventDispatcher)
43
    {
44
        $this->eventDispatcher = $eventDispatcher;
45
    }
46
47
    /**
48
     * Fetch all group records for a given user recursive.
49
     *
50
     * Note order is important: A user with main groups "1,2", where 1 has sub group 3,
51
     * results in "3,1,2" as record list array - sub groups are listed before the group
52
     * that includes the sub group.
53
     *
54
     * @param array $userRecord Used for context in PSR-14 event
55
     * @param string $sourceTable The database table to look up: be_groups / fe_groups depending on context
56
     * @return array List of group records. Note the ordering note above.
57
     */
58
    public function resolveGroupsForUser(array $userRecord, string $sourceTable): array
59
    {
60
        $this->sourceTable = $sourceTable;
61
        $originalGroupIds = GeneralUtility::intExplode(',', $userRecord[$this->sourceField] ?? '', true);
62
        $resolvedGroups = $this->fetchGroupsRecursive($originalGroupIds);
63
        $event = $this->eventDispatcher->dispatch(new AfterGroupsResolvedEvent($sourceTable, $resolvedGroups, $originalGroupIds, $userRecord));
64
        return $event->getGroups();
65
    }
66
67
    /**
68
     * Load a list of group uids, and take into account if groups have been loaded before.
69
     *
70
     * @param int[] $groupIds
71
     * @param array $processedGroupIds
72
     * @return array
73
     */
74
    protected function fetchGroupsRecursive(array $groupIds, array $processedGroupIds = []): array
75
    {
76
        if (empty($groupIds)) {
77
            return [];
78
        }
79
        $foundGroups = $this->fetchRowsFromDatabase($groupIds);
80
        $validGroups = [];
81
        foreach ($groupIds as $groupId) {
82
            // Database did not find the record
83
            if (!is_array($foundGroups[$groupId])) {
84
                continue;
85
            }
86
            // Record was already processed, continue to avoid adding this group again
87
            if (in_array($groupId, $processedGroupIds, true)) {
88
                continue;
89
            }
90
            // Add sub groups first
91
            $subgroupIds = GeneralUtility::intExplode(',', $foundGroups[$groupId][$this->recursiveSourceField] ?? '', true);
92
            if (!empty($subgroupIds)) {
93
                $subgroups = $this->fetchGroupsRecursive($subgroupIds, array_merge($processedGroupIds, [$groupId]));
94
                $validGroups = array_merge($validGroups, $subgroups);
95
            }
96
            // Add main group after sub groups have been added
97
            $validGroups[] = $foundGroups[$groupId];
98
        }
99
        return $validGroups;
100
    }
101
102
    /**
103
     * Does the database query. Does not care about ordering, this is done by caller.
104
     *
105
     * @param array $groupIds
106
     * @return array Full records with record uid as key
107
     */
108
    protected function fetchRowsFromDatabase(array $groupIds): array
109
    {
110
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->sourceTable);
111
        $result = $queryBuilder
112
            ->select('*')
113
            ->from($this->sourceTable)
114
            ->where(
115
                $queryBuilder->expr()->in(
116
                    'uid',
117
                    $queryBuilder->createNamedParameter(
118
                        $groupIds,
119
                        Connection::PARAM_INT_ARRAY
120
                    )
121
                )
122
            )
123
            ->execute();
124
        $groups = [];
125
        while ($row = $result->fetch()) {
126
            $groups[(int)$row['uid']] = $row;
127
        }
128
        return $groups;
129
    }
130
131
    /**
132
     * This works the other way around: Find all users that belong to some groups. Because groups are nested,
133
     * we need to find all groups and subgroups first, because maybe a user is only part of a higher group,
134
     * instead of a "All editors" group.
135
     *
136
     * @param int[] $groupIds a list of IDs of groups
137
     * @param string $sourceTable e.g. be_groups or fe_groups
138
     * @param string $userSourceTable e.g. be_users or fe_users
139
     * @return array full user records
140
     */
141
    public function findAllUsersInGroups(array $groupIds, string $sourceTable, string $userSourceTable): array
142
    {
143
        $this->sourceTable = $sourceTable;
144
145
        // Ensure the given groups exist
146
        $mainGroups = $this->fetchRowsFromDatabase($groupIds);
147
        $groupIds = array_map('intval', array_column($mainGroups, 'uid'));
148
        if (empty($groupIds)) {
149
            return [];
150
        }
151
        $parentGroupIds = $this->fetchParentGroupsRecursive($groupIds, $groupIds);
152
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($userSourceTable);
153
        $queryBuilder
154
            ->select('*')
155
            ->from($userSourceTable);
156
157
        $constraints = [];
158
        foreach ($groupIds as $groupUid) {
159
            $constraints[] = $queryBuilder->expr()->inSet($this->sourceField, (string)$groupUid);
160
        }
161
        foreach ($parentGroupIds as $groupUid) {
162
            $constraints[] = $queryBuilder->expr()->inSet($this->sourceField, (string)$groupUid);
163
        }
164
165
        $users = $queryBuilder
166
            ->where(
167
                $queryBuilder->expr()->orX(...$constraints)
168
            )
169
            ->execute()
170
            ->fetchAll();
171
        return !empty($users) ? $users : [];
172
    }
173
174
    /**
175
     * Load a list of group uids, and take into account if groups have been loaded before as part of recursive detection.
176
     *
177
     * @param int[] $groupIds a list of groups to find THEIR ancestors
178
     * @param array $processedGroupIds helper function to avoid recursive detection
179
     * @return array a list of parent groups and thus, grand grand parent groups as well
180
     */
181
    protected function fetchParentGroupsRecursive(array $groupIds, array $processedGroupIds = []): array
182
    {
183
        if (empty($groupIds)) {
184
            return [];
185
        }
186
        $parentGroups = $this->fetchParentGroupsFromDatabase($groupIds);
187
        $validParentGroupIds = [];
188
        foreach ($parentGroups as $parentGroup) {
189
            $parentGroupId = (int)$parentGroup['uid'];
190
            // Record was already processed, continue to avoid adding this group again
191
            if (in_array($parentGroupId, $processedGroupIds, true)) {
192
                continue;
193
            }
194
            $processedGroupIds[] = $parentGroupId;
195
            $validParentGroupIds[] = $parentGroupId;
196
        }
197
198
        $grandParentGroups = $this->fetchParentGroupsRecursive($validParentGroupIds, $processedGroupIds);
199
        return array_merge($validParentGroupIds, $grandParentGroups);
200
    }
201
202
    /**
203
     * Find all groups that have a FIND_IN_SET(subgroups, [$subgroupIds]) => the parent groups
204
     * via one SQL query.
205
     *
206
     * @param array $subgroupIds
207
     * @return array
208
     */
209
    protected function fetchParentGroupsFromDatabase(array $subgroupIds): array
210
    {
211
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->sourceTable);
212
        $queryBuilder
213
            ->select('*')
214
            ->from($this->sourceTable);
215
216
        $constraints = [];
217
        foreach ($subgroupIds as $subgroupId) {
218
            $constraints[] = $queryBuilder->expr()->inSet($this->recursiveSourceField, (string)$subgroupId);
219
        }
220
221
        $result = $queryBuilder
222
            ->where(
223
                $queryBuilder->expr()->orX(...$constraints)
224
            )
225
            ->execute();
226
227
        $groups = [];
228
        while ($row = $result->fetch()) {
229
            $groups[(int)$row['uid']] = $row;
230
        }
231
        return $groups;
232
    }
233
}
234