Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
RestrictedUserGroupChecker
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
9 / 9
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isGroupRestricted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canPerformerAddTargetToGroup
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 doPerformerAndTargetMeetConditionsForAddingToGroup
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 canPerformerIgnoreGroupRestrictions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 doesPerformerMeetConditions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 doesTargetMeetConditions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getGroupRestrictions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrivateConditionsForGroup
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\User;
8
9use MediaWiki\Permissions\Authority;
10
11/**
12 * A service to check whether a user can be added or removed to/from restricted user groups.
13 * It checks only the restrictions defined for the group and does not perform any other permission checks.
14 *
15 * @since 1.46
16 */
17class RestrictedUserGroupChecker {
18
19    /**
20     * @param array<string,UserGroupRestrictions> $groupRestrictions
21     * @param UserRequirementsConditionChecker $userRequirementsConditionChecker
22     */
23    public function __construct(
24        private readonly array $groupRestrictions,
25        private readonly UserRequirementsConditionChecker $userRequirementsConditionChecker,
26    ) {
27    }
28
29    /**
30     * Checks whether the given group is restricted. A group is considered restricted if it has an entry
31     * defined in $wgRestrictedGroups (even if its value would be an empty array).
32     */
33    public function isGroupRestricted( string $groupName ): bool {
34        return isset( $this->groupRestrictions[ $groupName ] );
35    }
36
37    /**
38     * Checks whether the performer can add the target to the given restricted group.
39     *
40     * Note: This method only tests against the restrictions defined for the group. It doesn't take into account
41     * other permission checks that may apply (e.g., whether the performer has the right to edit user groups at all).
42     *
43     * This method will return null if $evaluatePrivateConditions is set to false and the result depends on the
44     * private conditions.
45     */
46    public function canPerformerAddTargetToGroup(
47        Authority $performer,
48        UserIdentity $target,
49        string $groupName,
50        bool $evaluatePrivateConditions = true
51    ): ?bool {
52        if ( !$this->isGroupRestricted( $groupName ) ) {
53            return true;
54        }
55        if ( $this->canPerformerIgnoreGroupRestrictions( $performer, $groupName ) ) {
56            return true;
57        }
58        return $this->doPerformerAndTargetMeetConditionsForAddingToGroup(
59            $performer->getUser(),
60            $target,
61            $groupName,
62            $evaluatePrivateConditions
63        );
64    }
65
66    /**
67     * Checks whether both the performer and the target meet the conditions required for adding the target to
68     * the given restricted group.
69     *
70     * Note: Even if this method returns false, the performer may still be allowed to add the target to the group
71     * if they can ignore group restrictions (use {@see canPerformerAddTargetToGroup()} for that). Calling this
72     * method may be useful to inform the performer when they ignore the restrictions.
73     *
74     * This method will return null if $evaluatePrivateConditions is set to false and the result depends on the
75     * private conditions.
76     */
77    public function doPerformerAndTargetMeetConditionsForAddingToGroup(
78        UserIdentity $performer,
79        UserIdentity $target,
80        string $groupName,
81        bool $evaluatePrivateConditions = true
82    ): ?bool {
83        $groupRestrictions = $this->getGroupRestrictions( $groupName );
84        if ( !$this->doesPerformerMeetConditions( $performer, $groupRestrictions ) ) {
85            return false;
86        }
87        return $this->doesTargetMeetConditions( $target, $groupRestrictions, $evaluatePrivateConditions );
88    }
89
90    /**
91     * Returns true if the performer can ignore the conditions for adding or removing users to/from the given group.
92     * This is the case if the group allows ignoring restrictions and the performer has the 'ignore-restricted-groups'
93     * permission.
94     */
95    public function canPerformerIgnoreGroupRestrictions( Authority $performer, string $groupName ): bool {
96        $groupRestrictions = $this->getGroupRestrictions( $groupName );
97        if ( !$groupRestrictions->canBeIgnored() ) {
98            return false;
99        }
100        return $performer->isAllowed( 'ignore-restricted-groups' );
101    }
102
103    private function doesPerformerMeetConditions(
104        UserIdentity $performer,
105        UserGroupRestrictions $groupRestrictions
106    ): bool {
107        $performerRestrictions = $groupRestrictions->getUpdaterConditions();
108        if ( !$performerRestrictions ) {
109            // No restrictions, so automatically meets the requirements
110            return true;
111        }
112
113        // Here, we assume that the private conditions are always evaluated. There's no point in hiding
114        // data about the current request performer, as it doesn't leak anything to a third party.
115        return (bool)$this->userRequirementsConditionChecker->recursivelyCheckCondition(
116            $performerRestrictions,
117            $performer
118        );
119    }
120
121    private function doesTargetMeetConditions(
122        UserIdentity $target,
123        UserGroupRestrictions $groupRestrictions,
124        bool $evaluatePrivateConditions
125    ): ?bool {
126        $targetRestrictions = $groupRestrictions->getMemberConditions();
127        if ( !$targetRestrictions ) {
128            // No restrictions, so automatically meets them
129            return true;
130        }
131
132        return $this->userRequirementsConditionChecker->recursivelyCheckCondition(
133            $targetRestrictions,
134            $target,
135            $evaluatePrivateConditions
136        );
137    }
138
139    /**
140     * Get the restrictions defined for a given group.
141     */
142    public function getGroupRestrictions( string $groupName ): UserGroupRestrictions {
143        return $this->groupRestrictions[$groupName] ?? new UserGroupRestrictions( [] );
144    }
145
146    /**
147     * Returns a list of private conditions that apply to members of the specified group.
148     * @param string $groupName
149     * @return list<mixed>
150     */
151    public function getPrivateConditionsForGroup( string $groupName ): array {
152        if ( !$this->isGroupRestricted( $groupName ) ) {
153            return [];
154        }
155
156        $groupRestrictions = $this->getGroupRestrictions( $groupName );
157        return $this->userRequirementsConditionChecker->extractPrivateConditions(
158            $groupRestrictions->getMemberConditions() );
159    }
160}