Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.67% |
327 / 373 |
|
50.00% |
8 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialGlobalGroupMembership | |
87.67% |
327 / 373 |
|
50.00% |
8 / 16 |
80.46 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
56.52% |
39 / 69 |
|
0.00% |
0 / 1 |
40.75 | |||
getSuccessURL | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
saveUserGroups | |
86.36% |
19 / 22 |
|
0.00% |
0 / 1 |
8.16 | |||
doSaveUserGroups | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
7 | |||
addLogEntry | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
3 | |||
editUserGroupsForm | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
2.35 | |||
fetchUser | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
12.02 | |||
switchForm | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
1 | |||
showEditUserGroupsForm | |
100.00% |
91 / 91 |
|
100.00% |
1 / 1 |
7 | |||
groupCheckboxes | |
98.55% |
68 / 69 |
|
0.00% |
0 / 1 |
5 | |||
changeableGroups | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
showLogFragment | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
prefixSearchSubpages | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Extension\CentralAuth\Special; |
22 | |
23 | use LogEventsList; |
24 | use LogPage; |
25 | use ManualLogEntry; |
26 | use MediaWiki\CommentStore\CommentStore; |
27 | use MediaWiki\Extension\CentralAuth\GlobalGroup\GlobalGroupLookup; |
28 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
29 | use MediaWiki\Extension\CentralAuth\Widget\HTMLGlobalUserTextField; |
30 | use MediaWiki\Html\Html; |
31 | use MediaWiki\HTMLForm\HTMLForm; |
32 | use MediaWiki\Linker\Linker; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\Output\OutputPage; |
35 | use MediaWiki\SpecialPage\SpecialPage; |
36 | use MediaWiki\Specials\SpecialUserRights; |
37 | use MediaWiki\Status\Status; |
38 | use MediaWiki\Title\TitleFactory; |
39 | use MediaWiki\User\UserGroupMembership; |
40 | use MediaWiki\User\UserNamePrefixSearch; |
41 | use MediaWiki\User\UserNameUtils; |
42 | use MediaWiki\Xml\Xml; |
43 | use MediaWiki\Xml\XmlSelect; |
44 | use PermissionsError; |
45 | use UserBlockedError; |
46 | |
47 | /** |
48 | * Equivalent of Special:Userrights for global groups. |
49 | * |
50 | * @ingroup Extensions |
51 | */ |
52 | class SpecialGlobalGroupMembership extends SpecialPage { |
53 | |
54 | /** |
55 | * The target of the local right-adjuster's interest. Can be gotten from |
56 | * either a GET parameter or a subpage-style parameter, so have a member |
57 | * variable for it. |
58 | * @var null|string |
59 | */ |
60 | protected $mTarget; |
61 | |
62 | /** |
63 | * @var null|CentralAuthUser The user object of the target username or null. |
64 | */ |
65 | protected $mFetchedUser = null; |
66 | |
67 | private TitleFactory $titleFactory; |
68 | private UserNamePrefixSearch $userNamePrefixSearch; |
69 | private UserNameUtils $userNameUtils; |
70 | private GlobalGroupLookup $globalGroupLookup; |
71 | |
72 | public function __construct( |
73 | TitleFactory $titleFactory, |
74 | UserNamePrefixSearch $userNamePrefixSearch, |
75 | UserNameUtils $userNameUtils, |
76 | GlobalGroupLookup $globalGroupLookup |
77 | ) { |
78 | parent::__construct( 'GlobalGroupMembership' ); |
79 | $this->titleFactory = $titleFactory; |
80 | $this->userNamePrefixSearch = $userNamePrefixSearch; |
81 | $this->userNameUtils = $userNameUtils; |
82 | $this->globalGroupLookup = $globalGroupLookup; |
83 | } |
84 | |
85 | /** |
86 | * @inheritDoc |
87 | */ |
88 | public function doesWrites() { |
89 | return true; |
90 | } |
91 | |
92 | /** |
93 | * Manage forms to be shown according to posted data. |
94 | * Depending on the submit button used, call a form or a save function. |
95 | * |
96 | * @param string|null $par String if any subpage provided, else null |
97 | * @throws UserBlockedError|PermissionsError |
98 | */ |
99 | public function execute( $par ) { |
100 | $user = $this->getUser(); |
101 | $request = $this->getRequest(); |
102 | $session = $request->getSession(); |
103 | $out = $this->getOutput(); |
104 | |
105 | $out->addModules( [ 'mediawiki.special.userrights' ] ); |
106 | |
107 | $this->mTarget = $par ?? $request->getVal( 'user' ); |
108 | |
109 | $fetchedStatus = $this->mTarget === null ? Status::newFatal( 'nouserspecified' ) : |
110 | $this->fetchUser( $this->mTarget ); |
111 | if ( $fetchedStatus->isOK() ) { |
112 | $this->mFetchedUser = $fetchedStatus->value; |
113 | } |
114 | |
115 | // show a successbox, if the user rights was saved successfully |
116 | if ( |
117 | $session->get( 'specialUserrightsSaveSuccess' ) && |
118 | $this->mFetchedUser !== null |
119 | ) { |
120 | // Remove session data for the success message |
121 | $session->remove( 'specialUserrightsSaveSuccess' ); |
122 | |
123 | $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' ); |
124 | $out->addHTML( |
125 | Html::successBox( |
126 | Html::element( |
127 | 'p', |
128 | [], |
129 | $this->msg( 'savedrights', $this->mFetchedUser->getName() )->text() |
130 | ), |
131 | 'mw-notify-success' |
132 | ) |
133 | ); |
134 | } |
135 | |
136 | $this->setHeaders(); |
137 | $this->outputHeader(); |
138 | |
139 | $out->addModuleStyles( 'mediawiki.special' ); |
140 | $this->addHelpLink( 'Help:Assigning permissions' ); |
141 | |
142 | $this->switchForm(); |
143 | |
144 | if ( |
145 | $request->wasPosted() && |
146 | $request->getCheck( 'saveusergroups' ) && |
147 | $this->mTarget !== null && |
148 | $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget ) |
149 | ) { |
150 | /* |
151 | * If the user is blocked and they only have "partial" access |
152 | * (e.g. they don't have the userrights permission), then don't |
153 | * allow them to change any user rights. |
154 | */ |
155 | if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) { |
156 | $block = $user->getBlock(); |
157 | if ( $block && $block->isSitewide() ) { |
158 | throw new UserBlockedError( |
159 | $block, |
160 | $user, |
161 | $this->getLanguage(), |
162 | $request->getIP() |
163 | ); |
164 | } |
165 | } |
166 | |
167 | $this->checkReadOnly(); |
168 | |
169 | // save settings |
170 | if ( !$fetchedStatus->isOK() ) { |
171 | $this->getOutput()->addWikiTextAsInterface( |
172 | $fetchedStatus->getWikiText( false, false, $this->getLanguage() ) |
173 | ); |
174 | |
175 | return; |
176 | } |
177 | |
178 | $targetUser = $this->mFetchedUser; |
179 | |
180 | $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' ); |
181 | $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck ); |
182 | $userGroups = $targetUser->getGlobalGroups(); |
183 | |
184 | if ( $userGroups !== $conflictCheck ) { |
185 | $out->addHTML( Html::errorBox( |
186 | $this->msg( 'userrights-conflict' )->parse() |
187 | ) ); |
188 | } else { |
189 | $status = $this->saveUserGroups( |
190 | $targetUser, |
191 | $request->getVal( 'user-reason' ) |
192 | ); |
193 | |
194 | if ( $status->isOK() ) { |
195 | // Set session data for the success message |
196 | $session->set( 'specialUserrightsSaveSuccess', 1 ); |
197 | |
198 | $out->redirect( $this->getSuccessURL() ); |
199 | return; |
200 | } else { |
201 | // Print an error message and redisplay the form |
202 | $out->wrapWikiTextAsInterface( |
203 | 'error', $status->getWikiText( false, false, $this->getLanguage() ) |
204 | ); |
205 | } |
206 | } |
207 | } |
208 | |
209 | // show some more forms |
210 | if ( $this->mTarget !== null ) { |
211 | $this->editUserGroupsForm( $this->mTarget ); |
212 | } |
213 | } |
214 | |
215 | /** |
216 | * @return string |
217 | */ |
218 | private function getSuccessURL() { |
219 | return $this->getPageTitle( $this->mTarget )->getFullURL(); |
220 | } |
221 | |
222 | /** |
223 | * Save user groups changes in the database. |
224 | * Data comes from the editUserGroupsForm() form function |
225 | * |
226 | * @param CentralAuthUser $user Target user object. |
227 | * @param string $reason Reason for group change |
228 | * @return Status |
229 | */ |
230 | private function saveUserGroups( CentralAuthUser $user, string $reason ): Status { |
231 | $allgroups = $this->globalGroupLookup->getDefinedGroups(); |
232 | $addgroup = []; |
233 | // associative array of (group name => expiry) |
234 | $groupExpiries = []; |
235 | $removegroup = []; |
236 | $existingGroups = $user->getGlobalGroupsWithExpiration(); |
237 | |
238 | // This could possibly create a highly unlikely race condition if permissions are changed between |
239 | // when the form is loaded and when the form is saved. Ignoring it for the moment. |
240 | foreach ( $allgroups as $group ) { |
241 | // We'll tell it to remove all unchecked groups, and add all checked groups. |
242 | // Later on, this gets filtered for what can actually be removed |
243 | if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) { |
244 | $addgroup[] = $group; |
245 | |
246 | // read the expiry information from the request |
247 | $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" ); |
248 | if ( $expiryDropdown === 'existing' ) { |
249 | continue; |
250 | } |
251 | |
252 | if ( $expiryDropdown === 'other' ) { |
253 | $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" ); |
254 | } else { |
255 | $expiryValue = $expiryDropdown; |
256 | } |
257 | |
258 | // validate the expiry |
259 | $groupExpiries[$group] = SpecialUserRights::expiryToTimestamp( $expiryValue ); |
260 | |
261 | if ( $groupExpiries[$group] === false ) { |
262 | return Status::newFatal( 'userrights-invalid-expiry', $group ); |
263 | } |
264 | |
265 | // not allowed to have things expiring in the past |
266 | if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) { |
267 | return Status::newFatal( 'userrights-expiry-in-past', $group ); |
268 | } |
269 | } else { |
270 | $removegroup[] = $group; |
271 | } |
272 | } |
273 | |
274 | $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries ); |
275 | |
276 | return Status::newGood(); |
277 | } |
278 | |
279 | /** |
280 | * Save user groups changes in the database. This function does not throw errors; |
281 | * instead, it ignores groups that the performer does not have permission to set. |
282 | * |
283 | * @param CentralAuthUser $user |
284 | * @param string[] $add Array of groups to add |
285 | * @param string[] $remove Array of groups to remove |
286 | * @param string $reason Reason for group change |
287 | * @param string[] $tags Array of change tags to add to the log entry |
288 | * @param array<string,?string> $groupExpiries Associative array of (group name => expiry), |
289 | * containing only those groups that are to have new expiry values set |
290 | * @return array Tuple of added, then removed groups |
291 | */ |
292 | public function doSaveUserGroups( |
293 | CentralAuthUser $user, |
294 | array $add, |
295 | array $remove, |
296 | string $reason = '', |
297 | array $tags = [], |
298 | array $groupExpiries = [] |
299 | ) { |
300 | // Validate input set... |
301 | $groups = $user->getGlobalGroupsWithExpiration(); |
302 | $changeable = $this->changeableGroups(); |
303 | |
304 | $remove = array_unique( array_intersect( $remove, $changeable, array_keys( $groups ) ) ); |
305 | $add = array_intersect( $add, $changeable ); |
306 | |
307 | // add only groups that are not already present or that need their expiry updated |
308 | $add = array_filter( $add, |
309 | static function ( $group ) use ( $groups, $groupExpiries ) { |
310 | return !array_key_exists( $group, $groups ) || array_key_exists( $group, $groupExpiries ); |
311 | } ); |
312 | |
313 | // Remove groups, then add new ones/update expiries of existing ones |
314 | if ( $remove ) { |
315 | foreach ( $remove as $group ) { |
316 | $user->removeFromGlobalGroups( $group ); |
317 | } |
318 | } |
319 | if ( $add ) { |
320 | foreach ( $add as $group ) { |
321 | $expiry = $groupExpiries[$group] ?? null; |
322 | $user->addToGlobalGroup( $group, $expiry ); |
323 | } |
324 | } |
325 | |
326 | $newGroups = $user->getGlobalGroupsWithExpiration(); |
327 | |
328 | // Ensure that caches are cleared |
329 | $user->invalidateCache(); |
330 | |
331 | // Only add a log entry if something actually changed |
332 | if ( $groups !== $newGroups ) { |
333 | $this->addLogEntry( |
334 | $user, |
335 | $groups, |
336 | $newGroups, |
337 | $reason, |
338 | $tags |
339 | ); |
340 | } |
341 | |
342 | return [ $add, $remove ]; |
343 | } |
344 | |
345 | /** |
346 | * @param CentralAuthUser $user |
347 | * @param array $oldGroups |
348 | * @param array $newGroups |
349 | * @param string $reason |
350 | * @param array $tags Not currently used |
351 | */ |
352 | private function addLogEntry( |
353 | CentralAuthUser $user, |
354 | array $oldGroups, |
355 | array $newGroups, |
356 | string $reason, |
357 | array $tags |
358 | ) { |
359 | $oldGroupNames = []; |
360 | $newGroupNames = []; |
361 | $oldGroupMetadata = []; |
362 | $newGroupMetadata = []; |
363 | |
364 | foreach ( $oldGroups as $key => &$value ) { |
365 | $oldGroupNames[] = $key; |
366 | $oldGroupMetadata[] = [ 'expiry' => $value ]; |
367 | } |
368 | |
369 | foreach ( $newGroups as $key => &$value ) { |
370 | $newGroupNames[] = $key; |
371 | $newGroupMetadata[] = [ 'expiry' => $value ]; |
372 | } |
373 | |
374 | $entry = new ManualLogEntry( 'gblrights', 'usergroups' ); |
375 | $entry->setTarget( $this->titleFactory->makeTitle( NS_USER, $user->getName() ) ); |
376 | $entry->setPerformer( $this->getUser() ); |
377 | $entry->setComment( $reason ); |
378 | $entry->setParameters( [ |
379 | 'oldGroups' => $oldGroupNames, |
380 | 'newGroups' => $newGroupNames, |
381 | 'oldMetadata' => $oldGroupMetadata, |
382 | 'newMetadata' => $newGroupMetadata, |
383 | ] ); |
384 | $logid = $entry->insert(); |
385 | $entry->publish( $logid ); |
386 | } |
387 | |
388 | /** |
389 | * Edit user groups membership |
390 | * @param string $username Name of the user. |
391 | */ |
392 | private function editUserGroupsForm( $username ) { |
393 | $status = $this->fetchUser( $username ); |
394 | if ( !$status->isOK() ) { |
395 | $this->getOutput()->addWikiTextAsInterface( |
396 | $status->getWikiText( false, false, $this->getLanguage() ) |
397 | ); |
398 | |
399 | return; |
400 | } |
401 | |
402 | /** @var CentralAuthUser $user */ |
403 | $user = $status->value; |
404 | '@phan-var CentralAuthUser $user'; |
405 | |
406 | $this->showEditUserGroupsForm( $user ); |
407 | |
408 | // This isn't really ideal logging behavior, but let's not hide the |
409 | // interwiki logs if we're using them as is. |
410 | $this->showLogFragment( $user, $this->getOutput() ); |
411 | } |
412 | |
413 | /** |
414 | * @param string $username |
415 | * @return Status |
416 | */ |
417 | public function fetchUser( $username ) { |
418 | if ( $username === '' ) { |
419 | return Status::newFatal( 'nouserspecified' ); |
420 | } |
421 | |
422 | if ( $username[0] == '#' ) { |
423 | $id = intval( substr( $username, 1 ) ); |
424 | $globalUser = CentralAuthUser::newPrimaryInstanceFromId( $id ); |
425 | // If the user exists, but is hidden from the viewer, pretend that it does |
426 | // not exist. - T285190/T260863 |
427 | if ( |
428 | !$globalUser |
429 | || ( |
430 | ( $globalUser->isSuppressed() || $globalUser->isHidden() ) |
431 | && !$this->getContext()->getAuthority()->isAllowed( 'centralauth-suppress' ) |
432 | ) |
433 | ) { |
434 | return Status::newFatal( 'noname', $id ); |
435 | } |
436 | } else { |
437 | // fetchUser() is public; normalize in case the caller forgot to. See T343963 and |
438 | // T344495. |
439 | $canonUsername = $this->userNameUtils->getCanonical( $username ); |
440 | if ( !is_string( $canonUsername ) ) { |
441 | // $username was invalid, return nosuchuser. |
442 | return Status::newFatal( 'nosuchusershort', $username ); |
443 | } |
444 | |
445 | // If the user exists, but is hidden from the viewer, pretend that it does |
446 | // not exist. - T285190 |
447 | $globalUser = CentralAuthUser::getPrimaryInstanceByName( $canonUsername ); |
448 | if ( |
449 | !$globalUser->exists() |
450 | || ( |
451 | ( $globalUser->isSuppressed() || $globalUser->isHidden() ) |
452 | && !$this->getContext()->getAuthority()->isAllowed( 'centralauth-suppress' ) |
453 | ) |
454 | ) { |
455 | return Status::newFatal( 'nosuchusershort', $canonUsername ); |
456 | } |
457 | } |
458 | |
459 | return Status::newGood( $globalUser ); |
460 | } |
461 | |
462 | /** |
463 | * Output a form to allow searching for a user |
464 | */ |
465 | private function switchForm() { |
466 | $this->addHelpLink( 'Extension:CentralAuth' ); |
467 | $this->getOutput()->addModuleStyles( 'mediawiki.special' ); |
468 | $formDescriptor = [ |
469 | 'user' => [ |
470 | 'class' => HTMLGlobalUserTextField::class, |
471 | 'name' => 'user', |
472 | 'id' => 'username', |
473 | 'label-message' => 'userrights-user-editname', |
474 | 'size' => 30, |
475 | 'default' => $this->mTarget, |
476 | ] |
477 | ]; |
478 | |
479 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); |
480 | $htmlForm |
481 | ->setMethod( 'get' ) |
482 | // Strip subpage |
483 | ->setTitle( $this->getPageTitle() ) |
484 | ->setAction( $this->getConfig()->get( MainConfigNames::Script ) ) |
485 | ->setId( 'mw-userrights-form1' ) |
486 | ->setName( 'uluser' ) |
487 | ->setSubmitTextMsg( 'editusergroup' ) |
488 | ->setWrapperLegendMsg( 'userrights-lookup-user' ) |
489 | ->prepareForm() |
490 | ->displayForm( false ); |
491 | } |
492 | |
493 | /** |
494 | * Show the form to edit group memberships. |
495 | * @param CentralAuthUser $user user you're editing |
496 | */ |
497 | private function showEditUserGroupsForm( CentralAuthUser $user ) { |
498 | $list = $membersList = $tempList = $tempMembersList = []; |
499 | foreach ( $user->getGlobalGroupsWithExpiration() as $group => $expiration ) { |
500 | $ugm = new UserGroupMembership( $user->getId(), $group, $expiration ); |
501 | $linkG = UserGroupMembership::getLinkHTML( $ugm, $this->getContext() ); |
502 | $linkM = UserGroupMembership::getLinkHTML( $ugm, $this->getContext(), $user->getName() ); |
503 | if ( $ugm->getExpiry() ) { |
504 | $tempList[] = $linkG; |
505 | $tempMembersList[] = $linkM; |
506 | } else { |
507 | $list[] = $linkG; |
508 | $membersList[] = $linkM; |
509 | } |
510 | } |
511 | |
512 | $language = $this->getLanguage(); |
513 | $displayedList = $this->msg( 'userrights-groupsmember-type' ) |
514 | ->rawParams( |
515 | $language->commaList( array_merge( $tempList, $list ) ), |
516 | $language->commaList( array_merge( $tempMembersList, $membersList ) ) |
517 | )->escaped(); |
518 | |
519 | $grouplist = ''; |
520 | $count = count( $list ) + count( $tempList ); |
521 | if ( $count > 0 ) { |
522 | $grouplist = $this->msg( 'userrights-groupsmember' ) |
523 | ->numParams( $count ) |
524 | ->params( $user->getName() ) |
525 | ->parse(); |
526 | $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n"; |
527 | } |
528 | |
529 | $userToolLinks = Linker::userToolLinks( |
530 | $user->getId(), |
531 | $user->getName(), |
532 | // default for redContribsWhenNoEdits |
533 | false, |
534 | Linker::TOOL_LINKS_EMAIL |
535 | ); |
536 | |
537 | $canChangeAny = $this->changeableGroups() !== []; |
538 | $this->getOutput()->addHTML( |
539 | Xml::openElement( |
540 | 'form', |
541 | [ |
542 | 'method' => 'post', |
543 | 'action' => $this->getPageTitle()->getLocalURL(), |
544 | 'name' => 'editGroup', |
545 | 'id' => 'mw-userrights-form2' |
546 | ] |
547 | ) . |
548 | Html::hidden( 'user', $this->mTarget ) . |
549 | Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) . |
550 | // Conflict detection |
551 | Html::hidden( |
552 | 'conflictcheck-originalgroups', |
553 | implode( ',', $user->getGlobalGroups() ) |
554 | ) . |
555 | Xml::openElement( 'fieldset' ) . |
556 | Xml::element( |
557 | 'legend', |
558 | [], |
559 | $this->msg( |
560 | $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup', |
561 | $user->getName() |
562 | )->text() |
563 | ) . |
564 | $this->msg( |
565 | $canChangeAny ? 'editinguser' : 'viewinguserrights' |
566 | )->params( wfEscapeWikiText( $user->getName() ) ) |
567 | ->rawParams( $userToolLinks )->parse() |
568 | ); |
569 | if ( $canChangeAny ) { |
570 | $this->getOutput()->addHTML( |
571 | $this->msg( 'userrights-groups-help', $user->getName() )->parse() . |
572 | $grouplist . |
573 | $this->groupCheckboxes( $user ) . |
574 | Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) . |
575 | "<tr> |
576 | <td class='mw-label'>" . |
577 | Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) . |
578 | "</td> |
579 | <td class='mw-input'>" . |
580 | Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason' ) ?? false, [ |
581 | 'id' => 'wpReason', |
582 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
583 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
584 | // Unicode codepoints. |
585 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
586 | ] ) . |
587 | "</td> |
588 | </tr> |
589 | <tr> |
590 | <td></td> |
591 | <td class='mw-submit'>" . |
592 | Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(), |
593 | [ 'name' => 'saveusergroups' ] + |
594 | Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) |
595 | ) . |
596 | "</td> |
597 | </tr>" . |
598 | Xml::closeElement( 'table' ) . "\n" |
599 | ); |
600 | } else { |
601 | $this->getOutput()->addHTML( $grouplist ); |
602 | } |
603 | $this->getOutput()->addHTML( |
604 | Xml::closeElement( 'fieldset' ) . |
605 | Xml::closeElement( 'form' ) . "\n" |
606 | ); |
607 | } |
608 | |
609 | /** |
610 | * Adds a table with checkboxes where you can select what groups to add/remove. |
611 | * |
612 | * This is only called when the user can change any of the groups. |
613 | * |
614 | * @param CentralAuthUser $user |
615 | * @return string The HTML table element with checkboxes and expiry dropdowns |
616 | */ |
617 | private function groupCheckboxes( CentralAuthUser $user ) { |
618 | $allgroups = $this->globalGroupLookup->getDefinedGroups(); |
619 | $currentGroups = $user->getGlobalGroupsWithExpiration(); |
620 | $ret = ''; |
621 | |
622 | // Get the list of preset expiry times from the system message |
623 | $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage(); |
624 | $expiryOptions = $expiryOptionsMsg->isDisabled() |
625 | ? [] |
626 | : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() ); |
627 | |
628 | // Build the HTML table |
629 | $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) . |
630 | "<tr>\n"; |
631 | $ret .= Xml::element( |
632 | 'th', |
633 | null, |
634 | $this->msg( 'userrights-changeable-col', count( $allgroups ) )->text() |
635 | ); |
636 | |
637 | $ret .= "</tr>\n<tr>\n"; |
638 | $uiLanguage = $this->getLanguage(); |
639 | |
640 | $ret .= "\t<td style='vertical-align:top;'>\n"; |
641 | foreach ( $allgroups as $group ) { |
642 | $set = array_key_exists( $group, $currentGroups ); |
643 | |
644 | $member = $uiLanguage->getGroupMemberName( $group, $user->getName() ); |
645 | $id = "wpGroup-$group"; |
646 | $checkboxHtml = Html::element( 'input', [ |
647 | 'class' => 'mw-userrights-groupcheckbox', |
648 | 'type' => 'checkbox', 'value' => '1', 'checked' => $set, |
649 | 'id' => $id, 'name' => $id, |
650 | ] ) . ' ' . Html::label( $member, $id ); |
651 | |
652 | $uiUser = $this->getUser(); |
653 | |
654 | $currentExpiry = $currentGroups[$group] ?? null; |
655 | |
656 | $expiryHtml = Xml::element( 'span', null, |
657 | $this->msg( 'userrights-expiry' )->text() ); |
658 | $expiryHtml .= Xml::openElement( 'span' ); |
659 | |
660 | // add a form element to set the expiry date |
661 | $expiryFormOptions = new XmlSelect( |
662 | "wpExpiry-$group", |
663 | // forward compatibility with HTMLForm |
664 | "mw-input-wpExpiry-$group", |
665 | $currentExpiry ? 'existing' : 'infinite' |
666 | ); |
667 | |
668 | if ( $currentExpiry ) { |
669 | $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser ); |
670 | $d = $uiLanguage->userDate( $currentExpiry, $uiUser ); |
671 | $t = $uiLanguage->userTime( $currentExpiry, $uiUser ); |
672 | $existingExpiryMessage = $this->msg( 'userrights-expiry-existing', |
673 | $timestamp, $d, $t ); |
674 | $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' ); |
675 | } |
676 | |
677 | $expiryFormOptions->addOption( |
678 | $this->msg( 'userrights-expiry-none' )->text(), |
679 | 'infinite' |
680 | ); |
681 | $expiryFormOptions->addOption( |
682 | $this->msg( 'userrights-expiry-othertime' )->text(), |
683 | 'other' |
684 | ); |
685 | |
686 | $expiryFormOptions->addOptions( $expiryOptions ); |
687 | |
688 | // Add expiry dropdown |
689 | $expiryHtml .= $expiryFormOptions->getHTML() . '<br />'; |
690 | |
691 | // Add custom expiry field |
692 | $attribs = [ |
693 | 'id' => "mw-input-wpExpiry-$group-other", |
694 | 'class' => 'mw-userrights-expiryfield', |
695 | ]; |
696 | $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs ); |
697 | |
698 | $expiryHtml .= Xml::closeElement( 'span' ); |
699 | |
700 | $divAttribs = [ |
701 | 'id' => "mw-userrights-nested-wpGroup-$group", |
702 | 'class' => 'mw-userrights-nested', |
703 | ]; |
704 | $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n"; |
705 | |
706 | $ret .= "\t\t" . Xml::tags( 'div', [], $checkboxHtml |
707 | ) . "\n"; |
708 | } |
709 | $ret .= "\t</td>\n"; |
710 | |
711 | $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' ); |
712 | |
713 | return $ret; |
714 | } |
715 | |
716 | /** |
717 | * @return string[] |
718 | */ |
719 | private function changeableGroups() { |
720 | if ( $this->getContext()->getAuthority()->isAllowed( 'globalgroupmembership' ) ) { |
721 | return $this->globalGroupLookup->getDefinedGroups(); |
722 | } |
723 | return []; |
724 | } |
725 | |
726 | /** |
727 | * @param CentralAuthUser $user |
728 | * @param OutputPage $output |
729 | */ |
730 | private function showLogFragment( $user, $output ) { |
731 | $logPage = new LogPage( 'gblrights' ); |
732 | $output->addHTML( Xml::element( 'h2', null, $logPage->getName()->text() . "\n" ) ); |
733 | LogEventsList::showLogExtract( |
734 | $output, |
735 | 'gblrights', |
736 | $this->titleFactory->makeTitle( NS_USER, $user->getName() ) |
737 | ); |
738 | } |
739 | |
740 | /** |
741 | * Return an array of subpages beginning with $search that this special page will accept. |
742 | * |
743 | * @param string $search Prefix to search for |
744 | * @param int $limit Maximum number of results to return (usually 10) |
745 | * @param int $offset Number of results to skip (usually 0) |
746 | * @return string[] Matching subpages |
747 | */ |
748 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
749 | $search = $this->userNameUtils->getCanonical( $search ); |
750 | if ( !$search ) { |
751 | // No prefix suggestion for invalid user |
752 | return []; |
753 | } |
754 | // Autocomplete subpage as user list - public to allow caching |
755 | return $this->userNamePrefixSearch |
756 | ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset ); |
757 | } |
758 | |
759 | /** |
760 | * @inheritDoc |
761 | */ |
762 | protected function getGroupName() { |
763 | return 'users'; |
764 | } |
765 | } |