Lines 89.54% 137 / 153
Methods 72.50% 29 / 40
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 3 / 3 100.00% 1 / 1 1
 getGroupRights 100.00% 1 / 1 100.00% 1 / 1 1
 hasPermission 75.00% 9 / 12 0.00% 0 / 1 6.56
 checkUserGroupRight 100.00% 1 / 1 100.00% 1 / 1 1
 grantGroupRight 100.00% 4 / 4 100.00% 1 / 1 3
 addGroup 85.71% 6 / 7 0.00% 0 / 1 3.03
 getGroupId 100.00% 1 / 1 100.00% 1 / 1 1
 checkGroupData 100.00% 7 / 7 100.00% 1 / 1 3
 changeGroup 100.00% 2 / 2 100.00% 1 / 1 1
 deleteGroup 83.33% 5 / 6 0.00% 0 / 1 3.04
 getGroupMembers 100.00% 1 / 1 100.00% 1 / 1 1
 getUserGroups 100.00% 1 / 1 100.00% 1 / 1 1
 getAllGroupsOptions 91.66% 11 / 12 0.00% 0 / 1 5.01
 getAllGroups 80.00% 4 / 5 0.00% 0 / 1 4.13
 getGroupName 100.00% 1 / 1 100.00% 1 / 1 1
 getAllUserRights 100.00% 5 / 5 100.00% 1 / 1 2
 getUserRightsCount 0.00% 0 / 2 0.00% 0 / 1 6
 getUserGroupRights 100.00% 1 / 1 100.00% 1 / 1 1
 autoJoin 100.00% 6 / 6 100.00% 1 / 1 3
 addToGroup 80.00% 4 / 5 0.00% 0 / 1 4.13
 getGroupData 100.00% 1 / 1 100.00% 1 / 1 1
 removeFromAllGroups 100.00% 1 / 1 100.00% 1 / 1 1
 removeFromGroup 100.00% 3 / 3 100.00% 1 / 1 3
 refuseAllGroupRights 100.00% 1 / 1 100.00% 1 / 1 1
 removeAllUsersFromGroup 100.00% 1 / 1 100.00% 1 / 1 1
 findOrCreateGroupByName 100.00% 9 / 9 100.00% 1 / 1 3
 hasPermissionForCategory 76.92% 10 / 13 0.00% 0 / 1 7.60
 getCategoryRestrictions 100.00% 1 / 1 100.00% 1 / 1 1
 getAllCategoryRestrictions 100.00% 1 / 1 100.00% 1 / 1 1
 setCategoryRestrictions 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Permission\BasicPermission] grantUserRight 75.00% 3 / 4 0.00% 0 / 1 2.06
 [phpMyFAQ\Permission\BasicPermission] getRightData 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Permission\BasicPermission] getRightId 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Permission\BasicPermission] checkUserRight 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Permission\BasicPermission] getUserRights 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Permission\BasicPermission] addRight 90.00% 9 / 10 0.00% 0 / 1 5.03
 [phpMyFAQ\Permission\BasicPermission] checkRightData 100.00% 14 / 14 100.00% 1 / 1 6
 [phpMyFAQ\Permission\BasicPermission] renameRight 75.00% 3 / 4 0.00% 0 / 1 2.06
 [phpMyFAQ\Permission\BasicPermission] getAllRightsData 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Permission\BasicPermission] refuseAllUserRights 100.00% 1 / 1 100.00% 1 / 1 1
