MediaWiki 1.41.2
SpecialUserRights.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
27use LogPage;
50use Xml;
51use XmlSelect;
52
65 protected $mTarget;
69 protected $mFetchedUser = null;
70 protected $isself = false;
71
72 private UserGroupManagerFactory $userGroupManagerFactory;
73
75 private $userGroupManager = null;
76
77 private UserNameUtils $userNameUtils;
78 private UserNamePrefixSearch $userNamePrefixSearch;
79 private UserFactory $userFactory;
80 private ActorStoreFactory $actorStoreFactory;
81 private WatchlistManager $watchlistManager;
82
91 public function __construct(
92 UserGroupManagerFactory $userGroupManagerFactory = null,
93 UserNameUtils $userNameUtils = null,
94 UserNamePrefixSearch $userNamePrefixSearch = null,
95 UserFactory $userFactory = null,
96 ActorStoreFactory $actorStoreFactory = null,
97 WatchlistManager $watchlistManager = null
98 ) {
99 parent::__construct( 'Userrights' );
100 $services = MediaWikiServices::getInstance();
101 // This class is extended and therefore falls back to global state - T263207
102 $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
103 $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
104 $this->userFactory = $userFactory ?? $services->getUserFactory();
105 $this->userGroupManagerFactory = $userGroupManagerFactory ?? $services->getUserGroupManagerFactory();
106 $this->actorStoreFactory = $actorStoreFactory ?? $services->getActorStoreFactory();
107 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
108 }
109
110 public function doesWrites() {
111 return true;
112 }
113
125 public function userCanChangeRights( UserIdentity $targetUser, $checkIfSelf = true ) {
126 $isself = $this->getUser()->equals( $targetUser );
127
128 $userGroupManager = $this->userGroupManagerFactory
129 ->getUserGroupManager( $targetUser->getWikiId() );
130 $available = $userGroupManager->getGroupsChangeableBy( $this->getAuthority() );
131 if ( !$targetUser->isRegistered() ) {
132 return false;
133 }
134
135 if ( $available['add'] || $available['remove'] ) {
136 // can change some rights for any user
137 return true;
138 }
139
140 if ( ( $available['add-self'] || $available['remove-self'] )
141 && ( $isself || !$checkIfSelf )
142 ) {
143 // can change some rights for self
144 return true;
145 }
146
147 return false;
148 }
149
157 public function execute( $par ) {
158 $user = $this->getUser();
159 $request = $this->getRequest();
160 $session = $request->getSession();
161 $out = $this->getOutput();
162
163 $out->addModules( [ 'mediawiki.special.userrights' ] );
164
165 $this->mTarget = $par ?? $request->getVal( 'user' );
166
167 if ( is_string( $this->mTarget ) ) {
168 $this->mTarget = trim( $this->mTarget );
169 }
170
171 if ( $this->mTarget !== null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->getName() ) {
172 $this->isself = true;
173 }
174
175 $fetchedStatus = $this->mTarget === null ? Status::newFatal( 'nouserspecified' ) :
176 $this->fetchUser( $this->mTarget, true );
177 if ( $fetchedStatus->isOK() ) {
178 $this->mFetchedUser = $fetchedUser = $fetchedStatus->value;
179 // Phan false positive on Status object - T323205
180 '@phan-var UserIdentity $fetchedUser';
181 $wikiId = $fetchedUser->getWikiId();
182 if ( $wikiId === UserIdentity::LOCAL ) {
183 // Set the 'relevant user' in the skin, so it displays links like Contributions,
184 // User logs, UserRights, etc.
185 $this->getSkin()->setRelevantUser( $this->mFetchedUser );
186 }
187 $this->userGroupManager = $this->userGroupManagerFactory
188 ->getUserGroupManager( $wikiId );
189 }
190
191 // show a successbox, if the user rights was saved successfully
192 if (
193 $session->get( 'specialUserrightsSaveSuccess' ) &&
194 $this->mFetchedUser !== null
195 ) {
196 // Remove session data for the success message
197 $session->remove( 'specialUserrightsSaveSuccess' );
198
199 $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
200 $out->addHTML(
201 Html::successBox(
202 Html::element(
203 'p',
204 [],
205 $this->msg( 'savedrights', $this->getDisplayUsername( $this->mFetchedUser ) )->text()
206 ),
207 'mw-notify-success'
208 )
209 );
210 }
211
212 $this->setHeaders();
213 $this->outputHeader();
214
215 $out->addModuleStyles( 'mediawiki.special' );
216 $this->addHelpLink( 'Help:Assigning permissions' );
217
218 $this->switchForm();
219
220 if (
221 $request->wasPosted() &&
222 $request->getCheck( 'saveusergroups' ) &&
223 $this->mTarget !== null &&
224 $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
225 ) {
226 /*
227 * If the user is blocked and they only have "partial" access
228 * (e.g. they don't have the userrights permission), then don't
229 * allow them to change any user rights.
230 */
231 if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) {
232 $block = $user->getBlock();
233 if ( $block && $block->isSitewide() ) {
234 throw new UserBlockedError(
235 $block,
236 $user,
237 $this->getLanguage(),
238 $request->getIP()
239 );
240 }
241 }
242
243 $this->checkReadOnly();
244
245 // save settings
246 if ( !$fetchedStatus->isOK() ) {
247 $this->getOutput()->addWikiTextAsInterface(
248 $fetchedStatus->getWikiText( false, false, $this->getLanguage() )
249 );
250
251 return;
252 }
253
254 $targetUser = $this->mFetchedUser;
255 $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' );
256 $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck );
257 $userGroups = $this->userGroupManager->getUserGroups( $targetUser, UserGroupManager::READ_LATEST );
258
259 if ( $userGroups !== $conflictCheck ) {
260 $out->addHTML( Html::errorBox(
261 $this->msg( 'userrights-conflict' )->parse()
262 ) );
263 } else {
264 $status = $this->saveUserGroups(
265 $request->getVal( 'user-reason' ),
266 $targetUser
267 );
268
269 if ( $status->isOK() ) {
270 // Set session data for the success message
271 $session->set( 'specialUserrightsSaveSuccess', 1 );
272
273 $out->redirect( $this->getSuccessURL() );
274 return;
275 } else {
276 // Print an error message and redisplay the form
277 $out->wrapWikiTextAsInterface(
278 'error', $status->getWikiText( false, false, $this->getLanguage() )
279 );
280 }
281 }
282 }
283
284 // show some more forms
285 if ( $this->mTarget !== null ) {
286 $this->editUserGroupsForm( $this->mTarget );
287 }
288 }
289
290 private function getSuccessURL() {
291 return $this->getPageTitle( $this->mTarget )->getFullURL();
292 }
293
300 public function canProcessExpiries() {
301 return true;
302 }
303
313 public static function expiryToTimestamp( $expiry ) {
314 if ( wfIsInfinity( $expiry ) ) {
315 return null;
316 }
317
318 $unix = strtotime( $expiry );
319
320 if ( !$unix || $unix === -1 ) {
321 return false;
322 }
323
324 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
325 // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
326 return wfTimestamp( TS_MW, $unix );
327 }
328
337 protected function saveUserGroups( $reason, $user ) {
338 if ( $this->userNameUtils->isTemp( $user->getName() ) ) {
339 return Status::newFatal( 'userrights-no-tempuser' );
340 }
341 $allgroups = $this->userGroupManager->listAllGroups();
342 $addgroup = [];
343 $groupExpiries = []; // associative array of (group name => expiry)
344 $removegroup = [];
345 $existingUGMs = $this->userGroupManager->getUserGroupMemberships( $user );
346
347 // This could possibly create a highly unlikely race condition if permissions are changed between
348 // when the form is loaded and when the form is saved. Ignoring it for the moment.
349 foreach ( $allgroups as $group ) {
350 // We'll tell it to remove all unchecked groups, and add all checked groups.
351 // Later on, this gets filtered for what can actually be removed
352 if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
353 $addgroup[] = $group;
354
355 if ( $this->canProcessExpiries() ) {
356 // read the expiry information from the request
357 $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
358 if ( $expiryDropdown === 'existing' ) {
359 continue;
360 }
361
362 if ( $expiryDropdown === 'other' ) {
363 $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
364 } else {
365 $expiryValue = $expiryDropdown;
366 }
367
368 // validate the expiry
369 $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue );
370
371 if ( $groupExpiries[$group] === false ) {
372 return Status::newFatal( 'userrights-invalid-expiry', $group );
373 }
374
375 // not allowed to have things expiring in the past
376 if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
377 return Status::newFatal( 'userrights-expiry-in-past', $group );
378 }
379
380 // if the user can only add this group (not remove it), the expiry time
381 // cannot be brought forward (T156784)
382 if ( !$this->canRemove( $group ) &&
383 isset( $existingUGMs[$group] ) &&
384 ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
385 ( $groupExpiries[$group] ?: 'infinity' )
386 ) {
387 return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
388 }
389 }
390 } else {
391 $removegroup[] = $group;
392 }
393 }
394
395 $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
396
397 if ( $user->getWikiId() === UserIdentity::LOCAL && $this->getRequest()->getCheck( 'wpWatch' ) ) {
398 $this->watchlistManager->addWatchIgnoringRights(
399 $this->getUser(),
400 Title::makeTitle( NS_USER, $user->getName() )
401 );
402 }
403
404 return Status::newGood();
405 }
406
422 public function doSaveUserGroups( $user, array $add, array $remove, $reason = '',
423 array $tags = [], array $groupExpiries = []
424 ) {
425 // Validate input set...
426 $isself = $user->getName() == $this->getUser()->getName();
427 if ( $this->userGroupManager !== null ) {
428 // Used after form submit
429 $userGroupManager = $this->userGroupManager;
430 } else {
431 // Used as backend-function
432 $userGroupManager = $this->userGroupManagerFactory
433 ->getUserGroupManager( $user->getWikiId() );
434 }
435 $groups = $userGroupManager->getUserGroups( $user );
436 $ugms = $userGroupManager->getUserGroupMemberships( $user );
437 $changeable = $userGroupManager->getGroupsChangeableBy( $this->getAuthority() );
438 $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
439 $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
440
441 $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
442 $add = array_intersect( $add, $addable );
443
444 // add only groups that are not already present or that need their expiry updated,
445 // UNLESS the user can only add this group (not remove it) and the expiry time
446 // is being brought forward (T156784)
447 $add = array_filter( $add,
448 static function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
449 if ( isset( $groupExpiries[$group] ) &&
450 !in_array( $group, $removable ) &&
451 isset( $ugms[$group] ) &&
452 ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
453 ( $groupExpiries[$group] ?: 'infinity' )
454 ) {
455 return false;
456 }
457 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
458 } );
459
460 if ( $user->getWikiId() === UserIdentity::LOCAL ) {
461 // For compatibility local changes are provided as User object to the hook
462 $hookUser = $this->userFactory->newFromUserIdentity( $user );
463 } else {
464 // Interwiki changes are provided as UserIdentity since 1.41, was UserRightsProxy before
465 $hookUser = $user;
466 }
467 $this->getHookRunner()->onChangeUserGroups( $this->getUser(), $hookUser, $add, $remove );
468
469 $oldGroups = $groups;
470 $oldUGMs = $userGroupManager->getUserGroupMemberships( $user );
471 $newGroups = $oldGroups;
472
473 // Remove groups, then add new ones/update expiries of existing ones
474 if ( $remove ) {
475 foreach ( $remove as $index => $group ) {
476 if ( !$userGroupManager->removeUserFromGroup( $user, $group ) ) {
477 unset( $remove[$index] );
478 }
479 }
480 $newGroups = array_diff( $newGroups, $remove );
481 }
482 if ( $add ) {
483 foreach ( $add as $index => $group ) {
484 $expiry = $groupExpiries[$group] ?? null;
485 if ( !$userGroupManager->addUserToGroup( $user, $group, $expiry, true ) ) {
486 unset( $add[$index] );
487 }
488 }
489 $newGroups = array_merge( $newGroups, $add );
490 }
491 $newGroups = array_unique( $newGroups );
492 $newUGMs = $userGroupManager->getUserGroupMemberships( $user );
493
494 // Ensure that caches are cleared
495 $this->userFactory->invalidateCache( $user );
496
497 // update groups in external authentication database
498 $this->getHookRunner()->onUserGroupsChanged( $hookUser, $add, $remove,
499 $this->getUser(), $reason, $oldUGMs, $newUGMs );
500
501 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
502 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
503 wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) );
504 wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) );
505
506 // Only add a log entry if something actually changed
507 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
508 $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
509 }
510
511 return [ $add, $remove ];
512 }
513
521 protected static function serialiseUgmForLog( $ugm ) {
522 if ( !$ugm instanceof UserGroupMembership ) {
523 return null;
524 }
525 return [ 'expiry' => $ugm->getExpiry() ];
526 }
527
538 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
539 array $tags, array $oldUGMs, array $newUGMs
540 ) {
541 // make sure $oldUGMs and $newUGMs are in the same order, and serialise
542 // each UGM object to a simplified array
543 $oldUGMs = array_map( static function ( $group ) use ( $oldUGMs ) {
544 return isset( $oldUGMs[$group] ) ?
545 self::serialiseUgmForLog( $oldUGMs[$group] ) :
546 null;
547 }, $oldGroups );
548 $newUGMs = array_map( static function ( $group ) use ( $newUGMs ) {
549 return isset( $newUGMs[$group] ) ?
550 self::serialiseUgmForLog( $newUGMs[$group] ) :
551 null;
552 }, $newGroups );
553
554 $logEntry = new ManualLogEntry( 'rights', 'rights' );
555 $logEntry->setPerformer( $this->getUser() );
556 $logEntry->setTarget( Title::makeTitle( NS_USER, $this->getDisplayUsername( $user ) ) );
557 $logEntry->setComment( is_string( $reason ) ? $reason : "" );
558 $logEntry->setParameters( [
559 '4::oldgroups' => $oldGroups,
560 '5::newgroups' => $newGroups,
561 'oldmetadata' => $oldUGMs,
562 'newmetadata' => $newUGMs,
563 ] );
564 $logid = $logEntry->insert();
565 if ( count( $tags ) ) {
566 $logEntry->addTags( $tags );
567 }
568 $logEntry->publish( $logid );
569 }
570
575 private function editUserGroupsForm( $username ) {
576 $status = $this->fetchUser( $username, true );
577 if ( !$status->isOK() ) {
578 $this->getOutput()->addWikiTextAsInterface(
579 $status->getWikiText( false, false, $this->getLanguage() )
580 );
581
582 return;
583 }
584
586 $user = $status->value;
587 '@phan-var UserIdentity $user';
588
589 $groups = $this->userGroupManager->getUserGroups( $user );
590 $groupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
591 $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
592
593 // This isn't really ideal logging behavior, but let's not hide the
594 // interwiki logs if we're using them as is.
595 $this->showLogFragment( $user, $this->getOutput() );
596 }
597
607 public function fetchUser( $username, $writing = true ) {
608 $parts = explode( $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter ),
609 $username );
610 if ( count( $parts ) < 2 ) {
611 $name = trim( $username );
612 $wikiId = UserIdentity::LOCAL;
613 } else {
614 [ $name, $wikiId ] = array_map( 'trim', $parts );
615
616 if ( WikiMap::isCurrentWikiId( $wikiId ) ) {
617 $wikiId = UserIdentity::LOCAL;
618 } else {
619 if ( $writing &&
620 !$this->getAuthority()->isAllowed( 'userrights-interwiki' )
621 ) {
622 return Status::newFatal( 'userrights-no-interwiki' );
623 }
624 $localDatabases = $this->getConfig()->get( MainConfigNames::LocalDatabases );
625 if ( !in_array( $wikiId, $localDatabases ) ) {
626 return Status::newFatal( 'userrights-nodatabase', $wikiId );
627 }
628 }
629 }
630
631 if ( $name === '' ) {
632 return Status::newFatal( 'nouserspecified' );
633 }
634
635 $userIdentityLookup = $this->actorStoreFactory->getUserIdentityLookup( $wikiId );
636 if ( $name[0] == '#' ) {
637 // Numeric ID can be specified...
638 $id = intval( substr( $name, 1 ) );
639
640 $user = $userIdentityLookup->getUserIdentityByUserId( $id );
641 if ( !$user ) {
642 // Different error message for compatibility
643 return Status::newFatal( 'noname' );
644 }
645 $name = $user->getName();
646 } else {
647 $name = $this->userNameUtils->getCanonical( $name );
648 if ( $name === false ) {
649 // invalid name
650 return Status::newFatal( 'nosuchusershort', $username );
651 }
652 $user = $userIdentityLookup->getUserIdentityByName( $name );
653 }
654
655 if ( $this->userNameUtils->isTemp( $name ) ) {
656 return Status::newFatal( 'userrights-no-group' );
657 }
658
659 if ( !$user || !$user->isRegistered() ) {
660 return Status::newFatal( 'nosuchusershort', $username );
661 }
662
663 if ( $user->getWikiId() === UserIdentity::LOCAL &&
664 $this->userFactory->newFromUserIdentity( $user )->isHidden() &&
665 !$this->getAuthority()->isAllowed( 'hideuser' )
666 ) {
667 // Cannot see hidden users, pretend they don't exist
668 return Status::newFatal( 'nosuchusershort', $username );
669 }
670
671 return Status::newGood( $user );
672 }
673
681 public function makeGroupNameList( $ids ) {
682 if ( !$ids ) {
683 return $this->msg( 'rightsnone' )->inContentLanguage()->text();
684 } else {
685 return implode( ', ', $ids );
686 }
687 }
688
692 protected function switchForm() {
693 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
694
695 $this->getOutput()->addHTML(
696 Html::openElement(
697 'form',
698 [
699 'method' => 'get',
700 'action' => wfScript(),
701 'name' => 'uluser',
702 'id' => 'mw-userrights-form1'
703 ]
704 ) .
705 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
706 Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
707 Xml::inputLabel(
708 $this->msg( 'userrights-user-editname' )->text(),
709 'user',
710 'username',
711 30,
712 $this->mTarget ? str_replace( '_', ' ', $this->mTarget ) : '',
713 [
714 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
715 ] + (
716 // Set autofocus on blank input and error input
717 $this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
718 )
719 ) . ' ' .
720 Xml::submitButton(
721 $this->msg( 'editusergroup' )->text()
722 ) .
723 Html::closeElement( 'fieldset' ) .
724 Html::closeElement( 'form' ) . "\n"
725 );
726 }
727
737 protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) {
738 $list = $membersList = $tempList = $tempMembersList = [];
739 foreach ( $groupMemberships as $ugm ) {
740 $linkG = UserGroupMembership::getLinkHTML( $ugm, $this->getContext() );
741 $linkM = UserGroupMembership::getLinkHTML( $ugm, $this->getContext(), $user->getName() );
742 if ( $ugm->getExpiry() ) {
743 $tempList[] = $linkG;
744 $tempMembersList[] = $linkM;
745 } else {
746 $list[] = $linkG;
747 $membersList[] = $linkM;
748
749 }
750 }
751
752 $autoList = [];
753 $autoMembersList = [];
754
755 if ( $user->getWikiId() === UserIdentity::LOCAL ) {
756 // Listing autopromote groups works only on the local wiki
757 foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) {
758 $autoList[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() );
759 $autoMembersList[] = UserGroupMembership::getLinkHTML( $group, $this->getContext(), $user->getName() );
760 }
761 }
762
763 $language = $this->getLanguage();
764 $displayedList = $this->msg( 'userrights-groupsmember-type' )
765 ->rawParams(
766 $language->commaList( array_merge( $tempList, $list ) ),
767 $language->commaList( array_merge( $tempMembersList, $membersList ) )
768 )->escaped();
769 $displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
770 ->rawParams(
771 $language->commaList( $autoList ),
772 $language->commaList( $autoMembersList )
773 )->escaped();
774
775 $grouplist = '';
776 $count = count( $list ) + count( $tempList );
777 if ( $count > 0 ) {
778 $grouplist = $this->msg( 'userrights-groupsmember' )
779 ->numParams( $count )
780 ->params( $user->getName() )
781 ->parse();
782 $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
783 }
784
785 $count = count( $autoList );
786 if ( $count > 0 ) {
787 $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
788 ->numParams( $count )
789 ->params( $user->getName() )
790 ->parse();
791 $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
792 }
793
794 $systemUser = $user->getWikiId() === UserIdentity::LOCAL
795 && $this->userFactory->newFromUserIdentity( $user )->isSystemUser();
796 if ( $systemUser ) {
797 $systemusernote = $this->msg( 'userrights-systemuser' )
798 ->params( $user->getName() )
799 ->parse();
800 $grouplist .= '<p>' . $systemusernote . "</p>\n";
801 }
802
803 // Only add an email link if the user is not a system user
804 $flags = $systemUser ? 0 : Linker::TOOL_LINKS_EMAIL;
805 $userToolLinks = Linker::userToolLinks(
806 $user->getId( $user->getWikiId() ),
807 $this->getDisplayUsername( $user ),
808 false, /* default for redContribsWhenNoEdits */
809 $flags
810 );
811
812 [ $groupCheckboxes, $canChangeAny ] =
813 $this->groupCheckboxes( $groupMemberships, $user );
814 $this->getOutput()->addHTML(
815 Xml::openElement(
816 'form',
817 [
818 'method' => 'post',
819 'action' => $this->getPageTitle()->getLocalURL(),
820 'name' => 'editGroup',
821 'id' => 'mw-userrights-form2'
822 ]
823 ) .
824 Html::hidden( 'user', $this->mTarget ) .
825 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
826 Html::hidden(
827 'conflictcheck-originalgroups',
828 implode( ',', $this->userGroupManager->getUserGroups( $user ) )
829 ) . // Conflict detection
830 Xml::openElement( 'fieldset' ) .
831 Xml::element(
832 'legend',
833 [],
834 $this->msg(
835 $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
836 $user->getName()
837 )->text()
838 ) .
839 $this->msg(
840 $canChangeAny ? 'editinguser' : 'viewinguserrights'
841 )->params( wfEscapeWikiText( $this->getDisplayUsername( $user ) ) )
842 ->rawParams( $userToolLinks )->parse()
843 );
844 if ( $canChangeAny ) {
845 $this->getOutput()->addHTML(
846 $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
847 $grouplist .
848 $groupCheckboxes .
849 Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
850 "<tr>
851 <td class='mw-label'>" .
852 Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
853 "</td>
854 <td class='mw-input'>" .
855 Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason' ) ?? false, [
856 'id' => 'wpReason',
857 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
858 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
859 // Unicode codepoints.
860 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
861 ] ) .
862 "</td>
863 </tr>
864 <tr>
865 <td></td>
866 <td class='mw-submit'>" .
867 Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
868 [ 'name' => 'saveusergroups' ] +
869 Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
870 ) .
871 "</td>
872 </tr>
873 <tr>
874 <td></td>
875 <td class='mw-input'>" .
876 Xml::checkLabel( $this->msg( 'userrights-watchuser' )->text(), 'wpWatch', 'wpWatch' ) .
877 "</td>
878 </tr>" .
879 Xml::closeElement( 'table' ) . "\n"
880 );
881 } else {
882 $this->getOutput()->addHTML( $grouplist );
883 }
884 $this->getOutput()->addHTML(
885 Xml::closeElement( 'fieldset' ) .
886 Xml::closeElement( 'form' ) . "\n"
887 );
888 }
889
899 private function groupCheckboxes( $usergroups, $user ) {
900 $allgroups = $this->userGroupManager->listAllGroups();
901 $ret = '';
902
903 // Get the list of preset expiry times from the system message
904 $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage();
905 $expiryOptions = $expiryOptionsMsg->isDisabled()
906 ? []
907 : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() );
908
909 // Put all column info into an associative array so that extensions can
910 // more easily manage it.
911 $columns = [ 'unchangeable' => [], 'changeable' => [] ];
912
913 foreach ( $allgroups as $group ) {
914 $set = isset( $usergroups[$group] );
915 // Users who can add the group, but not remove it, can only lengthen
916 // expiries, not shorten them. So they should only see the expiry
917 // dropdown if the group currently has a finite expiry
918 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
919 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
920 // Should the checkbox be disabled?
921 $disabledCheckbox = !(
922 ( $set && $this->canRemove( $group ) ) ||
923 ( !$set && $this->canAdd( $group ) ) );
924 // Should the expiry elements be disabled?
925 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
926 // Do we need to point out that this action is irreversible?
927 $irreversible = !$disabledCheckbox && (
928 ( $set && !$this->canAdd( $group ) ) ||
929 ( !$set && !$this->canRemove( $group ) ) );
930
931 $checkbox = [
932 'set' => $set,
933 'disabled' => $disabledCheckbox,
934 'disabled-expiry' => $disabledExpiry,
935 'irreversible' => $irreversible
936 ];
937
938 if ( $disabledCheckbox && $disabledExpiry ) {
939 $columns['unchangeable'][$group] = $checkbox;
940 } else {
941 $columns['changeable'][$group] = $checkbox;
942 }
943 }
944
945 // Build the HTML table
946 $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
947 "<tr>\n";
948 foreach ( $columns as $name => $column ) {
949 if ( $column === [] ) {
950 continue;
951 }
952 // Messages: userrights-changeable-col, userrights-unchangeable-col
953 $ret .= Xml::element(
954 'th',
955 null,
956 $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
957 );
958 }
959
960 $ret .= "</tr>\n<tr>\n";
961 $uiLanguage = $this->getLanguage();
962 $userName = $user->getName();
963 foreach ( $columns as $column ) {
964 if ( $column === [] ) {
965 continue;
966 }
967 $ret .= "\t<td style='vertical-align:top;'>\n";
968 foreach ( $column as $group => $checkbox ) {
969 $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
970 if ( $checkbox['disabled'] ) {
971 $attr['disabled'] = 'disabled';
972 }
973
974 $member = $uiLanguage->getGroupMemberName( $group, $userName );
975 if ( $checkbox['irreversible'] ) {
976 $text = $this->msg( 'userrights-irreversible-marker', $member )->text();
977 } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
978 $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
979 } else {
980 $text = $member;
981 }
982 $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
983 "wpGroup-" . $group, $checkbox['set'], $attr );
984
985 if ( $this->canProcessExpiries() ) {
986 $uiUser = $this->getUser();
987
988 $currentExpiry = isset( $usergroups[$group] ) ?
989 $usergroups[$group]->getExpiry() :
990 null;
991
992 // If the user can't modify the expiry, print the current expiry below
993 // it in plain text. Otherwise provide UI to set/change the expiry
994 if ( $checkbox['set'] &&
995 ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
996 ) {
997 if ( $currentExpiry ) {
998 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
999 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
1000 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
1001 $expiryHtml = Xml::element( 'span', null,
1002 $this->msg( 'userrights-expiry-current' )->params(
1003 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
1004 } else {
1005 $expiryHtml = Xml::element( 'span', null,
1006 $this->msg( 'userrights-expiry-none' )->text() );
1007 }
1008 // T171345: Add a hidden form element so that other groups can still be manipulated,
1009 // otherwise saving errors out with an invalid expiry time for this group.
1010 $expiryHtml .= Html::hidden( "wpExpiry-$group",
1011 $currentExpiry ? 'existing' : 'infinite' );
1012 $expiryHtml .= "<br />\n";
1013 } else {
1014 $expiryHtml = Xml::element( 'span', null,
1015 $this->msg( 'userrights-expiry' )->text() );
1016 $expiryHtml .= Xml::openElement( 'span' );
1017
1018 // add a form element to set the expiry date
1019 $expiryFormOptions = new XmlSelect(
1020 "wpExpiry-$group",
1021 "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
1022 $currentExpiry ? 'existing' : 'infinite'
1023 );
1024 if ( $checkbox['disabled-expiry'] ) {
1025 $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
1026 }
1027
1028 if ( $currentExpiry ) {
1029 $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
1030 $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
1031 $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
1032 $existingExpiryMessage = $this->msg( 'userrights-expiry-existing',
1033 $timestamp, $d, $t );
1034 $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
1035 }
1036
1037 $expiryFormOptions->addOption(
1038 $this->msg( 'userrights-expiry-none' )->text(),
1039 'infinite'
1040 );
1041 $expiryFormOptions->addOption(
1042 $this->msg( 'userrights-expiry-othertime' )->text(),
1043 'other'
1044 );
1045
1046 $expiryFormOptions->addOptions( $expiryOptions );
1047
1048 // Add expiry dropdown
1049 $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
1050
1051 // Add custom expiry field
1052 $attribs = [
1053 'id' => "mw-input-wpExpiry-$group-other",
1054 'class' => 'mw-userrights-expiryfield',
1055 ];
1056 if ( $checkbox['disabled-expiry'] ) {
1057 $attribs['disabled'] = 'disabled';
1058 }
1059 $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
1060
1061 // If the user group is set but the checkbox is disabled, mimic a
1062 // checked checkbox in the form submission
1063 if ( $checkbox['set'] && $checkbox['disabled'] ) {
1064 $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
1065 }
1066
1067 $expiryHtml .= Xml::closeElement( 'span' );
1068 }
1069
1070 $divAttribs = [
1071 'id' => "mw-userrights-nested-wpGroup-$group",
1072 'class' => 'mw-userrights-nested',
1073 ];
1074 $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
1075 }
1076 $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
1077 ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
1078 : Xml::tags( 'div', [], $checkboxHtml )
1079 ) . "\n";
1080 }
1081 $ret .= "\t</td>\n";
1082 }
1083 $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
1084
1085 return [ $ret, (bool)$columns['changeable'] ];
1086 }
1087
1092 private function canRemove( $group ) {
1093 $groups = $this->changeableGroups();
1094
1095 return in_array(
1096 $group,
1097 $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
1098 );
1099 }
1100
1105 private function canAdd( $group ) {
1106 $groups = $this->changeableGroups();
1107
1108 return in_array(
1109 $group,
1110 $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
1111 );
1112 }
1113
1122 protected function changeableGroups() {
1123 return $this->userGroupManager->getGroupsChangeableBy( $this->getContext()->getAuthority() );
1124 }
1125
1134 private function getDisplayUsername( UserIdentity $user ) {
1135 $userName = $user->getName();
1136 if ( $user->getWikiId() !== UserIdentity::LOCAL ) {
1137 $userName .= $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter )
1138 . $user->getWikiId();
1139 }
1140 return $userName;
1141 }
1142
1149 protected function showLogFragment( $user, $output ) {
1150 $rightsLogPage = new LogPage( 'rights' );
1151 $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
1152 LogEventsList::showLogExtract( $output, 'rights',
1153 Title::makeTitle( NS_USER, $this->getDisplayUsername( $user ) ) );
1154 }
1155
1164 public function prefixSearchSubpages( $search, $limit, $offset ) {
1165 $search = $this->userNameUtils->getCanonical( $search );
1166 if ( !$search ) {
1167 // No prefix suggestion for invalid user
1168 return [];
1169 }
1170 // Autocomplete subpage as user list - public to allow caching
1171 return $this->userNamePrefixSearch
1172 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1173 }
1174
1175 protected function getGroupName() {
1176 return 'users';
1177 }
1178}
1179
1184class_alias( SpecialUserRights::class, 'UserrightsPage' );
const NS_USER
Definition Defines.php:66
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 URL path to a MediaWiki entry point.
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,...
Class to simplify the use of log pages.
Definition LogPage.php:43
Class for creating new log entries and inserting them into the database.
Handle database storage of comments such as edit summaries and log reasons.
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Some internal bits split of from Skin.php.
Definition Linker.php:65
A class containing constants representing the names of configuration variables.
const LocalDatabases
Name constant for the LocalDatabases setting, for use with Config::get()
const UserrightsInterwikiDelimiter
Name constant for the UserrightsInterwikiDelimiter setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
This is one of the Core classes and should be read at least once by any new developers.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Special page to allow managing user group membership.
execute( $par)
Manage forms to be shown according to posted data.
saveUserGroups( $reason, $user)
Save user groups changes in the database.
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.
null string $mTarget
The target of the local right-adjuster's interest.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
showEditUserGroupsForm( $user, $groups, $groupMemberships)
Show the form to edit group memberships.
userCanChangeRights(UserIdentity $targetUser, $checkIfSelf=true)
Check whether the current user (from context) can change the target user's rights.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
switchForm()
Output a form to allow searching for a user.
static expiryToTimestamp( $expiry)
Converts a user group membership expiry string into a timestamp.
__construct(UserGroupManagerFactory $userGroupManagerFactory=null, UserNameUtils $userNameUtils=null, UserNamePrefixSearch $userNamePrefixSearch=null, UserFactory $userFactory=null, ActorStoreFactory $actorStoreFactory=null, WatchlistManager $watchlistManager=null)
null UserIdentity $mFetchedUser
The user object of the target username or null.
doSaveUserGroups( $user, array $add, array $remove, $reason='', array $tags=[], array $groupExpiries=[])
Save user groups changes in the database.
fetchUser( $username, $writing=true)
Normalize the input username, which may be local or remote, and return a user identity object,...
canProcessExpiries()
Returns true if this user rights form can set and change user group expiries.
static serialiseUgmForLog( $ugm)
Serialise a UserGroupMembership object for storage in the log_params section of the logging table.
showLogFragment( $user, $output)
Show a rights log fragment for the specified user.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:58
Represents a title within MediaWiki.
Definition Title.php:76
Creates User objects.
Factory service for UserGroupManager instances.
addUserToGroup(UserIdentity $user, string $group, string $expiry=null, bool $allowUpdate=false)
Add the user to the given group.
getUserGroupMemberships(UserIdentity $user, int $queryFlags=self::READ_NORMAL)
Loads and returns UserGroupMembership objects for all the groups a user currently belongs to.
removeUserFromGroup(UserIdentity $user, string $group)
Remove the user from the given group.
getGroupsChangeableBy(Authority $authority)
Returns an array of groups that this $actor can add and remove.
getUserGroups(UserIdentity $user, int $queryFlags=self::READ_NORMAL)
Get the list of explicit group memberships this user has.
Represents a "user group membership" – a specific instance of a user belonging to a group.
Handles searching prefixes of user names.
UserNameUtils service.
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:31
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the user tries to do something whilst blocked.
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:28
setAttribute( $name, $value)
Definition XmlSelect.php:66
Module of static functions for generating XML.
Definition Xml.php:33
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:124
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:115
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition Xml.php:287
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition Xml.php:432
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition Xml.php:141
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:50
getWikiId()
Get the ID of the wiki this page belongs to.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...