MediaWiki REL1_35
SpecialUserrights.php
Go to the documentation of this file.
1<?php
25
38 protected $mTarget;
39 /*
40 * @var null|User $mFetchedUser The user object of the target username or null.
41 */
42 protected $mFetchedUser = null;
43 protected $isself = false;
44
45 public function __construct() {
46 parent::__construct( 'Userrights' );
47 }
48
49 public function doesWrites() {
50 return true;
51 }
52
62 public function userCanChangeRights( $targetUser, $checkIfSelf = true ) {
63 $isself = $this->getUser()->equals( $targetUser );
64
65 $available = $this->changeableGroups();
66 if ( $targetUser->getId() === 0 ) {
67 return false;
68 }
69
70 if ( $available['add'] || $available['remove'] ) {
71 // can change some rights for any user
72 return true;
73 }
74
75 if ( ( $available['add-self'] || $available['remove-self'] )
76 && ( $isself || !$checkIfSelf )
77 ) {
78 // can change some rights for self
79 return true;
80 }
81
82 return false;
83 }
84
93 public function execute( $par ) {
94 $user = $this->getUser();
95 $request = $this->getRequest();
96 $session = $request->getSession();
97 $out = $this->getOutput();
98
99 $out->addModules( [ 'mediawiki.special.userrights' ] );
100
101 $this->mTarget = $par ?? $request->getVal( 'user' );
102
103 if ( is_string( $this->mTarget ) ) {
104 $this->mTarget = trim( $this->mTarget );
105 }
106
107 if ( $this->mTarget !== null && User::getCanonicalName( $this->mTarget ) === $user->getName() ) {
108 $this->isself = true;
109 }
110
111 $fetchedStatus = $this->fetchUser( $this->mTarget, true );
112 if ( $fetchedStatus->isOK() ) {
113 $this->mFetchedUser = $fetchedStatus->value;
114 if ( $this->mFetchedUser instanceof User ) {
115 // Set the 'relevant user' in the skin, so it displays links like Contributions,
116 // User logs, UserRights, etc.
117 $this->getSkin()->setRelevantUser( $this->mFetchedUser );
118 }
119 }
120
121 // show a successbox, if the user rights was saved successfully
122 if (
123 $session->get( 'specialUserrightsSaveSuccess' ) &&
124 $this->mFetchedUser !== null
125 ) {
126 // Remove session data for the success message
127 $session->remove( 'specialUserrightsSaveSuccess' );
128
129 $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
130 $out->addHTML(
131 Html::rawElement(
132 'div',
133 [
134 'class' => 'mw-notify-success successbox',
135 'id' => 'mw-preferences-success',
136 'data-mw-autohide' => 'false',
137 ],
138 Html::element(
139 'p',
140 [],
141 $this->msg( 'savedrights', $this->mFetchedUser->getName() )->text()
142 )
143 )
144 );
145 }
146
147 $this->setHeaders();
148 $this->outputHeader();
149
150 $out->addModuleStyles( 'mediawiki.special' );
151 $this->addHelpLink( 'Help:Assigning permissions' );
152
153 $this->switchForm();
154
155 if (
156 $request->wasPosted() &&
157 $request->getCheck( 'saveusergroups' ) &&
158 $this->mTarget !== null &&
159 $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
160 ) {
161 /*
162 * If the user is blocked and they only have "partial" access
163 * (e.g. they don't have the userrights permission), then don't
164 * allow them to change any user rights.
165 */
166 if ( !MediaWikiServices::getInstance()
168 ->userHasRight( $user, 'userrights' )
169 ) {
170 $block = $user->getBlock();
171 if ( $block && $block->isSitewide() ) {
172 throw new UserBlockedError(
173 $block,
174 $user,
175 $this->getLanguage(),
176 $request->getIP()
177 );
178 }
179 }
180
181 $this->checkReadOnly();
182
183 // save settings
184 if ( !$fetchedStatus->isOK() ) {
185 $this->getOutput()->addWikiTextAsInterface(
186 $fetchedStatus->getWikiText( false, false, $this->getLanguage() )
187 );
188
189 return;
190 }
191
192 $targetUser = $this->mFetchedUser;
193 if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (T63252)
194 $targetUser->clearInstanceCache(); // T40989
195 }
196
197 $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' );
198 $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck );
199 $userGroups = $targetUser->getGroups();
200
201 if ( $userGroups !== $conflictCheck ) {
202 $out->wrapWikiMsg( '<span class="error">$1</span>', 'userrights-conflict' );
203 } else {
204 $status = $this->saveUserGroups(
205 $this->mTarget,
206 $request->getVal( 'user-reason' ),
207 $targetUser
208 );
209
210 if ( $status->isOK() ) {
211 // Set session data for the success message
212 $session->set( 'specialUserrightsSaveSuccess', 1 );
213
214 $out->redirect( $this->getSuccessURL() );
215 return;
216 } else {
217 // Print an error message and redisplay the form
218 $out->wrapWikiTextAsInterface(
219 'error', $status->getWikiText( false, false, $this->getLanguage() )
220 );
221 }
222 }
223 }
224
225 // show some more forms
226 if ( $this->mTarget !== null ) {
227 $this->editUserGroupsForm( $this->mTarget );
228 }
229 }
230
231 private function getSuccessURL() {
232 return $this->getPageTitle( $this->mTarget )->getFullURL();
233 }
234
241 public function canProcessExpiries() {
242 return true;
243 }
244
254 public static function expiryToTimestamp( $expiry ) {
255 if ( wfIsInfinity( $expiry ) ) {
256 return null;
257 }
258
259 $unix = strtotime( $expiry );
260
261 if ( !$unix || $unix === -1 ) {
262 return false;
263 }
264
265 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
266 // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
267 return wfTimestamp( TS_MW, $unix );
268 }
269
279 protected function saveUserGroups( $username, $reason, $user ) {
280 $allgroups = $this->getAllGroups();
281 $addgroup = [];
282 $groupExpiries = []; // associative array of (group name => expiry)
283 $removegroup = [];
284 $existingUGMs = $user->getGroupMemberships();
285
286 // This could possibly create a highly unlikely race condition if permissions are changed between
287 // when the form is loaded and when the form is saved. Ignoring it for the moment.
288 foreach ( $allgroups as $group ) {
289 // We'll tell it to remove all unchecked groups, and add all checked groups.
290 // Later on, this gets filtered for what can actually be removed
291 if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
292 $addgroup[] = $group;
293
294 if ( $this->canProcessExpiries() ) {
295 // read the expiry information from the request
296 $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
297 if ( $expiryDropdown === 'existing' ) {
298 continue;
299 }
300
301 if ( $expiryDropdown === 'other' ) {
302 $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
303 } else {
304 $expiryValue = $expiryDropdown;
305 }
306
307 // validate the expiry
308 $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue );
309
310 if ( $groupExpiries[$group] === false ) {
311 return Status::newFatal( 'userrights-invalid-expiry', $group );
312 }
313
314 // not allowed to have things expiring in the past
315 if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
316 return Status::newFatal( 'userrights-expiry-in-past', $group );
317 }
318
319 // if the user can only add this group (not remove it), the expiry time
320 // cannot be brought forward (T156784)
321 if ( !$this->canRemove( $group ) &&
322 isset( $existingUGMs[$group] ) &&
323 ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
324 ( $groupExpiries[$group] ?: 'infinity' )
325 ) {
326 return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
327 }
328 }
329 } else {
330 $removegroup[] = $group;
331 }
332 }
333
334 $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
335
336 return Status::newGood();
337 }
338
352 public function doSaveUserGroups( $user, array $add, array $remove, $reason = '',
353 array $tags = [], array $groupExpiries = []
354 ) {
355 // Validate input set...
356 $isself = $user->getName() == $this->getUser()->getName();
357 $groups = $user->getGroups();
358 $ugms = $user->getGroupMemberships();
359 $changeable = $this->changeableGroups();
360 $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
361 $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
362
363 $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
364 $add = array_intersect( $add, $addable );
365
366 // add only groups that are not already present or that need their expiry updated,
367 // UNLESS the user can only add this group (not remove it) and the expiry time
368 // is being brought forward (T156784)
369 $add = array_filter( $add,
370 function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
371 if ( isset( $groupExpiries[$group] ) &&
372 !in_array( $group, $removable ) &&
373 isset( $ugms[$group] ) &&
374 ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
375 ( $groupExpiries[$group] ?: 'infinity' )
376 ) {
377 return false;
378 }
379 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
380 } );
381
382 $this->getHookRunner()->onChangeUserGroups( $this->getUser(), $user, $add, $remove );
383
384 $oldGroups = $groups;
385 $oldUGMs = $user->getGroupMemberships();
386 $newGroups = $oldGroups;
387
388 // Remove groups, then add new ones/update expiries of existing ones
389 if ( $remove ) {
390 foreach ( $remove as $index => $group ) {
391 if ( !$user->removeGroup( $group ) ) {
392 unset( $remove[$index] );
393 }
394 }
395 $newGroups = array_diff( $newGroups, $remove );
396 }
397 if ( $add ) {
398 foreach ( $add as $index => $group ) {
399 $expiry = $groupExpiries[$group] ?? null;
400 if ( !$user->addGroup( $group, $expiry ) ) {
401 unset( $add[$index] );
402 }
403 }
404 $newGroups = array_merge( $newGroups, $add );
405 }
406 $newGroups = array_unique( $newGroups );
407 $newUGMs = $user->getGroupMemberships();
408
409 // Ensure that caches are cleared
410 $user->invalidateCache();
411
412 // update groups in external authentication database
413 $this->getHookRunner()->onUserGroupsChanged( $user, $add, $remove,
414 $this->getUser(), $reason, $oldUGMs, $newUGMs );
415
416 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
417 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
418 wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) );
419 wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) );
420
421 // Only add a log entry if something actually changed
422 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
423 $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
424 }
425
426 return [ $add, $remove ];
427 }
428
436 protected static function serialiseUgmForLog( $ugm ) {
437 if ( !$ugm instanceof UserGroupMembership ) {
438 return null;
439 }
440 return [ 'expiry' => $ugm->getExpiry() ];
441 }
442
453 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
454 array $tags, array $oldUGMs, array $newUGMs
455 ) {
456 // make sure $oldUGMs and $newUGMs are in the same order, and serialise
457 // each UGM object to a simplified array
458 $oldUGMs = array_map( function ( $group ) use ( $oldUGMs ) {
459 return isset( $oldUGMs[$group] ) ?
460 self::serialiseUgmForLog( $oldUGMs[$group] ) :
461 null;
462 }, $oldGroups );
463 $newUGMs = array_map( function ( $group ) use ( $newUGMs ) {
464 return isset( $newUGMs[$group] ) ?
465 self::serialiseUgmForLog( $newUGMs[$group] ) :
466 null;
467 }, $newGroups );
468
469 $logEntry = new ManualLogEntry( 'rights', 'rights' );
470 $logEntry->setPerformer( $this->getUser() );
471 $logEntry->setTarget( $user->getUserPage() );
472 $logEntry->setComment( $reason );
473 $logEntry->setParameters( [
474 '4::oldgroups' => $oldGroups,
475 '5::newgroups' => $newGroups,
476 'oldmetadata' => $oldUGMs,
477 'newmetadata' => $newUGMs,
478 ] );
479 $logid = $logEntry->insert();
480 if ( count( $tags ) ) {
481 $logEntry->addTags( $tags );
482 }
483 $logEntry->publish( $logid );
484 }
485
490 private function editUserGroupsForm( $username ) {
491 $status = $this->fetchUser( $username, true );
492 if ( !$status->isOK() ) {
493 $this->getOutput()->addWikiTextAsInterface(
494 $status->getWikiText( false, false, $this->getLanguage() )
495 );
496
497 return;
498 }
499
501 $user = $status->value;
502 '@phan-var User $user';
503
504 $groups = $user->getGroups();
505 $groupMemberships = $user->getGroupMemberships();
506 $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
507
508 // This isn't really ideal logging behavior, but let's not hide the
509 // interwiki logs if we're using them as is.
510 $this->showLogFragment( $user, $this->getOutput() );
511 }
512
522 public function fetchUser( $username, $writing = true ) {
523 $parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username );
524 if ( count( $parts ) < 2 ) {
525 $name = trim( $username );
526 $dbDomain = '';
527 } else {
528 list( $name, $dbDomain ) = array_map( 'trim', $parts );
529
530 if ( WikiMap::isCurrentWikiId( $dbDomain ) ) {
531 $dbDomain = '';
532 } else {
533 if ( $writing && !MediaWikiServices::getInstance()
535 ->userHasRight( $this->getUser(), 'userrights-interwiki' )
536 ) {
537 return Status::newFatal( 'userrights-no-interwiki' );
538 }
539 if ( !UserRightsProxy::validDatabase( $dbDomain ) ) {
540 return Status::newFatal( 'userrights-nodatabase', $dbDomain );
541 }
542 }
543 }
544
545 if ( $name === '' ) {
546 return Status::newFatal( 'nouserspecified' );
547 }
548
549 if ( $name[0] == '#' ) {
550 // Numeric ID can be specified...
551 // We'll do a lookup for the name internally.
552 $id = intval( substr( $name, 1 ) );
553
554 if ( $dbDomain == '' ) {
555 $name = User::whoIs( $id );
556 } else {
557 $name = UserRightsProxy::whoIs( $dbDomain, $id );
558 }
559
560 if ( !$name ) {
561 return Status::newFatal( 'noname' );
562 }
563 } else {
564 $name = User::getCanonicalName( $name );
565 if ( $name === false ) {
566 // invalid name
567 return Status::newFatal( 'nosuchusershort', $username );
568 }
569 }
570
571 if ( $dbDomain == '' ) {
572 $user = User::newFromName( $name );
573 } else {
574 $user = UserRightsProxy::newFromName( $dbDomain, $name );
575 }
576
577 if ( !$user || $user->isAnon() ) {
578 return Status::newFatal( 'nosuchusershort', $username );
579 }
580
581 if ( $user instanceof User &&
582 $user->isHidden() &&
583 !MediaWikiServices::getInstance()
584 ->getPermissionManager()
585 ->userHasRight( $this->getUser(), 'hideuser' )
586 ) {
587 // Cannot see hidden users, pretend they don't exist
588 return Status::newFatal( 'nosuchusershort', $username );
589 }
590
591 return Status::newGood( $user );
592 }
593
601 public function makeGroupNameList( $ids ) {
602 if ( empty( $ids ) ) {
603 return $this->msg( 'rightsnone' )->inContentLanguage()->text();
604 } else {
605 return implode( ', ', $ids );
606 }
607 }
608
612 protected function switchForm() {
613 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
614
615 $this->getOutput()->addHTML(
616 Html::openElement(
617 'form',
618 [
619 'method' => 'get',
620 'action' => wfScript(),
621 'name' => 'uluser',
622 'id' => 'mw-userrights-form1'
623 ]
624 ) .
625 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
626 Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
627 Xml::inputLabel(
628 $this->msg( 'userrights-user-editname' )->text(),
629 'user',
630 'username',
631 30,
632 $this->mTarget ? str_replace( '_', ' ', $this->mTarget ) : '',
633 [
634 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
635 ] + (
636 // Set autofocus on blank input and error input
637 $this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
638 )
639 ) . ' ' .
640 Xml::submitButton(
641 $this->msg( 'editusergroup' )->text()
642 ) .
643 Html::closeElement( 'fieldset' ) .
644 Html::closeElement( 'form' ) . "\n"
645 );
646 }
647
657 protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) {
658 $list = $membersList = $tempList = $tempMembersList = [];
659 foreach ( $groupMemberships as $ugm ) {
660 $linkG = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html' );
661 $linkM = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html',
662 $user->getName() );
663 if ( $ugm->getExpiry() ) {
664 $tempList[] = $linkG;
665 $tempMembersList[] = $linkM;
666 } else {
667 $list[] = $linkG;
668 $membersList[] = $linkM;
669
670 }
671 }
672
673 $autoList = [];
674 $autoMembersList = [];
675
676 $isUserInstance = $user instanceof User;
677
678 if ( $isUserInstance ) {
679 foreach ( Autopromote::getAutopromoteGroups( $user ) as $group ) {
680 $autoList[] = UserGroupMembership::getLink( $group, $this->getContext(), 'html' );
681 $autoMembersList[] = UserGroupMembership::getLink( $group, $this->getContext(),
682 'html', $user->getName() );
683 }
684 }
685
686 $language = $this->getLanguage();
687 $displayedList = $this->msg( 'userrights-groupsmember-type' )
688 ->rawParams(
689 $language->commaList( array_merge( $tempList, $list ) ),
690 $language->commaList( array_merge( $tempMembersList, $membersList ) )
691 )->escaped();
692 $displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
693 ->rawParams(
694 $language->commaList( $autoList ),
695 $language->commaList( $autoMembersList )
696 )->escaped();
697
698 $grouplist = '';
699 $count = count( $list ) + count( $tempList );
700 if ( $count > 0 ) {
701 $grouplist = $this->msg( 'userrights-groupsmember' )
702 ->numParams( $count )
703 ->params( $user->getName() )
704 ->parse();
705 $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
706 }
707
708 $count = count( $autoList );
709 if ( $count > 0 ) {
710 $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
711 ->numParams( $count )
712 ->params( $user->getName() )
713 ->parse();
714 $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
715 }
716
717 $systemUser = $isUserInstance && $user->isSystemUser();
718 if ( $systemUser ) {
719 $systemusernote = $this->msg( 'userrights-systemuser' )
720 ->params( $user->getName() )
721 ->parse();
722 $grouplist .= '<p>' . $systemusernote . "</p>\n";
723 }
724
725 // Only add an email link if the user is not a system user
726 $flags = $systemUser ? 0 : Linker::TOOL_LINKS_EMAIL;
727 $userToolLinks = Linker::userToolLinks(
728 $user->getId(),
729 $user->getName(),
730 false, /* default for redContribsWhenNoEdits */
731 $flags
732 );
733
734 list( $groupCheckboxes, $canChangeAny ) =
735 $this->groupCheckboxes( $groupMemberships, $user );
736 $this->getOutput()->addHTML(
737 Xml::openElement(
738 'form',
739 [
740 'method' => 'post',
741 'action' => $this->getPageTitle()->getLocalURL(),
742 'name' => 'editGroup',
743 'id' => 'mw-userrights-form2'
744 ]
745 ) .
746 Html::hidden( 'user', $this->mTarget ) .
747 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
748 Html::hidden(
749 'conflictcheck-originalgroups',
750 implode( ',', $user->getGroups() )
751 ) . // Conflict detection
752 Xml::openElement( 'fieldset' ) .
753 Xml::element(
754 'legend',
755 [],
756 $this->msg(
757 $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
758 $user->getName()
759 )->text()
760 ) .
761 $this->msg(
762 $canChangeAny ? 'editinguser' : 'viewinguserrights'
763 )->params( wfEscapeWikiText( $user->getName() ) )
764 ->rawParams( $userToolLinks )->parse()
765 );
766 if ( $canChangeAny ) {
767 $this->getOutput()->addHTML(
768 $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
769 $grouplist .
770 $groupCheckboxes .
771 Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
772 "<tr>
773 <td class='mw-label'>" .
774 Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
775 "</td>
776 <td class='mw-input'>" .
777 Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ), [
778 'id' => 'wpReason',
779 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
780 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
781 // Unicode codepoints.
782 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
783 ] ) .
784 "</td>
785 </tr>
786 <tr>
787 <td></td>
788 <td class='mw-submit'>" .
789 Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
790 [ 'name' => 'saveusergroups' ] +
791 Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
792 ) .
793 "</td>
794 </tr>" .
795 Xml::closeElement( 'table' ) . "\n"
796 );
797 } else {
798 $this->getOutput()->addHTML( $grouplist );
799 }
800 $this->getOutput()->addHTML(
801 Xml::closeElement( 'fieldset' ) .
802 Xml::closeElement( 'form' ) . "\n"
803 );
804 }
805
810 protected static function getAllGroups() {
811 return User::getAllGroups();
812 }
813
823 private function groupCheckboxes( $usergroups, $user ) {
824 $allgroups = $this->getAllGroups();
825 $ret = '';
826
827 // Get the list of preset expiry times from the system message
828 $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage();
829 $expiryOptions = $expiryOptionsMsg->isDisabled()
830 ? []
831 : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() );
832
833 // Put all column info into an associative array so that extensions can
834 // more easily manage it.
835 $columns = [ 'unchangeable' => [], 'changeable' => [] ];
836
837 foreach ( $allgroups as $group ) {
838 $set = isset( $usergroups[$group] );
839 // Users who can add the group, but not remove it, can only lengthen
840 // expiries, not shorten them. So they should only see the expiry
841 // dropdown if the group currently has a finite expiry
842 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
843 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
844 // Should the checkbox be disabled?
845 $disabledCheckbox = !(
846 ( $set && $this->canRemove( $group ) ) ||
847 ( !$set && $this->canAdd( $group ) ) );
848 // Should the expiry elements be disabled?
849 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
850 // Do we need to point out that this action is irreversible?
851 $irreversible = !$disabledCheckbox && (
852 ( $set && !$this->canAdd( $group ) ) ||
853 ( !$set && !$this->canRemove( $group ) ) );
854
855 $checkbox = [
856 'set' => $set,
857 'disabled' => $disabledCheckbox,
858 'disabled-expiry' => $disabledExpiry,
859 'irreversible' => $irreversible
860 ];
861
862 if ( $disabledCheckbox && $disabledExpiry ) {
863 $columns['unchangeable'][$group] = $checkbox;
864 } else {
865 $columns['changeable'][$group] = $checkbox;
866 }
867 }
868
869 // Build the HTML table
870 $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
871 "<tr>\n";
872 foreach ( $columns as $name => $column ) {
873 if ( $column === [] ) {
874 continue;
875 }
876 // Messages: userrights-changeable-col, userrights-unchangeable-col
877 $ret .= Xml::element(
878 'th',
879 null,
880 $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
881 );
882 }
883
884 $ret .= "</tr>\n<tr>\n";
885 foreach ( $columns as $column ) {
886 if ( $column === [] ) {
887 continue;
888 }
889 $ret .= "\t<td style='vertical-align:top;'>\n";
890 foreach ( $column as $group => $checkbox ) {
891 $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
892 if ( $checkbox['disabled'] ) {
893 $attr['disabled'] = 'disabled';
894 }
895
896 $member = UserGroupMembership::getGroupMemberName( $group, $user->getName() );
897 if ( $checkbox['irreversible'] ) {
898 $text = $this->msg( 'userrights-irreversible-marker', $member )->text();
899 } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
900 $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
901 } else {
902 $text = $member;
903 }
904 $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
905 "wpGroup-" . $group, $checkbox['set'], $attr );
906
907 if ( $this->canProcessExpiries() ) {
908 $uiUser = $this->getUser();
909 $uiLanguage = $this->getLanguage();
910
911 $currentExpiry = isset( $usergroups[$group] ) ?
912 $usergroups[$group]->getExpiry() :
913 null;
914
915 // If the user can't modify the expiry, print the current expiry below
916 // it in plain text. Otherwise provide UI to set/change the expiry
917 if ( $checkbox['set'] &&
918 ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
919 ) {
920 if ( $currentExpiry ) {
921 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
922 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
923 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
924 $expiryHtml = Xml::element( 'span', null,
925 $this->msg( 'userrights-expiry-current' )->params(
926 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
927 } else {
928 $expiryHtml = Xml::element( 'span', null,
929 $this->msg( 'userrights-expiry-none' )->text() );
930 }
931 // T171345: Add a hidden form element so that other groups can still be manipulated,
932 // otherwise saving errors out with an invalid expiry time for this group.
933 $expiryHtml .= Html::hidden( "wpExpiry-$group",
934 $currentExpiry ? 'existing' : 'infinite' );
935 $expiryHtml .= "<br />\n";
936 } else {
937 $expiryHtml = Xml::element( 'span', null,
938 $this->msg( 'userrights-expiry' )->text() );
939 $expiryHtml .= Xml::openElement( 'span' );
940
941 // add a form element to set the expiry date
942 $expiryFormOptions = new XmlSelect(
943 "wpExpiry-$group",
944 "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
945 $currentExpiry ? 'existing' : 'infinite'
946 );
947 if ( $checkbox['disabled-expiry'] ) {
948 $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
949 }
950
951 if ( $currentExpiry ) {
952 $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
953 $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
954 $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
955 $existingExpiryMessage = $this->msg( 'userrights-expiry-existing',
956 $timestamp, $d, $t );
957 $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
958 }
959
960 $expiryFormOptions->addOption(
961 $this->msg( 'userrights-expiry-none' )->text(),
962 'infinite'
963 );
964 $expiryFormOptions->addOption(
965 $this->msg( 'userrights-expiry-othertime' )->text(),
966 'other'
967 );
968
969 $expiryFormOptions->addOptions( $expiryOptions );
970
971 // Add expiry dropdown
972 $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
973
974 // Add custom expiry field
975 $attribs = [
976 'id' => "mw-input-wpExpiry-$group-other",
977 'class' => 'mw-userrights-expiryfield',
978 ];
979 if ( $checkbox['disabled-expiry'] ) {
980 $attribs['disabled'] = 'disabled';
981 }
982 $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
983
984 // If the user group is set but the checkbox is disabled, mimic a
985 // checked checkbox in the form submission
986 if ( $checkbox['set'] && $checkbox['disabled'] ) {
987 $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
988 }
989
990 $expiryHtml .= Xml::closeElement( 'span' );
991 }
992
993 $divAttribs = [
994 'id' => "mw-userrights-nested-wpGroup-$group",
995 'class' => 'mw-userrights-nested',
996 ];
997 $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
998 }
999 $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
1000 ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
1001 : Xml::tags( 'div', [], $checkboxHtml )
1002 ) . "\n";
1003 }
1004 $ret .= "\t</td>\n";
1005 }
1006 $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
1007
1008 return [ $ret, (bool)$columns['changeable'] ];
1009 }
1010
1015 private function canRemove( $group ) {
1016 $groups = $this->changeableGroups();
1017
1018 return in_array(
1019 $group,
1020 $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
1021 );
1022 }
1023
1028 private function canAdd( $group ) {
1029 $groups = $this->changeableGroups();
1030
1031 return in_array(
1032 $group,
1033 $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
1034 );
1035 }
1036
1047 protected function changeableGroups() {
1048 return $this->getUser()->changeableGroups();
1049 }
1050
1057 protected function showLogFragment( $user, $output ) {
1058 $rightsLogPage = new LogPage( 'rights' );
1059 $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
1060 LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
1061 }
1062
1071 public function prefixSearchSubpages( $search, $limit, $offset ) {
1072 $user = User::newFromName( $search );
1073 if ( !$user ) {
1074 // No prefix suggestion for invalid user
1075 return [];
1076 }
1077 // Autocomplete subpage as user list - public to allow caching
1078 return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
1079 }
1080
1081 protected function getGroupName() {
1082 return 'users';
1083 }
1084}
getPermissionManager()
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
static getAutopromoteGroups(User $user)
Get the groups for the given user based on $wgAutopromote.
const TOOL_LINKS_EMAIL
Definition Linker.php:41
static userToolLinks( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null, $useParentheses=true)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition Linker.php:951
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2304
Class to simplify the use of log pages.
Definition LogPage.php:37
Class for creating new log entries and inserting them into the database.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getSkin()
Shortcut to get the skin being used for this instance.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Show an error when the user tries to do something whilst blocked.
Represents a "user group membership" – a specific instance of a user belonging to a group.
static search( $audience, $search, $limit, $offset=0)
Do a prefix search of user names and return a list of matching user names.
static whoIs( $dbDomain, $id, $ignoreInvalidDB=false)
Same as User::whoIs()
static newFromName( $dbDomain, $name, $ignoreInvalidDB=false)
Factory function; get a remote user entry by name.
static validDatabase( $dbDomain)
Confirm the selected database name is a valid local interwiki database name.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:541
static getAllGroups()
Return the set of defined explicit groups.
Definition User.php:4262
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid.
Definition User.php:1130
isHidden()
Check if user account is hidden.
Definition User.php:2109
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:867
Special page to allow managing user group membership.
doSaveUserGroups( $user, array $add, array $remove, $reason='', array $tags=[], array $groupExpiries=[])
Save user groups changes in the database.
static expiryToTimestamp( $expiry)
Converts a user group membership expiry string into a timestamp.
showEditUserGroupsForm( $user, $groups, $groupMemberships)
Show the form to edit group memberships.
static getAllGroups()
Returns an array of all groups that may be edited.
switchForm()
Output a form to allow searching for a user.
null string $mTarget
The target of the local right-adjuster's interest.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
userCanChangeRights( $targetUser, $checkIfSelf=true)
Check whether the current user (from context) can change the target user's rights.
editUserGroupsForm( $username)
Edit user groups membership.
groupCheckboxes( $usergroups, $user)
Adds a table with checkboxes where you can select what groups to add/remove.
canProcessExpiries()
Returns true if this user rights form can set and change user group expiries.
fetchUser( $username, $writing=true)
Normalize the input username, which may be local or remote, and return a user (or proxy) object for m...
showLogFragment( $user, $output)
Show a rights log fragment for the specified user.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
execute( $par)
Manage forms to be shown according to posted data.
saveUserGroups( $username, $reason, $user)
Save user groups changes in the database.
static serialiseUgmForLog( $ugm)
Serialise a UserGroupMembership object for storage in the log_params section of the logging table.
doesWrites()
Indicates whether this special page may perform database writes.
addLogEntry( $user, array $oldGroups, array $newGroups, $reason, array $tags, array $oldUGMs, array $newUGMs)
Add a rights log entry for an action.
changeableGroups()
Returns $this->getUser()->changeableGroups()
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:26
static parseOptionsMessage(string $msg)
Parse labels and values out of a comma- and colon-separated list of options, such as is used for expi...
setAttribute( $name, $value)
Definition XmlSelect.php:64