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