33class MediumPermission extends BasicPermission implements PermissionInterface
34{
35    protected MediumPermissionRepository $mediumRepository;
36
37    protected GroupCategoryPermissionRepository $categoryPermissionRepository;
38
39    public function __construct(
40        protected Configuration $configuration,
41    ) {
42        parent::__construct($configuration);
43        $this->mediumRepository = new MediumPermissionRepository($configuration);
44        $this->categoryPermissionRepository = new GroupCategoryPermissionRepository($configuration);
45    }
46
47    /**
48     * Default data for new groups.
49     *
50     * @var array<string, string|bool>
51     */
52    public array $defaultGroupData = [
53        'name' => 'DEFAULT_GROUP',
54        'description' => 'Short group description.',
55        'auto_join' => false,
56    ];
57
58    /**
59     * Returns an array that contains the right-IDs of all
60     * group-rights the group $groupId owns.
61     *
62     * @param int $groupId Group ID
63     *
64     * @return array<int>
65     */
66    public function getGroupRights(int $groupId): array
67    {
68        return $this->mediumRepository->getGroupRights($groupId);
69    }
70
71    /**
72     * Returns true, if the user given by $userId owns the right
73     * specified by $right. It does not matter if the user owns this
74     * right as a user-right or because of a group-membership.
75     * The parameter $right may be a right-ID (recommended for
76     * performance) or a right-name.
77     *
78     * @param int   $userId Group ID
79     * @param mixed $right  Rights
80     * @throws Exception
81     */
82    #[\Override]
83    public function hasPermission(int $userId, mixed $right): bool
84    {
85        $currentUser = new CurrentUser($this->configuration);
86        $currentUser->getUserById($userId);
87
88        if ($currentUser->isSuperAdmin()) {
89            return true;
90        }
91
92        // get right id
93        if (!is_numeric($right) && is_string($right)) {
94            $right = $this->getRightId($right);
95        }
96
97        if ($right instanceof PermissionType) {
98            $right = $this->getRightId($right->value);
99        }
100
101        $rightId = (int) $right;
102
103        // check user right and group right
104        if ($this->checkUserGroupRight($userId, $rightId)) {
105            return true;
106        }
107
108        return $this->checkUserRight($userId, $rightId);
109    }
110
111    /**
112     * Returns true if the user $userId owns the right $rightId
113     * because of a group-membership, otherwise false.
114     *
115     * @param int $userId  User ID
116     * @param int $rightId Right ID
117     */
118    public function checkUserGroupRight(int $userId, int $rightId): bool
119    {
120        return $this->mediumRepository->checkUserGroupRight($userId, $rightId);
121    }
122
123    /**
124     * Grants the group given by $groupId the right specified by
125     * $rightId.
126     *
127     * @param int $groupId Group ID
128     * @param int $rightId Right ID
129     */
130    public function grantGroupRight(int $groupId, int $rightId): bool
131    {
132        $right_data = $this->getRightData($rightId);
133        if ($right_data === [] || !($right_data['for_groups'] ?? false)) {
134            return false;
135        }
136
137        return $this->mediumRepository->grantGroupRight($groupId, $rightId);
138    }
139
140    /**
141     * Adds a new group to the database and returns the ID of the
142     * new group. The associative array $groupData contains the
143     * data for the new group.
144     *
145     * @param array<string, int|string|bool> $groupData Array of group data
146     */
147    public function addGroup(array $groupData): int
148    {
149        if ($this->getGroupId((string) ($groupData['name'] ?? '')) > 0) {
150            return 0;
151        }
152
153        $nextId = $this->mediumRepository->nextGroupId();
154        $groupData = $this->checkGroupData($groupData);
155
156        if (!$this->mediumRepository->addGroup($groupData, $nextId)) {
157            return 0;
158        }
159
160        return $nextId;
161    }
162
163    /**
164     * Returns the ID of the group that has the name $name. Returns
165     * 0 if the group-name cannot be found.
166     *
167     * @param string $name Group name
168     */
169    public function getGroupId(string $name): int
170    {
171        return $this->mediumRepository->getGroupId($name);
172    }
173
174    /**
175     * Checks the given associative array $groupData. If a
176     * parameter is incorrect or is missing, it will be replaced
177     * by the default values in $this->defaultGroupData.
178     * Returns the corrected $groupData associative array.
179     *
180     * @param array<string, int|string|bool> $groupData Array of group data
181     *
182     * @return array<string, int|string>
183     */
184    public function checkGroupData(array $groupData): array
185    {
186        $name = $groupData['name'] ?? null;
187        $description = $groupData['description'] ?? null;
188
189        return [
190            'name' => is_string($name) ? $name : (string) $this->defaultGroupData['name'],
191            'description' => is_string($description) ? $description : (string) $this->defaultGroupData['description'],
192            'auto_join' => (int) ($groupData['auto_join'] ?? $this->defaultGroupData['auto_join']),
193        ];
194    }
195
196    /**
197     * Changes the group data of the given group.
198     *
199     * @param int $groupId Group ID
200     * @param array<string, int|string|bool> $groupData Array of group data
201     */
202    public function changeGroup(int $groupId, array $groupData): bool
203    {
204        $checkedData = $this->checkGroupData($groupData);
205        return $this->mediumRepository->changeGroup($groupId, $checkedData);
206    }
207
208    /**
209     * Removes the group given by $groupId from the database.
210     * Returns true if successful, otherwise false.
211     *
212     * @param int $groupId Group ID
213     */
214    public function deleteGroup(int $groupId): bool
215    {
216        if (!$this->mediumRepository->deleteGroup($groupId)) {
217            return false;
218        }
219
220        if (!$this->mediumRepository->deleteGroupMemberships($groupId)) {
221            return false;
222        }
223
224        $this->categoryPermissionRepository->deleteAllForGroup($groupId);
225
226        return $this->mediumRepository->deleteGroupRights($groupId);
227    }
228
229    /**
230     * Returns an array that contains the user-IDs of all members
231     * of the group $groupId.
232     *
233     * @param int $groupId Group ID
234     *
235     * @return array<int>
236     */
237    public function getGroupMembers(int $groupId): array
238    {
239        return $this->mediumRepository->getGroupMembers($groupId);
240    }
241
242    /**
243     * Returns an array that contains the IDs of all groups in which
244     * the user $userId is a member.
245     *
246     * @param int $userId User ID
247     *
248     * @return array<int>
249     */
250    public function getUserGroups(int $userId): array
251    {
252        return $this->mediumRepository->getUserGroups($userId);
253    }
254
255    /**
256     * Get all groups in <option> tags.
257     *
258     * @param array<int> $groups Selected groups
259     * @todo   Move into the Helper class
260     */
261    public function getAllGroupsOptions(array $groups, CurrentUser $currentUser): string
262    {
263        $options = '';
264        $allGroups = $this->getAllGroups($currentUser);
265
266        foreach ($allGroups as $allGroup) {
267            if (-1 === $allGroup) {
268                continue;
269            }
270
271            $options .= sprintf(
272                '<option value="%d" %s>%s</option>',
273                $allGroup,
274                in_array($allGroup, $groups, strict: true) || ($groups[0] ?? null) === -1 ? 'selected' : '',
275                $this->getGroupName($allGroup),
276            );
277        }
278
279        return $options;
280    }
281
282    /**
283     * Returns an array with the IDs of all groups stored in the
284     * database if no user is passed.
285     *
286     * @return array<int>
287     */
288    public function getAllGroups(CurrentUser $currentUser): array
289    {
290        if (
291            !$this->configuration->get(item: 'main.enableCategoryRestrictions')
292            && $currentUser->getUserId() !== 1
293            && !$currentUser->isSuperAdmin()
294        ) {
295            return $this->mediumRepository->getAllGroups($currentUser->getUserId());
296        }
297
298        return $this->mediumRepository->getAllGroups();
299    }
300
301    /**
302     * Returns the name of the group $groupId.
303     *
304     * @param int $groupId Group ID
305     */
306    public function getGroupName(int $groupId): string
307    {
308        return $this->mediumRepository->getGroupName($groupId);
309    }
310
311    /**
312     * Returns an array that contains the right-IDs of all rights
313     * the user $userId owns. User-rights and the rights the user
314     * owns because of a group-membership is taken into account.
315     *
316     * @param int $userId User ID
317     *
318     * @return array<int>
319     */
320    #[\Override]
321    public function getAllUserRights(int $userId): array
322    {
323        if ($userId <= 0) {
324            return [];
325        }
326
327        $userRights = $this->getUserRights($userId);
328        $groupRights = $this->getUserGroupRights($userId);
329
330        return array_unique(array_merge($userRights, $groupRights));
331    }
332
333    /**
334     * Returns the number of user- and group-rights the user specified by
335     * user_id owns.
336     *
337     * @param CurrentUser $currentUser User object
338     */
339    #[\Override]
340    public function getUserRightsCount(CurrentUser $currentUser): int
341    {
342        $userRights = $this->getAllUserRights($currentUser->getUserId());
343
344        return is_countable($userRights) ? count($userRights) : 0;
345    }
346
347    /**
348     * Returns an array that contains the IDs of all rights the user
349     * $userId owns because of a group-membership.
350     *
351     * @param int $userId User ID
352     *
353     * @return array<int>
354     */
355    public function getUserGroupRights(int $userId): array
356    {
357        return $this->mediumRepository->getUserGroupRights($userId);
358    }
359
360    /**
361     * Adds the user $userId to all groups with the auto_join
362     * option. By using the auto_join option, user administration
363     * can be much easier. For example, by setting this option only
364     * for a single group called 'All Users'. The autoJoin() method
365     * then has to be called every time a new user registers.
366     * Returns true if successful, otherwise false.
367     *
368     * @param int $userId User ID
369     */
370    public function autoJoin(int $userId): bool
371    {
372        if ($userId <= 0) {
373            return false;
374        }
375
376        $autoJoinGroups = $this->mediumRepository->getAutoJoinGroups();
377
378        // add to groups
379        foreach ($autoJoinGroups as $autoJoinGroup) {
380            $this->addToGroup($userId, $autoJoinGroup);
381        }
382
383        return true;
384    }
385
386    /**
387     * Adds a new member $userId to the group $groupId.
388     * Returns true if successful, otherwise false.
389     *
390     * @param int $userId  User ID
391     * @param int $groupId Group ID
392     */
393    public function addToGroup(int $userId, int $groupId): bool
394    {
395        if ($userId <= 0 || $groupId <= 0) {
396            return false;
397        }
398
399        if (!$this->getGroupData($groupId)) {
400            return false;
401        }
402
403        return $this->mediumRepository->addToGroup($userId, $groupId);
404    }
405
406    /**
407     * Returns an associative array with the group-data of the group
408     * $groupId.
409     *
410     * @param int $groupId Group ID
411     *
412     * @return array<array-key, mixed>
413     */
414    public function getGroupData(int $groupId): array
415    {
416        return $this->mediumRepository->getGroupData($groupId);
417    }
418
419    /**
420     * Removes the user $userId from all groups.
421     * Returns true for success, otherwise false.
422     *
423     * @param int $userId User ID
424     */
425    public function removeFromAllGroups(int $userId): bool
426    {
427        return $this->mediumRepository->removeFromAllGroups($userId);
428    }
429
430    /**
431     * Removes a user from a single group.
432     *
433     * @param int $userId User ID
434     * @param int $groupId Group ID
435     */
436    public function removeFromGroup(int $userId, int $groupId): bool
437    {
438        if ($userId <= 0 || $groupId <= 0) {
439            return false;
440        }
441
442        return $this->mediumRepository->removeFromGroup($userId, $groupId);
443    }
444
445    /**
446     * Refuses all group rights.
447     * Returns true for success, otherwise false.
448     *
449     * @param int $groupId Group ID
450     */
451    public function refuseAllGroupRights(int $groupId): bool
452    {
453        return $this->mediumRepository->refuseAllGroupRights($groupId);
454    }
455
456    /**
457     * Removes all users from the group $groupId.
458     * Returns true for success, otherwise false.
459     *
460     * @param int $groupId Group ID
461     */
462    public function removeAllUsersFromGroup(int $groupId): bool
463    {
464        return $this->mediumRepository->removeAllUsersFromGroup($groupId);
465    }
466
467    /**
468     * Finds or creates a group by name.
469     * Returns the group ID on success, 0 on failure.
470     *
471     * @param string $name Group name
472     * @param string $description Optional group description
473     */
474    public function findOrCreateGroupByName(string $name, string $description = ''): int
475    {
476        $groupId = $this->getGroupId($name);
477
478        if ($groupId > 0) {
479            return $groupId;
480        }
481
482        // Create a new group if it doesn't exist
483        $groupData = [
484            'name' => $name,
485            'description' => $description === '' ? 'Auto-created group for ' . $name : $description,
486            'auto_join' => false,
487        ];
488
489        return $this->addGroup($groupData);
490    }
491
492    /**
493     * Returns true if the user has the specified right for the given category,
494     * taking into account group-level category restrictions.
495     *
496     * If the user's group has no category restrictions for a right, the right
497     * applies globally. If restrictions exist, the right only applies to
498     * the specified categories.
499     *
500     * @param int   $userId     User ID
501     * @param mixed $right      Right ID, name, or PermissionType enum
502     * @param int   $categoryId Category ID
503     * @param CurrentUser|null $currentUser Optional pre-loaded user to avoid repeated instantiation
504     * @throws Exception
505     */
506    public function hasPermissionForCategory(
507        int $userId,
508        mixed $right,
509        int $categoryId,
510        ?CurrentUser $currentUser = null,
511    ): bool {
512        if ($currentUser === null) {
513            $currentUser = new CurrentUser($this->configuration);
514            $currentUser->getUserById($userId);
515        }
516
517        if ($currentUser->isSuperAdmin()) {
518            return true;
519        }
520
521        // Resolve right to ID
522        if (!is_numeric($right) && is_string($right)) {
523            $right = $this->getRightId($right);
524        }
525
526        if ($right instanceof PermissionType) {
527            $right = $this->getRightId($right->value);
528        }
529
530        $rightId = (int) $right;
531
532        // Check direct user right (always global, no category restriction)
533        if ($this->checkUserRight($userId, $rightId)) {
534            return true;
535        }
536
537        // Check group right with category restrictions
538        return $this->categoryPermissionRepository->checkUserGroupRightForCategory($userId, $rightId, $categoryId);
539    }
540
541    /**
542     * Returns the category IDs that a group's right is restricted to.
543     * An empty array means the right is unrestricted (applies globally).
544     *
545     * @param int $groupId Group ID
546     * @param int $rightId Right ID
547     * @return array<int>
548     */
549    public function getCategoryRestrictions(int $groupId, int $rightId): array
550    {
551        return $this->categoryPermissionRepository->getCategoryRestrictions($groupId, $rightId);
552    }
553
554    /**
555     * Returns all category restrictions for a group, keyed by right ID.
556     *
557     * @param int $groupId Group ID
558     * @return array<int, array<int>> Map of right_id => [category_ids]
559     */
560    public function getAllCategoryRestrictions(int $groupId): array
561    {
562        return $this->categoryPermissionRepository->getAllCategoryRestrictions($groupId);
563    }
564
565    /**
566     * Sets category restrictions for a group's right.
567     *
568     * @param int $groupId Group ID
569     * @param int $rightId Right ID
570     * @param array<int> $categoryIds Category IDs to restrict to (empty = unrestricted)
571     */
572    public function setCategoryRestrictions(int $groupId, int $rightId, array $categoryIds): bool
573    {
574        return $this->categoryPermissionRepository->setCategoryRestrictions($groupId, $rightId, $categoryIds);
575    }
576}

