Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
36.76% |
211 / 574 |
|
25.00% |
6 / 24 |
CRAP | |
0.00% |
0 / 1 |
SpecialUserRights | |
36.82% |
211 / 573 |
|
25.00% |
6 / 24 |
7284.73 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
userCanChangeRights | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
72 | |||
execute | |
57.69% |
45 / 78 |
|
0.00% |
0 / 1 |
54.40 | |||
getSuccessURL | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canProcessExpiries | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
expiryToTimestamp | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
4.59 | |||
saveUserGroups | |
77.14% |
27 / 35 |
|
0.00% |
0 / 1 |
20.45 | |||
doSaveUserGroups | |
82.69% |
43 / 52 |
|
0.00% |
0 / 1 |
22.07 | |||
serialiseUgmForLog | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
addLogEntry | |
95.83% |
23 / 24 |
|
0.00% |
0 / 1 |
5 | |||
editUserGroupsForm | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
fetchUser | |
47.22% |
17 / 36 |
|
0.00% |
0 / 1 |
53.64 | |||
makeGroupNameList | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
switchForm | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
3 | |||
showEditUserGroupsForm | |
0.00% |
0 / 121 |
|
0.00% |
0 / 1 |
182 | |||
groupCheckboxes | |
0.00% |
0 / 124 |
|
0.00% |
0 / 1 |
1482 | |||
canRemove | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
canAdd | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
changeableGroups | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDisplayUsername | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
showLogFragment | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
prefixSearchSubpages | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Implements Special:Userrights |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup SpecialPage |
22 | */ |
23 | |
24 | namespace MediaWiki\Specials; |
25 | |
26 | use IDBAccessObject; |
27 | use LogEventsList; |
28 | use LogPage; |
29 | use ManualLogEntry; |
30 | use MediaWiki\CommentStore\CommentStore; |
31 | use MediaWiki\Html\Html; |
32 | use MediaWiki\Linker\Linker; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\MediaWikiServices; |
35 | use MediaWiki\Output\OutputPage; |
36 | use MediaWiki\SpecialPage\SpecialPage; |
37 | use MediaWiki\Status\Status; |
38 | use MediaWiki\Title\Title; |
39 | use MediaWiki\User\ActorStoreFactory; |
40 | use MediaWiki\User\UserFactory; |
41 | use MediaWiki\User\UserGroupManager; |
42 | use MediaWiki\User\UserGroupManagerFactory; |
43 | use MediaWiki\User\UserGroupMembership; |
44 | use MediaWiki\User\UserIdentity; |
45 | use MediaWiki\User\UserNamePrefixSearch; |
46 | use MediaWiki\User\UserNameUtils; |
47 | use MediaWiki\Watchlist\WatchlistManager; |
48 | use MediaWiki\WikiMap\WikiMap; |
49 | use PermissionsError; |
50 | use UserBlockedError; |
51 | use Xml; |
52 | use XmlSelect; |
53 | |
54 | /** |
55 | * Special page to allow managing user group membership |
56 | * |
57 | * @ingroup SpecialPage |
58 | */ |
59 | class SpecialUserRights extends SpecialPage { |
60 | /** |
61 | * The target of the local right-adjuster's interest. Can be gotten from |
62 | * either a GET parameter or a subpage-style parameter, so have a member |
63 | * variable for it. |
64 | * @var null|string |
65 | */ |
66 | protected $mTarget; |
67 | /** |
68 | * @var null|UserIdentity The user object of the target username or null. |
69 | */ |
70 | protected $mFetchedUser = null; |
71 | protected $isself = false; |
72 | |
73 | private UserGroupManagerFactory $userGroupManagerFactory; |
74 | |
75 | /** @var UserGroupManager|null The UserGroupManager of the target username or null */ |
76 | private $userGroupManager = null; |
77 | |
78 | private UserNameUtils $userNameUtils; |
79 | private UserNamePrefixSearch $userNamePrefixSearch; |
80 | private UserFactory $userFactory; |
81 | private ActorStoreFactory $actorStoreFactory; |
82 | private WatchlistManager $watchlistManager; |
83 | |
84 | /** |
85 | * @param UserGroupManagerFactory|null $userGroupManagerFactory |
86 | * @param UserNameUtils|null $userNameUtils |
87 | * @param UserNamePrefixSearch|null $userNamePrefixSearch |
88 | * @param UserFactory|null $userFactory |
89 | * @param ActorStoreFactory|null $actorStoreFactory |
90 | * @param WatchlistManager|null $watchlistManager |
91 | */ |
92 | public function __construct( |
93 | UserGroupManagerFactory $userGroupManagerFactory = null, |
94 | UserNameUtils $userNameUtils = null, |
95 | UserNamePrefixSearch $userNamePrefixSearch = null, |
96 | UserFactory $userFactory = null, |
97 | ActorStoreFactory $actorStoreFactory = null, |
98 | WatchlistManager $watchlistManager = null |
99 | ) { |
100 | parent::__construct( 'Userrights' ); |
101 | $services = MediaWikiServices::getInstance(); |
102 | // This class is extended and therefore falls back to global state - T263207 |
103 | $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils(); |
104 | $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch(); |
105 | $this->userFactory = $userFactory ?? $services->getUserFactory(); |
106 | $this->userGroupManagerFactory = $userGroupManagerFactory ?? $services->getUserGroupManagerFactory(); |
107 | $this->actorStoreFactory = $actorStoreFactory ?? $services->getActorStoreFactory(); |
108 | $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager(); |
109 | } |
110 | |
111 | public function doesWrites() { |
112 | return true; |
113 | } |
114 | |
115 | /** |
116 | * Check whether the current user (from context) can change the target user's rights. |
117 | * |
118 | * This function can be used without submitting the special page |
119 | * |
120 | * @param UserIdentity $targetUser User whose rights are being changed |
121 | * @param bool $checkIfSelf If false, assume that the current user can add/remove groups defined |
122 | * in $wgGroupsAddToSelf / $wgGroupsRemoveFromSelf, without checking if it's the same as target |
123 | * user |
124 | * @return bool |
125 | */ |
126 | public function userCanChangeRights( UserIdentity $targetUser, $checkIfSelf = true ) { |
127 | $isself = $this->getUser()->equals( $targetUser ); |
128 | |
129 | $userGroupManager = $this->userGroupManagerFactory |
130 | ->getUserGroupManager( $targetUser->getWikiId() ); |
131 | $available = $userGroupManager->getGroupsChangeableBy( $this->getAuthority() ); |
132 | if ( !$targetUser->isRegistered() ) { |
133 | return false; |
134 | } |
135 | |
136 | if ( $available['add'] || $available['remove'] ) { |
137 | // can change some rights for any user |
138 | return true; |
139 | } |
140 | |
141 | if ( ( $available['add-self'] || $available['remove-self'] ) |
142 | && ( $isself || !$checkIfSelf ) |
143 | ) { |
144 | // can change some rights for self |
145 | return true; |
146 | } |
147 | |
148 | return false; |
149 | } |
150 | |
151 | /** |
152 | * Manage forms to be shown according to posted data. |
153 | * Depending on the submit button used, call a form or a save function. |
154 | * |
155 | * @param string|null $par String if any subpage provided, else null |
156 | * @throws UserBlockedError|PermissionsError |
157 | */ |
158 | public function execute( $par ) { |
159 | $user = $this->getUser(); |
160 | $request = $this->getRequest(); |
161 | $session = $request->getSession(); |
162 | $out = $this->getOutput(); |
163 | |
164 | $out->addModules( [ 'mediawiki.special.userrights' ] ); |
165 | |
166 | $this->mTarget = $par ?? $request->getVal( 'user' ); |
167 | |
168 | if ( is_string( $this->mTarget ) ) { |
169 | $this->mTarget = trim( $this->mTarget ); |
170 | } |
171 | |
172 | if ( $this->mTarget !== null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->getName() ) { |
173 | $this->isself = true; |
174 | } |
175 | |
176 | $fetchedStatus = $this->mTarget === null ? Status::newFatal( 'nouserspecified' ) : |
177 | $this->fetchUser( $this->mTarget, true ); |
178 | if ( $fetchedStatus->isOK() ) { |
179 | $this->mFetchedUser = $fetchedUser = $fetchedStatus->value; |
180 | // Phan false positive on Status object - T323205 |
181 | '@phan-var UserIdentity $fetchedUser'; |
182 | $wikiId = $fetchedUser->getWikiId(); |
183 | if ( $wikiId === UserIdentity::LOCAL ) { |
184 | // Set the 'relevant user' in the skin, so it displays links like Contributions, |
185 | // User logs, UserRights, etc. |
186 | $this->getSkin()->setRelevantUser( $this->mFetchedUser ); |
187 | } |
188 | $this->userGroupManager = $this->userGroupManagerFactory |
189 | ->getUserGroupManager( $wikiId ); |
190 | } |
191 | |
192 | // show a successbox, if the user rights was saved successfully |
193 | if ( |
194 | $session->get( 'specialUserrightsSaveSuccess' ) && |
195 | $this->mFetchedUser !== null |
196 | ) { |
197 | // Remove session data for the success message |
198 | $session->remove( 'specialUserrightsSaveSuccess' ); |
199 | |
200 | $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' ); |
201 | $out->addHTML( |
202 | Html::successBox( |
203 | Html::element( |
204 | 'p', |
205 | [], |
206 | $this->msg( 'savedrights', $this->getDisplayUsername( $this->mFetchedUser ) )->text() |
207 | ), |
208 | 'mw-notify-success' |
209 | ) |
210 | ); |
211 | } |
212 | |
213 | $this->setHeaders(); |
214 | $this->outputHeader(); |
215 | |
216 | $out->addModuleStyles( 'mediawiki.special' ); |
217 | $this->addHelpLink( 'Help:Assigning permissions' ); |
218 | |
219 | $this->switchForm(); |
220 | |
221 | if ( |
222 | $request->wasPosted() && |
223 | $request->getCheck( 'saveusergroups' ) && |
224 | $this->mTarget !== null && |
225 | $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget ) |
226 | ) { |
227 | /* |
228 | * If the user is blocked and they only have "partial" access |
229 | * (e.g. they don't have the userrights permission), then don't |
230 | * allow them to change any user rights. |
231 | */ |
232 | if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) { |
233 | $block = $user->getBlock(); |
234 | if ( $block && $block->isSitewide() ) { |
235 | throw new UserBlockedError( |
236 | $block, |
237 | $user, |
238 | $this->getLanguage(), |
239 | $request->getIP() |
240 | ); |
241 | } |
242 | } |
243 | |
244 | $this->checkReadOnly(); |
245 | |
246 | // save settings |
247 | if ( !$fetchedStatus->isOK() ) { |
248 | $this->getOutput()->addWikiTextAsInterface( |
249 | $fetchedStatus->getWikiText( false, false, $this->getLanguage() ) |
250 | ); |
251 | |
252 | return; |
253 | } |
254 | |
255 | $targetUser = $this->mFetchedUser; |
256 | $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' ); |
257 | $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck ); |
258 | $userGroups = $this->userGroupManager->getUserGroups( $targetUser, IDBAccessObject::READ_LATEST ); |
259 | |
260 | if ( $userGroups !== $conflictCheck ) { |
261 | $out->addHTML( Html::errorBox( |
262 | $this->msg( 'userrights-conflict' )->parse() |
263 | ) ); |
264 | } else { |
265 | $status = $this->saveUserGroups( |
266 | $request->getVal( 'user-reason' ), |
267 | $targetUser |
268 | ); |
269 | |
270 | if ( $status->isOK() ) { |
271 | // Set session data for the success message |
272 | $session->set( 'specialUserrightsSaveSuccess', 1 ); |
273 | |
274 | $out->redirect( $this->getSuccessURL() ); |
275 | return; |
276 | } else { |
277 | // Print an error message and redisplay the form |
278 | $out->wrapWikiTextAsInterface( |
279 | 'error', $status->getWikiText( false, false, $this->getLanguage() ) |
280 | ); |
281 | } |
282 | } |
283 | } |
284 | |
285 | // show some more forms |
286 | if ( $this->mTarget !== null ) { |
287 | $this->editUserGroupsForm( $this->mTarget ); |
288 | } |
289 | } |
290 | |
291 | private function getSuccessURL() { |
292 | return $this->getPageTitle( $this->mTarget )->getFullURL(); |
293 | } |
294 | |
295 | /** |
296 | * Returns true if this user rights form can set and change user group expiries. |
297 | * Subclasses may wish to override this to return false. |
298 | * |
299 | * @return bool |
300 | */ |
301 | public function canProcessExpiries() { |
302 | return true; |
303 | } |
304 | |
305 | /** |
306 | * Converts a user group membership expiry string into a timestamp. Words like |
307 | * 'existing' or 'other' should have been filtered out before calling this |
308 | * function. |
309 | * |
310 | * @param string $expiry |
311 | * @return string|null|false A string containing a valid timestamp, or null |
312 | * if the expiry is infinite, or false if the timestamp is not valid |
313 | */ |
314 | public static function expiryToTimestamp( $expiry ) { |
315 | if ( wfIsInfinity( $expiry ) ) { |
316 | return null; |
317 | } |
318 | |
319 | $unix = strtotime( $expiry ); |
320 | |
321 | if ( !$unix || $unix === -1 ) { |
322 | return false; |
323 | } |
324 | |
325 | // @todo FIXME: Non-qualified absolute times are not in users specified timezone |
326 | // and there isn't notice about it in the ui (see ProtectionForm::getExpiry) |
327 | return wfTimestamp( TS_MW, $unix ); |
328 | } |
329 | |
330 | /** |
331 | * Save user groups changes in the database. |
332 | * Data comes from the editUserGroupsForm() form function |
333 | * |
334 | * @param string $reason Reason for group change |
335 | * @param UserIdentity $user |
336 | * @return Status |
337 | */ |
338 | protected function saveUserGroups( $reason, $user ) { |
339 | if ( $this->userNameUtils->isTemp( $user->getName() ) ) { |
340 | return Status::newFatal( 'userrights-no-tempuser' ); |
341 | } |
342 | $allgroups = $this->userGroupManager->listAllGroups(); |
343 | $addgroup = []; |
344 | $groupExpiries = []; // associative array of (group name => expiry) |
345 | $removegroup = []; |
346 | $existingUGMs = $this->userGroupManager->getUserGroupMemberships( $user ); |
347 | |
348 | // This could possibly create a highly unlikely race condition if permissions are changed between |
349 | // when the form is loaded and when the form is saved. Ignoring it for the moment. |
350 | foreach ( $allgroups as $group ) { |
351 | // We'll tell it to remove all unchecked groups, and add all checked groups. |
352 | // Later on, this gets filtered for what can actually be removed |
353 | if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) { |
354 | $addgroup[] = $group; |
355 | |
356 | if ( $this->canProcessExpiries() ) { |
357 | // read the expiry information from the request |
358 | $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" ); |
359 | if ( $expiryDropdown === 'existing' ) { |
360 | continue; |
361 | } |
362 | |
363 | if ( $expiryDropdown === 'other' ) { |
364 | $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" ); |
365 | } else { |
366 | $expiryValue = $expiryDropdown; |
367 | } |
368 | |
369 | // validate the expiry |
370 | $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue ); |
371 | |
372 | if ( $groupExpiries[$group] === false ) { |
373 | return Status::newFatal( 'userrights-invalid-expiry', $group ); |
374 | } |
375 | |
376 | // not allowed to have things expiring in the past |
377 | if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) { |
378 | return Status::newFatal( 'userrights-expiry-in-past', $group ); |
379 | } |
380 | |
381 | // if the user can only add this group (not remove it), the expiry time |
382 | // cannot be brought forward (T156784) |
383 | if ( !$this->canRemove( $group ) && |
384 | isset( $existingUGMs[$group] ) && |
385 | ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) > |
386 | ( $groupExpiries[$group] ?: 'infinity' ) |
387 | ) { |
388 | return Status::newFatal( 'userrights-cannot-shorten-expiry', $group ); |
389 | } |
390 | } |
391 | } else { |
392 | $removegroup[] = $group; |
393 | } |
394 | } |
395 | |
396 | $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries ); |
397 | |
398 | if ( $user->getWikiId() === UserIdentity::LOCAL && $this->getRequest()->getCheck( 'wpWatch' ) ) { |
399 | $this->watchlistManager->addWatchIgnoringRights( |
400 | $this->getUser(), |
401 | Title::makeTitle( NS_USER, $user->getName() ) |
402 | ); |
403 | } |
404 | |
405 | return Status::newGood(); |
406 | } |
407 | |
408 | /** |
409 | * Save user groups changes in the database. This function does not throw errors; |
410 | * instead, it ignores groups that the performer does not have permission to set. |
411 | * |
412 | * This function can be used without submitting the special page |
413 | * |
414 | * @param UserIdentity $user |
415 | * @param string[] $add Array of groups to add |
416 | * @param string[] $remove Array of groups to remove |
417 | * @param string $reason Reason for group change |
418 | * @param string[] $tags Array of change tags to add to the log entry |
419 | * @param array<string,?string> $groupExpiries Associative array of (group name => expiry), |
420 | * containing only those groups that are to have new expiry values set |
421 | * @return array Tuple of added, then removed groups |
422 | */ |
423 | public function doSaveUserGroups( $user, array $add, array $remove, $reason = '', |
424 | array $tags = [], array $groupExpiries = [] |
425 | ) { |
426 | // Validate input set... |
427 | $isself = $user->getName() == $this->getUser()->getName(); |
428 | if ( $this->userGroupManager !== null ) { |
429 | // Used after form submit |
430 | $userGroupManager = $this->userGroupManager; |
431 | } else { |
432 | // Used as backend-function |
433 | $userGroupManager = $this->userGroupManagerFactory |
434 | ->getUserGroupManager( $user->getWikiId() ); |
435 | } |
436 | $groups = $userGroupManager->getUserGroups( $user ); |
437 | $ugms = $userGroupManager->getUserGroupMemberships( $user ); |
438 | $changeable = $userGroupManager->getGroupsChangeableBy( $this->getAuthority() ); |
439 | $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] ); |
440 | $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] ); |
441 | |
442 | $remove = array_unique( array_intersect( $remove, $removable, $groups ) ); |
443 | $add = array_intersect( $add, $addable ); |
444 | |
445 | // add only groups that are not already present or that need their expiry updated, |
446 | // UNLESS the user can only add this group (not remove it) and the expiry time |
447 | // is being brought forward (T156784) |
448 | $add = array_filter( $add, |
449 | static function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) { |
450 | if ( isset( $groupExpiries[$group] ) && |
451 | !in_array( $group, $removable ) && |
452 | isset( $ugms[$group] ) && |
453 | ( $ugms[$group]->getExpiry() ?: 'infinity' ) > |
454 | ( $groupExpiries[$group] ?: 'infinity' ) |
455 | ) { |
456 | return false; |
457 | } |
458 | return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries ); |
459 | } ); |
460 | |
461 | if ( $user->getWikiId() === UserIdentity::LOCAL ) { |
462 | // For compatibility local changes are provided as User object to the hook |
463 | $hookUser = $this->userFactory->newFromUserIdentity( $user ); |
464 | } else { |
465 | // Interwiki changes are provided as UserIdentity since 1.41, was UserRightsProxy before |
466 | $hookUser = $user; |
467 | } |
468 | $this->getHookRunner()->onChangeUserGroups( $this->getUser(), $hookUser, $add, $remove ); |
469 | |
470 | $oldGroups = $groups; |
471 | $oldUGMs = $userGroupManager->getUserGroupMemberships( $user ); |
472 | $newGroups = $oldGroups; |
473 | |
474 | // Remove groups, then add new ones/update expiries of existing ones |
475 | if ( $remove ) { |
476 | foreach ( $remove as $index => $group ) { |
477 | if ( !$userGroupManager->removeUserFromGroup( $user, $group ) ) { |
478 | unset( $remove[$index] ); |
479 | } |
480 | } |
481 | $newGroups = array_diff( $newGroups, $remove ); |
482 | } |
483 | if ( $add ) { |
484 | foreach ( $add as $index => $group ) { |
485 | $expiry = $groupExpiries[$group] ?? null; |
486 | if ( !$userGroupManager->addUserToGroup( $user, $group, $expiry, true ) ) { |
487 | unset( $add[$index] ); |
488 | } |
489 | } |
490 | $newGroups = array_merge( $newGroups, $add ); |
491 | } |
492 | $newGroups = array_unique( $newGroups ); |
493 | $newUGMs = $userGroupManager->getUserGroupMemberships( $user ); |
494 | |
495 | // Ensure that caches are cleared |
496 | $this->userFactory->invalidateCache( $user ); |
497 | |
498 | // update groups in external authentication database |
499 | $this->getHookRunner()->onUserGroupsChanged( $hookUser, $add, $remove, |
500 | $this->getUser(), $reason, $oldUGMs, $newUGMs ); |
501 | |
502 | wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) ); |
503 | wfDebug( 'newGroups: ' . print_r( $newGroups, true ) ); |
504 | wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) ); |
505 | wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) ); |
506 | |
507 | // Only add a log entry if something actually changed |
508 | if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) { |
509 | $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs ); |
510 | } |
511 | |
512 | return [ $add, $remove ]; |
513 | } |
514 | |
515 | /** |
516 | * Serialise a UserGroupMembership object for storage in the log_params section |
517 | * of the logging table. Only keeps essential data, removing redundant fields. |
518 | * |
519 | * @param UserGroupMembership|null $ugm May be null if things get borked |
520 | * @return array|null |
521 | */ |
522 | protected static function serialiseUgmForLog( $ugm ) { |
523 | if ( !$ugm instanceof UserGroupMembership ) { |
524 | return null; |
525 | } |
526 | return [ 'expiry' => $ugm->getExpiry() ]; |
527 | } |
528 | |
529 | /** |
530 | * Add a rights log entry for an action. |
531 | * @param UserIdentity $user |
532 | * @param array $oldGroups |
533 | * @param array $newGroups |
534 | * @param string $reason |
535 | * @param string[] $tags Change tags for the log entry |
536 | * @param array $oldUGMs Associative array of (group name => UserGroupMembership) |
537 | * @param array $newUGMs Associative array of (group name => UserGroupMembership) |
538 | */ |
539 | protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason, |
540 | array $tags, array $oldUGMs, array $newUGMs |
541 | ) { |
542 | // make sure $oldUGMs and $newUGMs are in the same order, and serialise |
543 | // each UGM object to a simplified array |
544 | $oldUGMs = array_map( static function ( $group ) use ( $oldUGMs ) { |
545 | return isset( $oldUGMs[$group] ) ? |
546 | self::serialiseUgmForLog( $oldUGMs[$group] ) : |
547 | null; |
548 | }, $oldGroups ); |
549 | $newUGMs = array_map( static function ( $group ) use ( $newUGMs ) { |
550 | return isset( $newUGMs[$group] ) ? |
551 | self::serialiseUgmForLog( $newUGMs[$group] ) : |
552 | null; |
553 | }, $newGroups ); |
554 | |
555 | $logEntry = new ManualLogEntry( 'rights', 'rights' ); |
556 | $logEntry->setPerformer( $this->getUser() ); |
557 | $logEntry->setTarget( Title::makeTitle( NS_USER, $this->getDisplayUsername( $user ) ) ); |
558 | $logEntry->setComment( is_string( $reason ) ? $reason : "" ); |
559 | $logEntry->setParameters( [ |
560 | '4::oldgroups' => $oldGroups, |
561 | '5::newgroups' => $newGroups, |
562 | 'oldmetadata' => $oldUGMs, |
563 | 'newmetadata' => $newUGMs, |
564 | ] ); |
565 | $logid = $logEntry->insert(); |
566 | if ( count( $tags ) ) { |
567 | $logEntry->addTags( $tags ); |
568 | } |
569 | $logEntry->publish( $logid ); |
570 | } |
571 | |
572 | /** |
573 | * Edit user groups membership |
574 | * @param string $username Name of the user. |
575 | */ |
576 | private function editUserGroupsForm( $username ) { |
577 | $status = $this->fetchUser( $username, true ); |
578 | if ( !$status->isOK() ) { |
579 | $this->getOutput()->addWikiTextAsInterface( |
580 | $status->getWikiText( false, false, $this->getLanguage() ) |
581 | ); |
582 | |
583 | return; |
584 | } |
585 | |
586 | /** @var UserIdentity $user */ |
587 | $user = $status->value; |
588 | '@phan-var UserIdentity $user'; |
589 | |
590 | $groups = $this->userGroupManager->getUserGroups( $user ); |
591 | $groupMemberships = $this->userGroupManager->getUserGroupMemberships( $user ); |
592 | $this->showEditUserGroupsForm( $user, $groups, $groupMemberships ); |
593 | |
594 | // This isn't really ideal logging behavior, but let's not hide the |
595 | // interwiki logs if we're using them as is. |
596 | $this->showLogFragment( $user, $this->getOutput() ); |
597 | } |
598 | |
599 | /** |
600 | * Normalize the input username, which may be local or remote, and |
601 | * return a user identity object, use it on other services for manipulating rights |
602 | * |
603 | * Side effects: error output for invalid access |
604 | * @param string $username |
605 | * @param bool $writing |
606 | * @return Status |
607 | */ |
608 | public function fetchUser( $username, $writing = true ) { |
609 | $parts = explode( $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter ), |
610 | $username ); |
611 | if ( count( $parts ) < 2 ) { |
612 | $name = trim( $username ); |
613 | $wikiId = UserIdentity::LOCAL; |
614 | } else { |
615 | [ $name, $wikiId ] = array_map( 'trim', $parts ); |
616 | |
617 | if ( WikiMap::isCurrentWikiId( $wikiId ) ) { |
618 | $wikiId = UserIdentity::LOCAL; |
619 | } else { |
620 | if ( $writing && |
621 | !$this->getAuthority()->isAllowed( 'userrights-interwiki' ) |
622 | ) { |
623 | return Status::newFatal( 'userrights-no-interwiki' ); |
624 | } |
625 | $localDatabases = $this->getConfig()->get( MainConfigNames::LocalDatabases ); |
626 | if ( !in_array( $wikiId, $localDatabases ) ) { |
627 | return Status::newFatal( 'userrights-nodatabase', $wikiId ); |
628 | } |
629 | } |
630 | } |
631 | |
632 | if ( $name === '' ) { |
633 | return Status::newFatal( 'nouserspecified' ); |
634 | } |
635 | |
636 | $userIdentityLookup = $this->actorStoreFactory->getUserIdentityLookup( $wikiId ); |
637 | if ( $name[0] == '#' ) { |
638 | // Numeric ID can be specified... |
639 | $id = intval( substr( $name, 1 ) ); |
640 | |
641 | $user = $userIdentityLookup->getUserIdentityByUserId( $id ); |
642 | if ( !$user ) { |
643 | // Different error message for compatibility |
644 | return Status::newFatal( 'noname' ); |
645 | } |
646 | $name = $user->getName(); |
647 | } else { |
648 | $name = $this->userNameUtils->getCanonical( $name ); |
649 | if ( $name === false ) { |
650 | // invalid name |
651 | return Status::newFatal( 'nosuchusershort', $username ); |
652 | } |
653 | $user = $userIdentityLookup->getUserIdentityByName( $name ); |
654 | } |
655 | |
656 | if ( $this->userNameUtils->isTemp( $name ) ) { |
657 | return Status::newFatal( 'userrights-no-group' ); |
658 | } |
659 | |
660 | if ( !$user || !$user->isRegistered() ) { |
661 | return Status::newFatal( 'nosuchusershort', $username ); |
662 | } |
663 | |
664 | if ( $user->getWikiId() === UserIdentity::LOCAL && |
665 | $this->userFactory->newFromUserIdentity( $user )->isHidden() && |
666 | !$this->getAuthority()->isAllowed( 'hideuser' ) |
667 | ) { |
668 | // Cannot see hidden users, pretend they don't exist |
669 | return Status::newFatal( 'nosuchusershort', $username ); |
670 | } |
671 | |
672 | return Status::newGood( $user ); |
673 | } |
674 | |
675 | /** |
676 | * @since 1.15 |
677 | * |
678 | * @param array $ids |
679 | * |
680 | * @return string |
681 | */ |
682 | public function makeGroupNameList( $ids ) { |
683 | if ( !$ids ) { |
684 | return $this->msg( 'rightsnone' )->inContentLanguage()->text(); |
685 | } else { |
686 | return implode( ', ', $ids ); |
687 | } |
688 | } |
689 | |
690 | /** |
691 | * Output a form to allow searching for a user |
692 | */ |
693 | protected function switchForm() { |
694 | $this->getOutput()->addModules( 'mediawiki.userSuggest' ); |
695 | |
696 | $this->getOutput()->addHTML( |
697 | Html::openElement( |
698 | 'form', |
699 | [ |
700 | 'method' => 'get', |
701 | 'action' => wfScript(), |
702 | 'name' => 'uluser', |
703 | 'id' => 'mw-userrights-form1' |
704 | ] |
705 | ) . |
706 | Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . |
707 | Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) . |
708 | Xml::inputLabel( |
709 | $this->msg( 'userrights-user-editname' )->text(), |
710 | 'user', |
711 | 'username', |
712 | 30, |
713 | $this->mTarget !== null ? str_replace( '_', ' ', $this->mTarget ) : '', |
714 | [ |
715 | 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest |
716 | ] + ( |
717 | // Set autofocus on blank input and error input |
718 | $this->mFetchedUser === null ? [ 'autofocus' => '' ] : [] |
719 | ) |
720 | ) . ' ' . |
721 | Xml::submitButton( |
722 | $this->msg( 'editusergroup' )->text() |
723 | ) . |
724 | Html::closeElement( 'fieldset' ) . |
725 | Html::closeElement( 'form' ) . "\n" |
726 | ); |
727 | } |
728 | |
729 | /** |
730 | * Show the form to edit group memberships. |
731 | * |
732 | * @param UserIdentity $user |
733 | * @param string[] $groups Array of groups the user is in. Not used by this implementation |
734 | * anymore, but kept for backward compatibility with subclasses |
735 | * @param UserGroupMembership[] $groupMemberships Associative array of (group name => UserGroupMembership |
736 | * object) containing the groups the user is in |
737 | */ |
738 | protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) { |
739 | $list = $membersList = $tempList = $tempMembersList = []; |
740 | foreach ( $groupMemberships as $ugm ) { |
741 | $linkG = UserGroupMembership::getLinkHTML( $ugm, $this->getContext() ); |
742 | $linkM = UserGroupMembership::getLinkHTML( $ugm, $this->getContext(), $user->getName() ); |
743 | if ( $ugm->getExpiry() ) { |
744 | $tempList[] = $linkG; |
745 | $tempMembersList[] = $linkM; |
746 | } else { |
747 | $list[] = $linkG; |
748 | $membersList[] = $linkM; |
749 | |
750 | } |
751 | } |
752 | |
753 | $autoList = []; |
754 | $autoMembersList = []; |
755 | |
756 | if ( $user->getWikiId() === UserIdentity::LOCAL ) { |
757 | // Listing autopromote groups works only on the local wiki |
758 | foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) { |
759 | $autoList[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() ); |
760 | $autoMembersList[] = UserGroupMembership::getLinkHTML( $group, $this->getContext(), $user->getName() ); |
761 | } |
762 | } |
763 | |
764 | $language = $this->getLanguage(); |
765 | $displayedList = $this->msg( 'userrights-groupsmember-type' ) |
766 | ->rawParams( |
767 | $language->commaList( array_merge( $tempList, $list ) ), |
768 | $language->commaList( array_merge( $tempMembersList, $membersList ) ) |
769 | )->escaped(); |
770 | $displayedAutolist = $this->msg( 'userrights-groupsmember-type' ) |
771 | ->rawParams( |
772 | $language->commaList( $autoList ), |
773 | $language->commaList( $autoMembersList ) |
774 | )->escaped(); |
775 | |
776 | $grouplist = ''; |
777 | $count = count( $list ) + count( $tempList ); |
778 | if ( $count > 0 ) { |
779 | $grouplist = $this->msg( 'userrights-groupsmember' ) |
780 | ->numParams( $count ) |
781 | ->params( $user->getName() ) |
782 | ->parse(); |
783 | $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n"; |
784 | } |
785 | |
786 | $count = count( $autoList ); |
787 | if ( $count > 0 ) { |
788 | $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' ) |
789 | ->numParams( $count ) |
790 | ->params( $user->getName() ) |
791 | ->parse(); |
792 | $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n"; |
793 | } |
794 | |
795 | $systemUser = $user->getWikiId() === UserIdentity::LOCAL |
796 | && $this->userFactory->newFromUserIdentity( $user )->isSystemUser(); |
797 | if ( $systemUser ) { |
798 | $systemusernote = $this->msg( 'userrights-systemuser' ) |
799 | ->params( $user->getName() ) |
800 | ->parse(); |
801 | $grouplist .= '<p>' . $systemusernote . "</p>\n"; |
802 | } |
803 | |
804 | // Only add an email link if the user is not a system user |
805 | $flags = $systemUser ? 0 : Linker::TOOL_LINKS_EMAIL; |
806 | $userToolLinks = Linker::userToolLinks( |
807 | $user->getId( $user->getWikiId() ), |
808 | $this->getDisplayUsername( $user ), |
809 | false, /* default for redContribsWhenNoEdits */ |
810 | $flags |
811 | ); |
812 | |
813 | [ $groupCheckboxes, $canChangeAny ] = |
814 | $this->groupCheckboxes( $groupMemberships, $user ); |
815 | $this->getOutput()->addHTML( |
816 | Html::openElement( |
817 | 'form', |
818 | [ |
819 | 'method' => 'post', |
820 | 'action' => $this->getPageTitle()->getLocalURL(), |
821 | 'name' => 'editGroup', |
822 | 'id' => 'mw-userrights-form2' |
823 | ] |
824 | ) . |
825 | Html::hidden( 'user', $this->mTarget ) . |
826 | Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) . |
827 | Html::hidden( |
828 | 'conflictcheck-originalgroups', |
829 | implode( ',', $this->userGroupManager->getUserGroups( $user ) ) |
830 | ) . // Conflict detection |
831 | Html::openElement( 'fieldset' ) . |
832 | Html::element( |
833 | 'legend', |
834 | [], |
835 | $this->msg( |
836 | $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup', |
837 | $user->getName() |
838 | )->text() |
839 | ) . |
840 | $this->msg( |
841 | $canChangeAny ? 'editinguser' : 'viewinguserrights' |
842 | )->params( wfEscapeWikiText( $this->getDisplayUsername( $user ) ) ) |
843 | ->rawParams( $userToolLinks )->parse() |
844 | ); |
845 | if ( $canChangeAny ) { |
846 | $this->getOutput()->addHTML( |
847 | $this->msg( 'userrights-groups-help', $user->getName() )->parse() . |
848 | $grouplist . |
849 | $groupCheckboxes . |
850 | Html::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) . |
851 | "<tr> |
852 | <td class='mw-label'>" . |
853 | Html::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) . |
854 | "</td> |
855 | <td class='mw-input'>" . |
856 | Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason' ) ?? false, [ |
857 | 'id' => 'wpReason', |
858 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
859 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
860 | // Unicode codepoints. |
861 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
862 | ] ) . |
863 | "</td> |
864 | </tr> |
865 | <tr> |
866 | <td></td> |
867 | <td class='mw-submit'>" . |
868 | Html::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(), |
869 | [ 'name' => 'saveusergroups' ] + |
870 | Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) |
871 | ) . |
872 | "</td> |
873 | </tr> |
874 | <tr> |
875 | <td></td> |
876 | <td class='mw-input'>" . |
877 | Html::check( 'wpWatch', false, [ 'id' => 'wpWatch' ] ) . |
878 | ' ' . Html::label( $this->msg( 'userrights-watchuser' )->text(), 'wpWatch' ) . |
879 | "</td> |
880 | </tr>" . |
881 | Xml::closeElement( 'table' ) . "\n" |
882 | ); |
883 | } else { |
884 | $this->getOutput()->addHTML( $grouplist ); |
885 | } |
886 | $this->getOutput()->addHTML( |
887 | Xml::closeElement( 'fieldset' ) . |
888 | Xml::closeElement( 'form' ) . "\n" |
889 | ); |
890 | } |
891 | |
892 | /** |
893 | * Adds a table with checkboxes where you can select what groups to add/remove |
894 | * |
895 | * @param UserGroupMembership[] $usergroups Associative array of (group name as string => |
896 | * UserGroupMembership object) for groups the user belongs to |
897 | * @param UserIdentity $user |
898 | * @return array Array with 2 elements: the XHTML table element with checkxboes, and |
899 | * whether any groups are changeable |
900 | */ |
901 | private function groupCheckboxes( $usergroups, $user ) { |
902 | $allgroups = $this->userGroupManager->listAllGroups(); |
903 | $ret = ''; |
904 | |
905 | // Get the list of preset expiry times from the system message |
906 | $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage(); |
907 | $expiryOptions = $expiryOptionsMsg->isDisabled() |
908 | ? [] |
909 | : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() ); |
910 | |
911 | // Put all column info into an associative array so that extensions can |
912 | // more easily manage it. |
913 | $columns = [ 'unchangeable' => [], 'changeable' => [] ]; |
914 | |
915 | foreach ( $allgroups as $group ) { |
916 | $set = isset( $usergroups[$group] ); |
917 | // Users who can add the group, but not remove it, can only lengthen |
918 | // expiries, not shorten them. So they should only see the expiry |
919 | // dropdown if the group currently has a finite expiry |
920 | $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) && |
921 | !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() ); |
922 | // Should the checkbox be disabled? |
923 | $disabledCheckbox = !( |
924 | ( $set && $this->canRemove( $group ) ) || |
925 | ( !$set && $this->canAdd( $group ) ) ); |
926 | // Should the expiry elements be disabled? |
927 | $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry; |
928 | // Do we need to point out that this action is irreversible? |
929 | $irreversible = !$disabledCheckbox && ( |
930 | ( $set && !$this->canAdd( $group ) ) || |
931 | ( !$set && !$this->canRemove( $group ) ) ); |
932 | |
933 | $checkbox = [ |
934 | 'set' => $set, |
935 | 'disabled' => $disabledCheckbox, |
936 | 'disabled-expiry' => $disabledExpiry, |
937 | 'irreversible' => $irreversible |
938 | ]; |
939 | |
940 | if ( $disabledCheckbox && $disabledExpiry ) { |
941 | $columns['unchangeable'][$group] = $checkbox; |
942 | } else { |
943 | $columns['changeable'][$group] = $checkbox; |
944 | } |
945 | } |
946 | |
947 | // Build the HTML table |
948 | $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) . |
949 | "<tr>\n"; |
950 | foreach ( $columns as $name => $column ) { |
951 | if ( $column === [] ) { |
952 | continue; |
953 | } |
954 | // Messages: userrights-changeable-col, userrights-unchangeable-col |
955 | $ret .= Xml::element( |
956 | 'th', |
957 | null, |
958 | $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text() |
959 | ); |
960 | } |
961 | |
962 | $ret .= "</tr>\n<tr>\n"; |
963 | $uiLanguage = $this->getLanguage(); |
964 | $userName = $user->getName(); |
965 | foreach ( $columns as $column ) { |
966 | if ( $column === [] ) { |
967 | continue; |
968 | } |
969 | $ret .= "\t<td style='vertical-align:top;'>\n"; |
970 | foreach ( $column as $group => $checkbox ) { |
971 | $member = $uiLanguage->getGroupMemberName( $group, $userName ); |
972 | if ( $checkbox['irreversible'] ) { |
973 | $text = $this->msg( 'userrights-irreversible-marker', $member )->text(); |
974 | } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) { |
975 | $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text(); |
976 | } else { |
977 | $text = $member; |
978 | } |
979 | $checkboxHtml = Html::element( 'input', [ |
980 | 'type' => 'checkbox', 'name' => "wpGroup-$group", 'value' => '1', |
981 | 'id' => "wpGroup-$group", 'checked' => $checkbox['set'], |
982 | 'class' => 'mw-userrights-groupcheckbox', |
983 | 'disabled' => $checkbox['disabled'], |
984 | ] ) . ' ' . Html::label( $text, "wpGroup-$group" ); |
985 | |
986 | if ( $this->canProcessExpiries() ) { |
987 | $uiUser = $this->getUser(); |
988 | |
989 | $currentExpiry = isset( $usergroups[$group] ) ? |
990 | $usergroups[$group]->getExpiry() : |
991 | null; |
992 | |
993 | // If the user can't modify the expiry, print the current expiry below |
994 | // it in plain text. Otherwise provide UI to set/change the expiry |
995 | if ( $checkbox['set'] && |
996 | ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] ) |
997 | ) { |
998 | if ( $currentExpiry ) { |
999 | $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser ); |
1000 | $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser ); |
1001 | $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser ); |
1002 | $expiryHtml = Xml::element( 'span', null, |
1003 | $this->msg( 'userrights-expiry-current' )->params( |
1004 | $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() ); |
1005 | } else { |
1006 | $expiryHtml = Xml::element( 'span', null, |
1007 | $this->msg( 'userrights-expiry-none' )->text() ); |
1008 | } |
1009 | // T171345: Add a hidden form element so that other groups can still be manipulated, |
1010 | // otherwise saving errors out with an invalid expiry time for this group. |
1011 | $expiryHtml .= Html::hidden( "wpExpiry-$group", |
1012 | $currentExpiry ? 'existing' : 'infinite' ); |
1013 | $expiryHtml .= "<br />\n"; |
1014 | } else { |
1015 | $expiryHtml = Html::element( 'span', [], |
1016 | $this->msg( 'userrights-expiry' )->text() ); |
1017 | $expiryHtml .= Html::openElement( 'span' ); |
1018 | |
1019 | // add a form element to set the expiry date |
1020 | $expiryFormOptions = new XmlSelect( |
1021 | "wpExpiry-$group", |
1022 | "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm |
1023 | $currentExpiry ? 'existing' : 'infinite' |
1024 | ); |
1025 | if ( $checkbox['disabled-expiry'] ) { |
1026 | $expiryFormOptions->setAttribute( 'disabled', 'disabled' ); |
1027 | } |
1028 | |
1029 | if ( $currentExpiry ) { |
1030 | $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser ); |
1031 | $d = $uiLanguage->userDate( $currentExpiry, $uiUser ); |
1032 | $t = $uiLanguage->userTime( $currentExpiry, $uiUser ); |
1033 | $existingExpiryMessage = $this->msg( 'userrights-expiry-existing', |
1034 | $timestamp, $d, $t ); |
1035 | $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' ); |
1036 | } |
1037 | |
1038 | $expiryFormOptions->addOption( |
1039 | $this->msg( 'userrights-expiry-none' )->text(), |
1040 | 'infinite' |
1041 | ); |
1042 | $expiryFormOptions->addOption( |
1043 | $this->msg( 'userrights-expiry-othertime' )->text(), |
1044 | 'other' |
1045 | ); |
1046 | |
1047 | $expiryFormOptions->addOptions( $expiryOptions ); |
1048 | |
1049 | // Add expiry dropdown |
1050 | $expiryHtml .= $expiryFormOptions->getHTML() . '<br />'; |
1051 | |
1052 | // Add custom expiry field |
1053 | $expiryHtml .= Html::element( 'input', [ |
1054 | 'name' => "wpExpiry-$group-other", 'size' => 30, 'value' => '', |
1055 | 'id' => "mw-input-wpExpiry-$group-other", |
1056 | 'class' => 'mw-userrights-expiryfield', |
1057 | 'disabled' => $checkbox['disabled-expiry'], |
1058 | ] ); |
1059 | |
1060 | // If the user group is set but the checkbox is disabled, mimic a |
1061 | // checked checkbox in the form submission |
1062 | if ( $checkbox['set'] && $checkbox['disabled'] ) { |
1063 | $expiryHtml .= Html::hidden( "wpGroup-$group", 1 ); |
1064 | } |
1065 | |
1066 | $expiryHtml .= Html::closeElement( 'span' ); |
1067 | } |
1068 | |
1069 | $divAttribs = [ |
1070 | 'id' => "mw-userrights-nested-wpGroup-$group", |
1071 | 'class' => 'mw-userrights-nested', |
1072 | ]; |
1073 | $checkboxHtml .= "\t\t\t" . Html::rawElement( 'div', $divAttribs, $expiryHtml ) . "\n"; |
1074 | } |
1075 | $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] ) |
1076 | ? Html::rawElement( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml ) |
1077 | : Html::rawElement( 'div', [], $checkboxHtml ) |
1078 | ) . "\n"; |
1079 | } |
1080 | $ret .= "\t</td>\n"; |
1081 | } |
1082 | $ret .= Html::closeElement( 'tr' ) . Html::closeElement( 'table' ); |
1083 | |
1084 | return [ $ret, (bool)$columns['changeable'] ]; |
1085 | } |
1086 | |
1087 | /** |
1088 | * @param string $group The name of the group to check |
1089 | * @return bool Can we remove the group? |
1090 | */ |
1091 | private function canRemove( $group ) { |
1092 | $groups = $this->changeableGroups(); |
1093 | |
1094 | return in_array( |
1095 | $group, |
1096 | $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] ) |
1097 | ); |
1098 | } |
1099 | |
1100 | /** |
1101 | * @param string $group The name of the group to check |
1102 | * @return bool Can we add the group? |
1103 | */ |
1104 | private function canAdd( $group ) { |
1105 | $groups = $this->changeableGroups(); |
1106 | |
1107 | return in_array( |
1108 | $group, |
1109 | $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] ) |
1110 | ); |
1111 | } |
1112 | |
1113 | /** |
1114 | * @return array [ |
1115 | * 'add' => [ addablegroups ], |
1116 | * 'remove' => [ removablegroups ], |
1117 | * 'add-self' => [ addablegroups to self ], |
1118 | * 'remove-self' => [ removable groups from self ] |
1119 | * ] |
1120 | * @phan-return array{add:list<string>,remove:list<string>,add-self:list<string>,remove-self:list<string>} |
1121 | */ |
1122 | protected function changeableGroups() { |
1123 | return $this->userGroupManager->getGroupsChangeableBy( $this->getContext()->getAuthority() ); |
1124 | } |
1125 | |
1126 | /** |
1127 | * Get a display user name. This includes the {@}domain part for interwiki users. |
1128 | * Use UserIdentity::getName for {{GENDER:}} in messages and |
1129 | * use the "display user name" for visible user names in logs or messages |
1130 | * |
1131 | * @param UserIdentity $user |
1132 | * @return string |
1133 | */ |
1134 | private function getDisplayUsername( UserIdentity $user ) { |
1135 | $userName = $user->getName(); |
1136 | if ( $user->getWikiId() !== UserIdentity::LOCAL ) { |
1137 | $userName .= $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter ) |
1138 | . $user->getWikiId(); |
1139 | } |
1140 | return $userName; |
1141 | } |
1142 | |
1143 | /** |
1144 | * Show a rights log fragment for the specified user |
1145 | * |
1146 | * @param UserIdentity $user User to show log for |
1147 | * @param OutputPage $output OutputPage to use |
1148 | */ |
1149 | protected function showLogFragment( $user, $output ) { |
1150 | $rightsLogPage = new LogPage( 'rights' ); |
1151 | $output->addHTML( Html::element( 'h2', [], $rightsLogPage->getName()->text() ) ); |
1152 | LogEventsList::showLogExtract( $output, 'rights', |
1153 | Title::makeTitle( NS_USER, $this->getDisplayUsername( $user ) ) ); |
1154 | } |
1155 | |
1156 | /** |
1157 | * Return an array of subpages beginning with $search that this special page will accept. |
1158 | * |
1159 | * @param string $search Prefix to search for |
1160 | * @param int $limit Maximum number of results to return (usually 10) |
1161 | * @param int $offset Number of results to skip (usually 0) |
1162 | * @return string[] Matching subpages |
1163 | */ |
1164 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
1165 | $search = $this->userNameUtils->getCanonical( $search ); |
1166 | if ( !$search ) { |
1167 | // No prefix suggestion for invalid user |
1168 | return []; |
1169 | } |
1170 | // Autocomplete subpage as user list - public to allow caching |
1171 | return $this->userNamePrefixSearch |
1172 | ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset ); |
1173 | } |
1174 | |
1175 | protected function getGroupName() { |
1176 | return 'users'; |
1177 | } |
1178 | } |
1179 | |
1180 | /** |
1181 | * Retain the old class name for backwards compatibility. |
1182 | * @deprecated since 1.40 |
1183 | */ |
1184 | class_alias( SpecialUserRights::class, 'UserrightsPage' ); |