MediaWiki master
UserGroupAssignmentServiceBase.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\User;
8
11use Wikimedia\Timestamp\ConvertibleTimestamp;
12use Wikimedia\Timestamp\TimestampFormat as TS;
13
22
23 public function __construct(
24 private readonly HookRunner $hookRunner,
25 ) {
26 }
27
47 abstract public function getChangeableGroups(
48 Authority $performer,
49 UserIdentity $target,
50 bool $evaluatePrivateConditionsForRestrictedGroups = true
51 ): array;
52
65 public function validateUserGroups(
66 Authority $performer,
67 UserIdentity $target,
68 array $addGroups,
69 array $removeGroups,
70 array $newExpiries,
71 array $groupMemberships,
72 ): array {
73 // We have to find out which groups the user is unable to change and also whether it's due to
74 // private conditions or not. In the former case, we need to be able to log access to the conditions.
75 $permittedChangesNoPrivate = $this->getChangeableGroups( $performer, $target, false );
76 $permittedChangesWithPrivate = $this->getChangeableGroups( $performer, $target );
77
78 [ $unaddableNoPrivate, $irremovableNoPrivate ] = $this->getDisallowedGroupChanges(
79 $addGroups, $removeGroups, $newExpiries, $groupMemberships, $permittedChangesNoPrivate );
80 [ $unaddableWithPrivate, $irremovableWithPrivate ] = $this->getDisallowedGroupChanges(
81 $addGroups, $removeGroups, $newExpiries, $groupMemberships, $permittedChangesWithPrivate );
82
83 $unchangeableGroupsNoPrivate = array_merge( $unaddableNoPrivate, $irremovableNoPrivate );
84 $unchangeableGroupsWithPrivate = array_merge( $unaddableWithPrivate, $irremovableWithPrivate );
85
86 $unchangeableGroupsDueToPrivate = array_diff( $unchangeableGroupsWithPrivate, $unchangeableGroupsNoPrivate );
87
88 $restrictedGroups = $permittedChangesWithPrivate['restricted'];
89 $knownGroups = $this->getKnownGroups( $target );
90
91 $result = [];
92 foreach ( $unchangeableGroupsWithPrivate as $group ) {
93 // Sometimes people are assigned to groups that no longer are defined. Let's ignore them for validation
94 if ( !in_array( $group, $knownGroups ) ) {
95 continue;
96 }
97
98 if ( in_array( $group, $unchangeableGroupsDueToPrivate ) ) {
99 $result[$group] = 'private-condition';
100 } elseif ( isset( $restrictedGroups[$group] ) ) {
101 $result[$group] = 'restricted';
102 } else {
103 $result[$group] = 'rights';
104 }
105 }
106 return $result;
107 }
108
115 abstract protected function getKnownGroups( UserIdentity $target ): array;
116
127 Authority $performer,
128 UserIdentity $target,
129 array $addGroups,
130 array $newExpiries,
131 array $existingUGMs,
132 ): void {
133 // Potentially changeable - groups that might be changed if it weren't for private conditions
134 $potentiallyChangeable = $this->getChangeableGroups( $performer, $target, false );
135 $conditions = $this->getPrivateConditionsInvolvedInChange(
136 $target,
137 $addGroups,
138 $newExpiries,
139 $existingUGMs,
140 $potentiallyChangeable
141 );
142
143 if ( !$conditions ) {
144 return;
145 }
146
147 $this->hookRunner->onReadPrivateUserRequirementsCondition( $performer->getUser(), $target, $conditions );
148 }
149
161 private function getPrivateConditionsInvolvedInChange(
162 UserIdentity $target,
163 array $addGroups,
164 array $newExpiries,
165 array $existingUGMs,
166 array $potentiallyChangeableGroups,
167 ): array {
168 $restrictedGroups = $potentiallyChangeableGroups['restricted'];
169 $groupsWithPrivateConditionsInvolved = [];
170 foreach ( $restrictedGroups as $group => $groupData ) {
171 if ( $groupData['condition-met'] === null && !$groupData['ignore-condition'] ) {
172 $groupsWithPrivateConditionsInvolved[] = $group;
173 }
174 }
175
176 $groupsToCheck = [];
177 foreach ( $addGroups as $group ) {
178 // If a group is for sure unaddable or addable, private conditions don't matter, so we don't
179 // consider them as involved into the change
180 if ( !in_array( $group, $groupsWithPrivateConditionsInvolved ) ) {
181 continue;
182 }
183
184 // Ensure that we only test groups that are added or prolonged (conditions don't apply for
185 // removals from groups)
186 if ( !isset( $existingUGMs[$group] ) ) {
187 $groupsToCheck[] = $group;
188 continue;
189 }
190 $currentExpiry = $existingUGMs[$group]->getExpiry() ?? 'infinity';
191 $newExpiry = $newExpiries[$group] ?? 'infinity';
192
193 if ( $newExpiry > $currentExpiry ) {
194 $groupsToCheck[] = $group;
195 }
196 }
197
198 $privateConditions = [];
199 $restrictedGroupChecker = $this->getRestrictedGroupChecker( $target );
200 foreach ( $groupsToCheck as $group ) {
201 $privateConditions = array_merge(
202 $privateConditions,
203 $restrictedGroupChecker->getPrivateConditionsForGroup( $group )
204 );
205 }
206 return array_values( array_unique( $privateConditions ) );
207 }
208
213
225 public static function enforceChangeGroupPermissions(
226 array &$addGroups,
227 array &$removeGroups,
228 array &$newExpiries,
229 array $existingUGMs,
230 array $permittedChanges
231 ): void {
232 [ $unaddableGroups, $irremovableGroups ] = self::getDisallowedGroupChanges(
233 $addGroups, $removeGroups, $newExpiries, $existingUGMs, $permittedChanges
234 );
235
236 $addGroups = array_diff( $addGroups, $unaddableGroups );
237 $removeGroups = array_diff( $removeGroups, $irremovableGroups );
238 foreach ( $unaddableGroups as $group ) {
239 unset( $newExpiries[$group] );
240 }
241 }
242
252 private static function getDisallowedGroupChanges(
253 array $addGroups,
254 array $removeGroups,
255 array $newExpiries,
256 array $existingUGMs,
257 array $permittedChanges
258 ): array {
259 $canAdd = $permittedChanges['add'];
260 $canRemove = $permittedChanges['remove'];
261 $involvedGroups = array_unique( array_merge( array_keys( $existingUGMs ), $addGroups, $removeGroups ) );
262
263 // These do not reflect actual permissions, but rather the groups to remove from $addGroups and $removeGroups
264 $unaddableGroups = [];
265 $irremovableGroups = [];
266
267 foreach ( $involvedGroups as $group ) {
268 $hasGroup = isset( $existingUGMs[$group] );
269 $wantsAddGroup = in_array( $group, $addGroups );
270 $wantsRemoveGroup = in_array( $group, $removeGroups );
271
272 // Better safe than sorry - catch it if the input is contradictory
273 if (
274 ( !$hasGroup && $wantsRemoveGroup ) ||
275 ( $wantsAddGroup && $wantsRemoveGroup )
276 ) {
277 $unaddableGroups[] = $group;
278 $irremovableGroups[] = $group;
279 continue;
280 }
281 // If there's no change, we don't have to change anything
282 if ( !$hasGroup && !$wantsAddGroup ) {
283 continue;
284 }
285 if ( $hasGroup && !$wantsRemoveGroup && !$wantsAddGroup ) {
286 // We have to check for adding group, because it's set when changing expiry
287 continue;
288 }
289
290 if ( $hasGroup && $wantsRemoveGroup ) {
291 if ( !in_array( $group, $canRemove ) ) {
292 $irremovableGroups[] = $group;
293 }
294 } elseif ( !$hasGroup && $wantsAddGroup ) {
295 if ( !in_array( $group, $canAdd ) ) {
296 $unaddableGroups[] = $group;
297 }
298 } elseif ( $hasGroup && $wantsAddGroup ) {
299 $currentExpiry = $existingUGMs[$group]->getExpiry() ?? 'infinity';
300 $wantedExpiry = $newExpiries[$group] ?? 'infinity';
301
302 if ( $wantedExpiry > $currentExpiry ) {
303 // Prolongation requires 'add' permission
304 $canChange = in_array( $group, $canAdd );
305 } else {
306 // Shortening requires 'remove' permission
307 $canChange = in_array( $group, $canRemove );
308 }
309
310 if ( !$canChange ) {
311 // Restore the original group expiry if user can't change it
312 $unaddableGroups[] = $group;
313 }
314 }
315 }
316
317 return [ $unaddableGroups, $irremovableGroups ];
318 }
319
329 public static function expiryToTimestamp( $expiry ) {
330 if ( wfIsInfinity( $expiry ) ) {
331 return null;
332 }
333
334 $unix = strtotime( $expiry, ConvertibleTimestamp::time() );
335
336 if ( !$unix || $unix === -1 ) {
337 return false;
338 }
339
340 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
341 // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
342 return wfTimestamp( TS::MW, $unix );
343 }
344}
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfIsInfinity( $str)
Determine input string is represents as infinity.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A service to check whether a user can be added or removed to/from restricted user groups.
This class represents a base for service that provides high-level operations on user groups.
getKnownGroups(UserIdentity $target)
Returns a list of possible known groups for a given identity.
logAccessToPrivateConditions(Authority $performer, UserIdentity $target, array $addGroups, array $newExpiries, array $existingUGMs,)
Triggers a hook that allows extensions to log when user read some private conditions.
getRestrictedGroupChecker(UserIdentity $target)
Returns an instance of {.
static enforceChangeGroupPermissions(array &$addGroups, array &$removeGroups, array &$newExpiries, array $existingUGMs, array $permittedChanges)
Ensures that the content of $addGroups, $removeGroups and $newExpiries is compliant with the possible...
__construct(private readonly HookRunner $hookRunner,)
getChangeableGroups(Authority $performer, UserIdentity $target, bool $evaluatePrivateConditionsForRestrictedGroups=true)
Returns the groups that the performer can add or remove from the target user.
validateUserGroups(Authority $performer, UserIdentity $target, array $addGroups, array $removeGroups, array $newExpiries, array $groupMemberships,)
Validates the requested changes to user groups and returns an array, specifying if some groups are un...
static expiryToTimestamp( $expiry)
Converts a user group membership expiry string into a timestamp.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
getUser()
Returns the performer of the actions associated with this authority.
Interface for objects representing user identity.