Inherited from phpMyFAQ\Permission\BasicPermission

62    public function grantUserRight(int $userId, int $rightId): bool
63    {
64        $rightData = $this->getRightData($rightId);
65
66        if (!array_key_exists('for_users', $rightData)) {
67            return false;
68        }
69
70        return $this->repository->grantUserRight($userId, $rightId);
71    }
80    public function getRightData(int $rightId): array
81    {
82        return $this->repository->getRightData($rightId);
83    }
120    public function getRightId(string $name): int
121    {
122        return $this->repository->getRightId($name);
123    }
132    public function checkUserRight(int $userId, int $rightId): bool
133    {
134        return $this->repository->checkUserRight($userId, $rightId);
135    }
172    public function getUserRights(int $userId): array
173    {
174        return $this->repository->getUserRights($userId);
175    }
184    public function addRight(array $rightData): int
185    {
186        if ($this->getRightId((string) ($rightData['name'] ?? '')) > 0) {
187            return 0;
188        }
189
190        $nextId = $this->repository->nextRightId();
191        $checkedRightData = $this->checkRightData($rightData);
192        $rightRow = [];
193        foreach ($checkedRightData as $fieldName => $fieldValue) {
194            $rightRow[$fieldName] = is_int($fieldValue) ? $fieldValue : (string) $fieldValue;
195        }
196
197        if (!$this->repository->addRight($rightRow, $nextId)) {
198            return 0;
199        }
200
201        return $nextId;
202    }
214    public function checkRightData(array $rightData): array
215    {
216        $stringFields = ['name', 'description'];
217        foreach ($stringFields as $field) {
218            if (array_key_exists($field, $rightData) && is_string($rightData[$field])) {
219                continue;
220            }
221
222            $rightData[$field] = $this->defaultRightData[$field];
223        }
224
225        $booleanLikeFields = ['for_users', 'for_groups', 'for_sections'];
226        foreach ($booleanLikeFields as $field) {
227            if (array_key_exists($field, $rightData)) {
228                continue;
229            }
230
231            $rightData[$field] = $this->defaultRightData[$field];
232        }
233
234        $rightData['for_users'] = (int) $rightData['for_users'];
235        $rightData['for_groups'] = (int) $rightData['for_groups'];
236        $rightData['for_sections'] = (int) $rightData['for_sections'];
237
238        return $rightData;
239    }
244    public function renameRight(string $oldName, string $newName): bool
245    {
246        $rightId = $this->getRightId($oldName);
247        if ($rightId === 0) {
248            return false;
249        }
250
251        return $this->repository->renameRight($rightId, $newName);
252    }
265    public function getAllRightsData(string $order = 'ASC'): array
266    {
267        return $this->repository->getAllRightsData($order);
268    }
276    public function refuseAllUserRights(int $userId): bool
277    {
278        return $this->repository->refuseAllUserRights($userId);
279    }