MediaWiki REL1_39
SpecialUserrights.php
Go to the documentation of this file.
1<?php
32
45 protected $mTarget;
49 protected $mFetchedUser = null;
50 protected $isself = false;
51
53 private $userGroupManager;
54
56 private $userNameUtils;
57
59 private $userNamePrefixSearch;
60
62 private $userFactory;
63
70 public function __construct(
71 UserGroupManagerFactory $userGroupManagerFactory = null,
72 UserNameUtils $userNameUtils = null,
73 UserNamePrefixSearch $userNamePrefixSearch = null,
74 UserFactory $userFactory = null
75 ) {
76 parent::__construct( 'Userrights' );
77 $services = MediaWikiServices::getInstance();
78 // This class is extended and therefore falls back to global state - T263207
79 $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
80 $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
81 $this->userFactory = $userFactory ?? $services->getUserFactory();
82
83 // TODO don't hard code false, use interwiki domains. See T14518
84 $this->userGroupManager = ( $userGroupManagerFactory ?? $services->getUserGroupManagerFactory() )
85 ->getUserGroupManager( false );
86 }
87
88 public function doesWrites() {
89 return true;
90 }
91
101 public function userCanChangeRights( UserIdentity $targetUser, $checkIfSelf = true ) {
102 $isself = $this->getUser()->equals( $targetUser );
103
104 $available = $this->changeableGroups();
105 if ( $targetUser->getId() === 0 ) {
106 return false;
107 }
108
109 if ( $available['add'] || $available['remove'] ) {
110 // can change some rights for any user
111 return true;
112 }
113
114 if ( ( $available['add-self'] || $available['remove-self'] )
115 && ( $isself || !$checkIfSelf )
116 ) {
117 // can change some rights for self
118 return true;
119 }
120
121 return false;
122 }
123
131 public function execute( $par ) {
132 $user = $this->getUser();
133 $request = $this->getRequest();
134 $session = $request->getSession();
135 $out = $this->getOutput();
136
137 $out->addModules( [ 'mediawiki.special.userrights' ] );
138
139 $this->mTarget = $par ?? $request->getVal( 'user' );
140
141 if ( is_string( $this->mTarget ) ) {
142 $this->mTarget = trim( $this->mTarget );
143 }
144
145 if ( $this->mTarget !== null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->getName() ) {
146 $this->isself = true;
147 }
148
149 $fetchedStatus = $this->mTarget === null ? Status::newFatal( 'nouserspecified' ) :
150 $this->fetchUser( $this->mTarget, true );
151 if ( $fetchedStatus->isOK() ) {
152 $this->mFetchedUser = $fetchedStatus->value;
153 if ( $this->mFetchedUser instanceof User ) {
154 // Set the 'relevant user' in the skin, so it displays links like Contributions,
155 // User logs, UserRights, etc.
156 $this->getSkin()->setRelevantUser( $this->mFetchedUser );
157 }
158 }
159
160 // show a successbox, if the user rights was saved successfully
161 if (
162 $session->get( 'specialUserrightsSaveSuccess' ) &&
163 $this->mFetchedUser !== null
164 ) {
165 // Remove session data for the success message
166 $session->remove( 'specialUserrightsSaveSuccess' );
167
168 $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
169 $out->addHTML(
170 Html::successBox(
171 Html::element(
172 'p',
173 [],
174 $this->msg( 'savedrights', $this->mFetchedUser->getName() )->text()
175 ),
176 'mw-notify-success'
177 )
178 );
179 }
180
181 $this->setHeaders();
182 $this->outputHeader();
183
184 $out->addModuleStyles( 'mediawiki.special' );
185 $this->addHelpLink( 'Help:Assigning permissions' );
186
187 $this->switchForm();
188
189 if (
190 $request->wasPosted() &&
191 $request->getCheck( 'saveusergroups' ) &&
192 $this->mTarget !== null &&
193 $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
194 ) {
195 /*
196 * If the user is blocked and they only have "partial" access
197 * (e.g. they don't have the userrights permission), then don't
198 * allow them to change any user rights.
199 */
200 if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) {
201 $block = $user->getBlock();
202 if ( $block && $block->isSitewide() ) {
203 throw new UserBlockedError(
204 $block,
205 $user,
206 $this->getLanguage(),
207 $request->getIP()
208 );
209 }
210 }
211
212 $this->checkReadOnly();
213
214 // save settings
215 if ( !$fetchedStatus->isOK() ) {
216 $this->getOutput()->addWikiTextAsInterface(
217 $fetchedStatus->getWikiText( false, false, $this->getLanguage() )
218 );
219
220 return;
221 }
222
223 $targetUser = $this->mFetchedUser;
224 if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (T63252)
225 $targetUser->clearInstanceCache(); // T40989
226 }
227
228 $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' );
229 $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck );
230 $userGroups = $targetUser->getGroups();
231
232 if ( $userGroups !== $conflictCheck ) {
233 $out->addHTML( Html::errorBox(
234 $this->msg( 'userrights-conflict' )->parse()
235 ) );
236 } else {
237 $status = $this->saveUserGroups(
238 $this->mTarget,
239 $request->getVal( 'user-reason' ),
240 $targetUser
241 );
242
243 if ( $status->isOK() ) {
244 // Set session data for the success message
245 $session->set( 'specialUserrightsSaveSuccess', 1 );
246
247 $out->redirect( $this->getSuccessURL() );
248 return;
249 } else {
250 // Print an error message and redisplay the form
251 $out->wrapWikiTextAsInterface(
252 'error', $status->getWikiText( false, false, $this->getLanguage() )
253 );
254 }
255 }
256 }
257
258 // show some more forms
259 if ( $this->mTarget !== null ) {
260 $this->editUserGroupsForm( $this->mTarget );
261 }
262 }
263
264 private function getSuccessURL() {
265 return $this->getPageTitle( $this->mTarget )->getFullURL();
266 }
267
274 public function canProcessExpiries() {
275 return true;
276 }
277
287 public static function expiryToTimestamp( $expiry ) {
288 if ( wfIsInfinity( $expiry ) ) {
289 return null;
290 }
291
292 $unix = strtotime( $expiry );
293
294 if ( !$unix || $unix === -1 ) {
295 return false;
296 }
297
298 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
299 // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
300 return wfTimestamp( TS_MW, $unix );
301 }
302
312 protected function saveUserGroups( $username, $reason, $user ) {
313 $allgroups = $this->getAllGroups();
314 $addgroup = [];
315 $groupExpiries = []; // associative array of (group name => expiry)
316 $removegroup = [];
317 $existingUGMs = $user->getGroupMemberships();
318
319 // This could possibly create a highly unlikely race condition if permissions are changed between
320 // when the form is loaded and when the form is saved. Ignoring it for the moment.
321 foreach ( $allgroups as $group ) {
322 // We'll tell it to remove all unchecked groups, and add all checked groups.
323 // Later on, this gets filtered for what can actually be removed
324 if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
325 $addgroup[] = $group;
326
327 if ( $this->canProcessExpiries() ) {
328 // read the expiry information from the request
329 $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
330 if ( $expiryDropdown === 'existing' ) {
331 continue;
332 }
333
334 if ( $expiryDropdown === 'other' ) {
335 $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
336 } else {
337 $expiryValue = $expiryDropdown;
338 }
339
340 // validate the expiry
341 $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue );
342
343 if ( $groupExpiries[$group] === false ) {
344 return Status::newFatal( 'userrights-invalid-expiry', $group );
345 }
346
347 // not allowed to have things expiring in the past
348 if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
349 return Status::newFatal( 'userrights-expiry-in-past', $group );
350 }
351
352 // if the user can only add this group (not remove it), the expiry time
353 // cannot be brought forward (T156784)
354 if ( !$this->canRemove( $group ) &&
355 isset( $existingUGMs[$group] ) &&
356 ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
357 ( $groupExpiries[$group] ?: 'infinity' )
358 ) {
359 return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
360 }
361 }
362 } else {
363 $removegroup[] = $group;
364 }
365 }
366
367 $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
368
369 return Status::newGood();
370 }
371
385 public function doSaveUserGroups( $user, array $add, array $remove, $reason = '',
386 array $tags = [], array $groupExpiries = []
387 ) {
388 // Validate input set...
389 $isself = $user->getName() == $this->getUser()->getName();
390 $groups = $user->getGroups();
391 $ugms = $user->getGroupMemberships();
392 $changeable = $this->changeableGroups();
393 $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
394 $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
395
396 $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
397 $add = array_intersect( $add, $addable );
398
399 // add only groups that are not already present or that need their expiry updated,
400 // UNLESS the user can only add this group (not remove it) and the expiry time
401 // is being brought forward (T156784)
402 $add = array_filter( $add,
403 static function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
404 if ( isset( $groupExpiries[$group] ) &&
405 !in_array( $group, $removable ) &&
406 isset( $ugms[$group] ) &&
407 ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
408 ( $groupExpiries[$group] ?: 'infinity' )
409 ) {
410 return false;
411 }
412 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
413 } );
414
415 $this->getHookRunner()->onChangeUserGroups( $this->getUser(), $user, $add, $remove );
416
417 $oldGroups = $groups;
418 $oldUGMs = $user->getGroupMemberships();
419 $newGroups = $oldGroups;
420
421 // Remove groups, then add new ones/update expiries of existing ones
422 if ( $remove ) {
423 foreach ( $remove as $index => $group ) {
424 if ( !$user->removeGroup( $group ) ) {
425 unset( $remove[$index] );
426 }
427 }
428 $newGroups = array_diff( $newGroups, $remove );
429 }
430 if ( $add ) {
431 foreach ( $add as $index => $group ) {
432 $expiry = $groupExpiries[$group] ?? null;
433 if ( !$user->addGroup( $group, $expiry ) ) {
434 unset( $add[$index] );
435 }
436 }
437 $newGroups = array_merge( $newGroups, $add );
438 }
439 $newGroups = array_unique( $newGroups );
440 $newUGMs = $user->getGroupMemberships();
441
442 // Ensure that caches are cleared
443 $user->invalidateCache();
444
445 // update groups in external authentication database
446 $this->getHookRunner()->onUserGroupsChanged( $user, $add, $remove,
447 $this->getUser(), $reason, $oldUGMs, $newUGMs );
448
449 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
450 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
451 wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) );
452 wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) );
453
454 // Only add a log entry if something actually changed
455 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
456 $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
457 }
458
459 return [ $add, $remove ];
460 }
461
469 protected static function serialiseUgmForLog( $ugm ) {
470 if ( !$ugm instanceof UserGroupMembership ) {
471 return null;
472 }
473 return [ 'expiry' => $ugm->getExpiry() ];
474 }
475
486 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
487 array $tags, array $oldUGMs, array $newUGMs
488 ) {
489 // make sure $oldUGMs and $newUGMs are in the same order, and serialise
490 // each UGM object to a simplified array
491 $oldUGMs = array_map( function ( $group ) use ( $oldUGMs ) {
492 return isset( $oldUGMs[$group] ) ?
493 self::serialiseUgmForLog( $oldUGMs[$group] ) :
494 null;
495 }, $oldGroups );
496 $newUGMs = array_map( function ( $group ) use ( $newUGMs ) {
497 return isset( $newUGMs[$group] ) ?
498 self::serialiseUgmForLog( $newUGMs[$group] ) :
499 null;
500 }, $newGroups );
501
502 $logEntry = new ManualLogEntry( 'rights', 'rights' );
503 $logEntry->setPerformer( $this->getUser() );
504 $logEntry->setTarget( $user->getUserPage() );
505 $logEntry->setComment( is_string( $reason ) ? $reason : "" );
506 $logEntry->setParameters( [
507 '4::oldgroups' => $oldGroups,
508 '5::newgroups' => $newGroups,
509 'oldmetadata' => $oldUGMs,
510 'newmetadata' => $newUGMs,
511 ] );
512 $logid = $logEntry->insert();
513 if ( count( $tags ) ) {
514 $logEntry->addTags( $tags );
515 }
516 $logEntry->publish( $logid );
517 }
518
523 private function editUserGroupsForm( $username ) {
524 $status = $this->fetchUser( $username, true );
525 if ( !$status->isOK() ) {
526 $this->getOutput()->addWikiTextAsInterface(
527 $status->getWikiText( false, false, $this->getLanguage() )
528 );
529
530 return;
531 }
532
534 $user = $status->value;
535 '@phan-var User $user';
536
537 $groups = $user->getGroups();
538 $groupMemberships = $user->getGroupMemberships();
539 $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
540
541 // This isn't really ideal logging behavior, but let's not hide the
542 // interwiki logs if we're using them as is.
543 $this->showLogFragment( $user, $this->getOutput() );
544 }
545
555 public function fetchUser( $username, $writing = true ) {
556 $parts = explode( $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter ),
557 $username );
558 if ( count( $parts ) < 2 ) {
559 $name = trim( $username );
560 $dbDomain = '';
561 } else {
562 list( $name, $dbDomain ) = array_map( 'trim', $parts );
563
564 if ( WikiMap::isCurrentWikiId( $dbDomain ) ) {
565 $dbDomain = '';
566 } else {
567 if ( $writing &&
568 !$this->getAuthority()->isAllowed( 'userrights-interwiki' )
569 ) {
570 return Status::newFatal( 'userrights-no-interwiki' );
571 }
572 if ( !UserRightsProxy::validDatabase( $dbDomain ) ) {
573 return Status::newFatal( 'userrights-nodatabase', $dbDomain );
574 }
575 }
576 }
577
578 if ( $name === '' ) {
579 return Status::newFatal( 'nouserspecified' );
580 }
581
582 if ( $name[0] == '#' ) {
583 // Numeric ID can be specified...
584 // We'll do a lookup for the name internally.
585 $id = intval( substr( $name, 1 ) );
586
587 if ( $dbDomain == '' ) {
588 $name = User::whoIs( $id );
589 } else {
590 $name = UserRightsProxy::whoIs( $dbDomain, $id );
591 }
592
593 if ( !$name ) {
594 return Status::newFatal( 'noname' );
595 }
596 } else {
597 $name = $this->userNameUtils->getCanonical( $name );
598 if ( $name === false ) {
599 // invalid name
600 return Status::newFatal( 'nosuchusershort', $username );
601 }
602 }
603
604 if ( $dbDomain == '' ) {
605 $user = $this->userFactory->newFromName( $name );
606 } else {
607 $user = UserRightsProxy::newFromName( $dbDomain, $name );
608 }
609
610 if ( !$user || $user->isAnon() ) {
611 return Status::newFatal( 'nosuchusershort', $username );
612 }
613
614 if ( $user instanceof User &&
615 $user->isHidden() &&
616 !$this->getAuthority()->isAllowed( 'hideuser' )
617 ) {
618 // Cannot see hidden users, pretend they don't exist
619 return Status::newFatal( 'nosuchusershort', $username );
620 }
621
622 return Status::newGood( $user );
623 }
624
632 public function makeGroupNameList( $ids ) {
633 if ( empty( $ids ) ) {
634 return $this->msg( 'rightsnone' )->inContentLanguage()->text();
635 } else {
636 return implode( ', ', $ids );
637 }
638 }
639
643 protected function switchForm() {
644 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
645
646 $this->getOutput()->addHTML(
647 Html::openElement(
648 'form',
649 [
650 'method' => 'get',
651 'action' => wfScript(),
652 'name' => 'uluser',
653 'id' => 'mw-userrights-form1'
654 ]
655 ) .
656 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
657 Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
658 Xml::inputLabel(
659 $this->msg( 'userrights-user-editname' )->text(),
660 'user',
661 'username',
662 30,
663 $this->mTarget ? str_replace( '_', ' ', $this->mTarget ) : '',
664 [
665 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
666 ] + (
667 // Set autofocus on blank input and error input
668 $this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
669 )
670 ) . ' ' .
671 Xml::submitButton(
672 $this->msg( 'editusergroup' )->text()
673 ) .
674 Html::closeElement( 'fieldset' ) .
675 Html::closeElement( 'form' ) . "\n"
676 );
677 }
678
688 protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) {
689 $list = $membersList = $tempList = $tempMembersList = [];
690 foreach ( $groupMemberships as $ugm ) {
691 $linkG = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html' );
692 $linkM = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html',
693 $user->getName() );
694 if ( $ugm->getExpiry() ) {
695 $tempList[] = $linkG;
696 $tempMembersList[] = $linkM;
697 } else {
698 $list[] = $linkG;
699 $membersList[] = $linkM;
700
701 }
702 }
703
704 $autoList = [];
705 $autoMembersList = [];
706
707 $isUserInstance = $user instanceof User;
708
709 if ( $isUserInstance ) {
710 foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) {
711 $autoList[] = UserGroupMembership::getLink( $group, $this->getContext(), 'html' );
712 $autoMembersList[] = UserGroupMembership::getLink( $group, $this->getContext(),
713 'html', $user->getName() );
714 }
715 }
716
717 $language = $this->getLanguage();
718 $displayedList = $this->msg( 'userrights-groupsmember-type' )
719 ->rawParams(
720 $language->commaList( array_merge( $tempList, $list ) ),
721 $language->commaList( array_merge( $tempMembersList, $membersList ) )
722 )->escaped();
723 $displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
724 ->rawParams(
725 $language->commaList( $autoList ),
726 $language->commaList( $autoMembersList )
727 )->escaped();
728
729 $grouplist = '';
730 $count = count( $list ) + count( $tempList );
731 if ( $count > 0 ) {
732 $grouplist = $this->msg( 'userrights-groupsmember' )
733 ->numParams( $count )
734 ->params( $user->getName() )
735 ->parse();
736 $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
737 }
738
739 $count = count( $autoList );
740 if ( $count > 0 ) {
741 $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
742 ->numParams( $count )
743 ->params( $user->getName() )
744 ->parse();
745 $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
746 }
747
748 $systemUser = $isUserInstance && $user->isSystemUser();
749 if ( $systemUser ) {
750 $systemusernote = $this->msg( 'userrights-systemuser' )
751 ->params( $user->getName() )
752 ->parse();
753 $grouplist .= '<p>' . $systemusernote . "</p>\n";
754 }
755
756 // Only add an email link if the user is not a system user
757 $flags = $systemUser ? 0 : Linker::TOOL_LINKS_EMAIL;
758 $userToolLinks = Linker::userToolLinks(
759 $user->getId(),
760 $user->getName(),
761 false, /* default for redContribsWhenNoEdits */
762 $flags
763 );
764
765 list( $groupCheckboxes, $canChangeAny ) =
766 $this->groupCheckboxes( $groupMemberships, $user );
767 $this->getOutput()->addHTML(
768 Xml::openElement(
769 'form',
770 [
771 'method' => 'post',
772 'action' => $this->getPageTitle()->getLocalURL(),
773 'name' => 'editGroup',
774 'id' => 'mw-userrights-form2'
775 ]
776 ) .
777 Html::hidden( 'user', $this->mTarget ) .
778 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
779 Html::hidden(
780 'conflictcheck-originalgroups',
781 implode( ',', $user->getGroups() )
782 ) . // Conflict detection
783 Xml::openElement( 'fieldset' ) .
784 Xml::element(
785 'legend',
786 [],
787 $this->msg(
788 $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
789 $user->getName()
790 )->text()
791 ) .
792 $this->msg(
793 $canChangeAny ? 'editinguser' : 'viewinguserrights'
794 )->params( wfEscapeWikiText( $user->getName() ) )
795 ->rawParams( $userToolLinks )->parse()
796 );
797 if ( $canChangeAny ) {
798 $this->getOutput()->addHTML(
799 $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
800 $grouplist .
801 $groupCheckboxes .
802 Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
803 "<tr>
804 <td class='mw-label'>" .
805 Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
806 "</td>
807 <td class='mw-input'>" .
808 Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason' ) ?? false, [
809 'id' => 'wpReason',
810 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
811 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
812 // Unicode codepoints.
813 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
814 ] ) .
815 "</td>
816 </tr>
817 <tr>
818 <td></td>
819 <td class='mw-submit'>" .
820 Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
821 [ 'name' => 'saveusergroups' ] +
822 Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
823 ) .
824 "</td>
825 </tr>" .
826 Xml::closeElement( 'table' ) . "\n"
827 );
828 } else {
829 $this->getOutput()->addHTML( $grouplist );
830 }
831 $this->getOutput()->addHTML(
832 Xml::closeElement( 'fieldset' ) .
833 Xml::closeElement( 'form' ) . "\n"
834 );
835 }
836
840 protected static function getAllGroups() {
841 // TODO don't hard code false here (refers to local domain). See T14518
842 return MediaWikiServices::getInstance()
843 ->getUserGroupManagerFactory()
844 ->getUserGroupManager( false )
845 ->listAllGroups();
846 }
847
857 private function groupCheckboxes( $usergroups, $user ) {
858 $allgroups = $this->getAllGroups();
859 $ret = '';
860
861 // Get the list of preset expiry times from the system message
862 $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage();
863 $expiryOptions = $expiryOptionsMsg->isDisabled()
864 ? []
865 : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() );
866
867 // Put all column info into an associative array so that extensions can
868 // more easily manage it.
869 $columns = [ 'unchangeable' => [], 'changeable' => [] ];
870
871 foreach ( $allgroups as $group ) {
872 $set = isset( $usergroups[$group] );
873 // Users who can add the group, but not remove it, can only lengthen
874 // expiries, not shorten them. So they should only see the expiry
875 // dropdown if the group currently has a finite expiry
876 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
877 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
878 // Should the checkbox be disabled?
879 $disabledCheckbox = !(
880 ( $set && $this->canRemove( $group ) ) ||
881 ( !$set && $this->canAdd( $group ) ) );
882 // Should the expiry elements be disabled?
883 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
884 // Do we need to point out that this action is irreversible?
885 $irreversible = !$disabledCheckbox && (
886 ( $set && !$this->canAdd( $group ) ) ||
887 ( !$set && !$this->canRemove( $group ) ) );
888
889 $checkbox = [
890 'set' => $set,
891 'disabled' => $disabledCheckbox,
892 'disabled-expiry' => $disabledExpiry,
893 'irreversible' => $irreversible
894 ];
895
896 if ( $disabledCheckbox && $disabledExpiry ) {
897 $columns['unchangeable'][$group] = $checkbox;
898 } else {
899 $columns['changeable'][$group] = $checkbox;
900 }
901 }
902
903 // Build the HTML table
904 $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
905 "<tr>\n";
906 foreach ( $columns as $name => $column ) {
907 if ( $column === [] ) {
908 continue;
909 }
910 // Messages: userrights-changeable-col, userrights-unchangeable-col
911 $ret .= Xml::element(
912 'th',
913 null,
914 $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
915 );
916 }
917
918 $ret .= "</tr>\n<tr>\n";
919 $uiLanguage = $this->getLanguage();
920 $userName = $user->getName();
921 foreach ( $columns as $column ) {
922 if ( $column === [] ) {
923 continue;
924 }
925 $ret .= "\t<td style='vertical-align:top;'>\n";
926 foreach ( $column as $group => $checkbox ) {
927 $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
928 if ( $checkbox['disabled'] ) {
929 $attr['disabled'] = 'disabled';
930 }
931
932 $member = $uiLanguage->getGroupMemberName( $group, $userName );
933 if ( $checkbox['irreversible'] ) {
934 $text = $this->msg( 'userrights-irreversible-marker', $member )->text();
935 } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
936 $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
937 } else {
938 $text = $member;
939 }
940 $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
941 "wpGroup-" . $group, $checkbox['set'], $attr );
942
943 if ( $this->canProcessExpiries() ) {
944 $uiUser = $this->getUser();
945
946 $currentExpiry = isset( $usergroups[$group] ) ?
947 $usergroups[$group]->getExpiry() :
948 null;
949
950 // If the user can't modify the expiry, print the current expiry below
951 // it in plain text. Otherwise provide UI to set/change the expiry
952 if ( $checkbox['set'] &&
953 ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
954 ) {
955 if ( $currentExpiry ) {
956 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
957 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
958 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
959 $expiryHtml = Xml::element( 'span', null,
960 $this->msg( 'userrights-expiry-current' )->params(
961 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
962 } else {
963 $expiryHtml = Xml::element( 'span', null,
964 $this->msg( 'userrights-expiry-none' )->text() );
965 }
966 // T171345: Add a hidden form element so that other groups can still be manipulated,
967 // otherwise saving errors out with an invalid expiry time for this group.
968 $expiryHtml .= Html::hidden( "wpExpiry-$group",
969 $currentExpiry ? 'existing' : 'infinite' );
970 $expiryHtml .= "<br />\n";
971 } else {
972 $expiryHtml = Xml::element( 'span', null,
973 $this->msg( 'userrights-expiry' )->text() );
974 $expiryHtml .= Xml::openElement( 'span' );
975
976 // add a form element to set the expiry date
977 $expiryFormOptions = new XmlSelect(
978 "wpExpiry-$group",
979 "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
980 $currentExpiry ? 'existing' : 'infinite'
981 );
982 if ( $checkbox['disabled-expiry'] ) {
983 $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
984 }
985
986 if ( $currentExpiry ) {
987 $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
988 $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
989 $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
990 $existingExpiryMessage = $this->msg( 'userrights-expiry-existing',
991 $timestamp, $d, $t );
992 $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
993 }
994
995 $expiryFormOptions->addOption(
996 $this->msg( 'userrights-expiry-none' )->text(),
997 'infinite'
998 );
999 $expiryFormOptions->addOption(
1000 $this->msg( 'userrights-expiry-othertime' )->text(),
1001 'other'
1002 );
1003
1004 $expiryFormOptions->addOptions( $expiryOptions );
1005
1006 // Add expiry dropdown
1007 $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
1008
1009 // Add custom expiry field
1010 $attribs = [
1011 'id' => "mw-input-wpExpiry-$group-other",
1012 'class' => 'mw-userrights-expiryfield',
1013 ];
1014 if ( $checkbox['disabled-expiry'] ) {
1015 $attribs['disabled'] = 'disabled';
1016 }
1017 $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
1018
1019 // If the user group is set but the checkbox is disabled, mimic a
1020 // checked checkbox in the form submission
1021 if ( $checkbox['set'] && $checkbox['disabled'] ) {
1022 $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
1023 }
1024
1025 $expiryHtml .= Xml::closeElement( 'span' );
1026 }
1027
1028 $divAttribs = [
1029 'id' => "mw-userrights-nested-wpGroup-$group",
1030 'class' => 'mw-userrights-nested',
1031 ];
1032 $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
1033 }
1034 $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
1035 ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
1036 : Xml::tags( 'div', [], $checkboxHtml )
1037 ) . "\n";
1038 }
1039 $ret .= "\t</td>\n";
1040 }
1041 $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
1042
1043 return [ $ret, (bool)$columns['changeable'] ];
1044 }
1045
1050 private function canRemove( $group ) {
1051 $groups = $this->changeableGroups();
1052
1053 return in_array(
1054 $group,
1055 $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
1056 );
1057 }
1058
1063 private function canAdd( $group ) {
1064 $groups = $this->changeableGroups();
1065
1066 return in_array(
1067 $group,
1068 $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
1069 );
1070 }
1071
1080 protected function changeableGroups() {
1081 return $this->userGroupManager->getGroupsChangeableBy( $this->getContext()->getAuthority() );
1082 }
1083
1090 protected function showLogFragment( $user, $output ) {
1091 $rightsLogPage = new LogPage( 'rights' );
1092 $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
1093 LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
1094 }
1095
1104 public function prefixSearchSubpages( $search, $limit, $offset ) {
1105 $search = $this->userNameUtils->getCanonical( $search );
1106 if ( !$search ) {
1107 // No prefix suggestion for invalid user
1108 return [];
1109 }
1110 // Autocomplete subpage as user list - public to allow caching
1111 return $this->userNamePrefixSearch
1112 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1113 }
1114
1115 protected function getGroupName() {
1116 return 'users';
1117 }
1118}
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 hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition Html.php:851
const TOOL_LINKS_EMAIL
Definition Linker.php:47
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:1159
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null, $localizer=null, $user=null, $config=null, $relevantTitle=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2299
Class to simplify the use of log pages.
Definition LogPage.php:39
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Creates User objects.
Factory service for UserGroupManager instances.
Handles searching prefixes of user names.
UserNameUtils service.
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.
getAuthority()
Shortcut to get the Authority executing this instance.
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 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.
internal since 1.36
Definition User.php:70
clearInstanceCache( $reloadFrom=false)
Clear various cached data stored in this object.
Definition User.php:1372
isHidden()
Check if user account is hidden.
Definition User.php:1633
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:915
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.
__construct(UserGroupManagerFactory $userGroupManagerFactory=null, UserNameUtils $userNameUtils=null, UserNamePrefixSearch $userNamePrefixSearch=null, UserFactory $userFactory=null)
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.
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...
null User $mFetchedUser
The user object of the target username or null.
showLogFragment( $user, $output)
Show a rights log fragment for the specified user.
userCanChangeRights(UserIdentity $targetUser, $checkIfSelf=true)
Check whether the current user (from context) can change the target user's rights.
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.
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
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:121
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:112
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition Xml.php:283
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition Xml.php:428
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition Xml.php:134
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:43
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)