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