MediaWiki REL1_40
SpecialUserRights.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
27use LogPage;
42use OutputPage;
44use SpecialPage;
45use Status;
46use User;
50use Xml;
51use XmlSelect;
52
65 protected $mTarget;
69 protected $mFetchedUser = null;
70 protected $isself = false;
71
73 private $userGroupManagerFactory;
74
76 private $userGroupManager = null;
77
79 private $userNameUtils;
80
82 private $userNamePrefixSearch;
83
85 private $userFactory;
86
93 public function __construct(
94 UserGroupManagerFactory $userGroupManagerFactory = null,
95 UserNameUtils $userNameUtils = null,
96 UserNamePrefixSearch $userNamePrefixSearch = null,
97 UserFactory $userFactory = 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 }
107
108 public function doesWrites() {
109 return true;
110 }
111
123 public function userCanChangeRights( UserIdentity $targetUser, $checkIfSelf = true ) {
124 $isself = $this->getUser()->equals( $targetUser );
125
126 $userGroupManager = $this->userGroupManagerFactory
127 ->getUserGroupManager( $targetUser->getWikiId() );
128 $available = $userGroupManager->getGroupsChangeableBy( $this->getAuthority() );
129 if ( $targetUser->getId() === 0 ) {
130 return false;
131 }
132
133 if ( $available['add'] || $available['remove'] ) {
134 // can change some rights for any user
135 return true;
136 }
137
138 if ( ( $available['add-self'] || $available['remove-self'] )
139 && ( $isself || !$checkIfSelf )
140 ) {
141 // can change some rights for self
142 return true;
143 }
144
145 return false;
146 }
147
155 public function execute( $par ) {
156 $user = $this->getUser();
157 $request = $this->getRequest();
158 $session = $request->getSession();
159 $out = $this->getOutput();
160
161 $out->addModules( [ 'mediawiki.special.userrights' ] );
162
163 $this->mTarget = $par ?? $request->getVal( 'user' );
164
165 if ( is_string( $this->mTarget ) ) {
166 $this->mTarget = trim( $this->mTarget );
167 }
168
169 if ( $this->mTarget !== null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->getName() ) {
170 $this->isself = true;
171 }
172
173 $fetchedStatus = $this->mTarget === null ? Status::newFatal( 'nouserspecified' ) :
174 $this->fetchUser( $this->mTarget, true );
175 if ( $fetchedStatus->isOK() ) {
176 $this->mFetchedUser = $fetchedUser = $fetchedStatus->value;
177 // Phan false positive on Status object - T323205
178 '@phan-var UserIdentity $fetchedUser';
179 $wikiId = $fetchedUser->getWikiId();
180 if ( $wikiId === UserIdentity::LOCAL ) {
181 // Set the 'relevant user' in the skin, so it displays links like Contributions,
182 // User logs, UserRights, etc.
183 $this->getSkin()->setRelevantUser( $this->mFetchedUser );
184 }
185 $this->userGroupManager = $this->userGroupManagerFactory
186 ->getUserGroupManager( $wikiId );
187 }
188
189 // show a successbox, if the user rights was saved successfully
190 if (
191 $session->get( 'specialUserrightsSaveSuccess' ) &&
192 $this->mFetchedUser !== null
193 ) {
194 // Remove session data for the success message
195 $session->remove( 'specialUserrightsSaveSuccess' );
196
197 $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
198 $out->addHTML(
199 Html::successBox(
200 Html::element(
201 'p',
202 [],
203 $this->msg( 'savedrights', $this->mFetchedUser->getName() )->text()
204 ),
205 'mw-notify-success'
206 )
207 );
208 }
209
210 $this->setHeaders();
211 $this->outputHeader();
212
213 $out->addModuleStyles( 'mediawiki.special' );
214 $this->addHelpLink( 'Help:Assigning permissions' );
215
216 $this->switchForm();
217
218 if (
219 $request->wasPosted() &&
220 $request->getCheck( 'saveusergroups' ) &&
221 $this->mTarget !== null &&
222 $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
223 ) {
224 /*
225 * If the user is blocked and they only have "partial" access
226 * (e.g. they don't have the userrights permission), then don't
227 * allow them to change any user rights.
228 */
229 if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) {
230 $block = $user->getBlock();
231 if ( $block && $block->isSitewide() ) {
232 throw new UserBlockedError(
233 $block,
234 $user,
235 $this->getLanguage(),
236 $request->getIP()
237 );
238 }
239 }
240
241 $this->checkReadOnly();
242
243 // save settings
244 if ( !$fetchedStatus->isOK() ) {
245 $this->getOutput()->addWikiTextAsInterface(
246 $fetchedStatus->getWikiText( false, false, $this->getLanguage() )
247 );
248
249 return;
250 }
251
252 $targetUser = $this->mFetchedUser;
253 $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' );
254 $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck );
255 $userGroups = $this->userGroupManager->getUserGroups( $targetUser, UserGroupManager::READ_LATEST );
256
257 if ( $userGroups !== $conflictCheck ) {
258 $out->addHTML( Html::errorBox(
259 $this->msg( 'userrights-conflict' )->parse()
260 ) );
261 } else {
262 $status = $this->saveUserGroups(
263 $this->mTarget,
264 $request->getVal( 'user-reason' ),
265 $targetUser
266 );
267
268 if ( $status->isOK() ) {
269 // Set session data for the success message
270 $session->set( 'specialUserrightsSaveSuccess', 1 );
271
272 $out->redirect( $this->getSuccessURL() );
273 return;
274 } else {
275 // Print an error message and redisplay the form
276 $out->wrapWikiTextAsInterface(
277 'error', $status->getWikiText( false, false, $this->getLanguage() )
278 );
279 }
280 }
281 }
282
283 // show some more forms
284 if ( $this->mTarget !== null ) {
285 $this->editUserGroupsForm( $this->mTarget );
286 }
287 }
288
289 private function getSuccessURL() {
290 return $this->getPageTitle( $this->mTarget )->getFullURL();
291 }
292
299 public function canProcessExpiries() {
300 return true;
301 }
302
312 public static function expiryToTimestamp( $expiry ) {
313 if ( wfIsInfinity( $expiry ) ) {
314 return null;
315 }
316
317 $unix = strtotime( $expiry );
318
319 if ( !$unix || $unix === -1 ) {
320 return false;
321 }
322
323 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
324 // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
325 return wfTimestamp( TS_MW, $unix );
326 }
327
337 protected function saveUserGroups( $username, $reason, $user ) {
338 $allgroups = $this->userGroupManager->listAllGroups();
339 $addgroup = [];
340 $groupExpiries = []; // associative array of (group name => expiry)
341 $removegroup = [];
342 $existingUGMs = $this->userGroupManager->getUserGroupMemberships( $user );
343
344 // This could possibly create a highly unlikely race condition if permissions are changed between
345 // when the form is loaded and when the form is saved. Ignoring it for the moment.
346 foreach ( $allgroups as $group ) {
347 // We'll tell it to remove all unchecked groups, and add all checked groups.
348 // Later on, this gets filtered for what can actually be removed
349 if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
350 $addgroup[] = $group;
351
352 if ( $this->canProcessExpiries() ) {
353 // read the expiry information from the request
354 $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
355 if ( $expiryDropdown === 'existing' ) {
356 continue;
357 }
358
359 if ( $expiryDropdown === 'other' ) {
360 $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
361 } else {
362 $expiryValue = $expiryDropdown;
363 }
364
365 // validate the expiry
366 $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue );
367
368 if ( $groupExpiries[$group] === false ) {
369 return Status::newFatal( 'userrights-invalid-expiry', $group );
370 }
371
372 // not allowed to have things expiring in the past
373 if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
374 return Status::newFatal( 'userrights-expiry-in-past', $group );
375 }
376
377 // if the user can only add this group (not remove it), the expiry time
378 // cannot be brought forward (T156784)
379 if ( !$this->canRemove( $group ) &&
380 isset( $existingUGMs[$group] ) &&
381 ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
382 ( $groupExpiries[$group] ?: 'infinity' )
383 ) {
384 return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
385 }
386 }
387 } else {
388 $removegroup[] = $group;
389 }
390 }
391
392 $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
393
394 return Status::newGood();
395 }
396
412 public function doSaveUserGroups( $user, array $add, array $remove, $reason = '',
413 array $tags = [], array $groupExpiries = []
414 ) {
415 // Validate input set...
416 $isself = $user->getName() == $this->getUser()->getName();
417 if ( $this->userGroupManager !== null ) {
418 // Used after form submit
419 $userGroupManager = $this->userGroupManager;
420 } else {
421 // Used as backend-function
422 $userGroupManager = $this->userGroupManagerFactory
423 ->getUserGroupManager( $user->getWikiId() );
424 }
425 $groups = $userGroupManager->getUserGroups( $user );
426 $ugms = $userGroupManager->getUserGroupMemberships( $user );
427 $changeable = $userGroupManager->getGroupsChangeableBy( $this->getAuthority() );
428 $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
429 $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
430
431 $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
432 $add = array_intersect( $add, $addable );
433
434 // add only groups that are not already present or that need their expiry updated,
435 // UNLESS the user can only add this group (not remove it) and the expiry time
436 // is being brought forward (T156784)
437 $add = array_filter( $add,
438 static function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
439 if ( isset( $groupExpiries[$group] ) &&
440 !in_array( $group, $removable ) &&
441 isset( $ugms[$group] ) &&
442 ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
443 ( $groupExpiries[$group] ?: 'infinity' )
444 ) {
445 return false;
446 }
447 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
448 } );
449
450 $this->getHookRunner()->onChangeUserGroups( $this->getUser(), $user, $add, $remove );
451
452 $oldGroups = $groups;
453 $oldUGMs = $userGroupManager->getUserGroupMemberships( $user );
454 $newGroups = $oldGroups;
455
456 // Remove groups, then add new ones/update expiries of existing ones
457 if ( $remove ) {
458 foreach ( $remove as $index => $group ) {
459 if ( !$userGroupManager->removeUserFromGroup( $user, $group ) ) {
460 unset( $remove[$index] );
461 }
462 }
463 $newGroups = array_diff( $newGroups, $remove );
464 }
465 if ( $add ) {
466 foreach ( $add as $index => $group ) {
467 $expiry = $groupExpiries[$group] ?? null;
468 if ( !$userGroupManager->addUserToGroup( $user, $group, $expiry, true ) ) {
469 unset( $add[$index] );
470 }
471 }
472 $newGroups = array_merge( $newGroups, $add );
473 }
474 $newGroups = array_unique( $newGroups );
475 $newUGMs = $userGroupManager->getUserGroupMemberships( $user );
476
477 // Ensure that caches are cleared
478 $user->invalidateCache();
479
480 // update groups in external authentication database
481 $this->getHookRunner()->onUserGroupsChanged( $user, $add, $remove,
482 $this->getUser(), $reason, $oldUGMs, $newUGMs );
483
484 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
485 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
486 wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) );
487 wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) );
488
489 // Only add a log entry if something actually changed
490 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
491 $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
492 }
493
494 return [ $add, $remove ];
495 }
496
504 protected static function serialiseUgmForLog( $ugm ) {
505 if ( !$ugm instanceof UserGroupMembership ) {
506 return null;
507 }
508 return [ 'expiry' => $ugm->getExpiry() ];
509 }
510
521 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
522 array $tags, array $oldUGMs, array $newUGMs
523 ) {
524 // make sure $oldUGMs and $newUGMs are in the same order, and serialise
525 // each UGM object to a simplified array
526 $oldUGMs = array_map( function ( $group ) use ( $oldUGMs ) {
527 return isset( $oldUGMs[$group] ) ?
528 self::serialiseUgmForLog( $oldUGMs[$group] ) :
529 null;
530 }, $oldGroups );
531 $newUGMs = array_map( function ( $group ) use ( $newUGMs ) {
532 return isset( $newUGMs[$group] ) ?
533 self::serialiseUgmForLog( $newUGMs[$group] ) :
534 null;
535 }, $newGroups );
536
537 $logEntry = new ManualLogEntry( 'rights', 'rights' );
538 $logEntry->setPerformer( $this->getUser() );
539 $logEntry->setTarget( Title::makeTitle( NS_USER, $user->getName() ) );
540 $logEntry->setComment( is_string( $reason ) ? $reason : "" );
541 $logEntry->setParameters( [
542 '4::oldgroups' => $oldGroups,
543 '5::newgroups' => $newGroups,
544 'oldmetadata' => $oldUGMs,
545 'newmetadata' => $newUGMs,
546 ] );
547 $logid = $logEntry->insert();
548 if ( count( $tags ) ) {
549 $logEntry->addTags( $tags );
550 }
551 $logEntry->publish( $logid );
552 }
553
558 private function editUserGroupsForm( $username ) {
559 $status = $this->fetchUser( $username, true );
560 if ( !$status->isOK() ) {
561 $this->getOutput()->addWikiTextAsInterface(
562 $status->getWikiText( false, false, $this->getLanguage() )
563 );
564
565 return;
566 }
567
569 $user = $status->value;
570 '@phan-var UserIdentity $user';
571
572 $groups = $this->userGroupManager->getUserGroups( $user );
573 $groupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
574 $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
575
576 // This isn't really ideal logging behavior, but let's not hide the
577 // interwiki logs if we're using them as is.
578 $this->showLogFragment( $user, $this->getOutput() );
579 }
580
590 public function fetchUser( $username, $writing = true ) {
591 $parts = explode( $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter ),
592 $username );
593 if ( count( $parts ) < 2 ) {
594 $name = trim( $username );
595 $dbDomain = '';
596 } else {
597 [ $name, $dbDomain ] = array_map( 'trim', $parts );
598
599 if ( WikiMap::isCurrentWikiId( $dbDomain ) ) {
600 $dbDomain = '';
601 } else {
602 if ( $writing &&
603 !$this->getAuthority()->isAllowed( 'userrights-interwiki' )
604 ) {
605 return Status::newFatal( 'userrights-no-interwiki' );
606 }
607 if ( !UserRightsProxy::validDatabase( $dbDomain ) ) {
608 return Status::newFatal( 'userrights-nodatabase', $dbDomain );
609 }
610 }
611 }
612
613 if ( $name === '' ) {
614 return Status::newFatal( 'nouserspecified' );
615 }
616
617 if ( $name[0] == '#' ) {
618 // Numeric ID can be specified...
619 // We'll do a lookup for the name internally.
620 $id = intval( substr( $name, 1 ) );
621
622 if ( $dbDomain == '' ) {
623 $name = User::whoIs( $id );
624 } else {
625 $name = UserRightsProxy::whoIs( $dbDomain, $id );
626 }
627
628 if ( !$name ) {
629 return Status::newFatal( 'noname' );
630 }
631 } else {
632 $name = $this->userNameUtils->getCanonical( $name );
633 if ( $name === false ) {
634 // invalid name
635 return Status::newFatal( 'nosuchusershort', $username );
636 }
637 }
638
639 if ( $dbDomain == '' ) {
640 $user = $this->userFactory->newFromName( $name );
641 } else {
642 $user = UserRightsProxy::newFromName( $dbDomain, $name );
643 }
644
645 if ( !$user || $user->isAnon() ) {
646 return Status::newFatal( 'nosuchusershort', $username );
647 }
648
649 if ( $user->getWikiId() === UserIdentity::LOCAL &&
650 $this->userFactory->newFromUserIdentity( $user )->isHidden() &&
651 !$this->getAuthority()->isAllowed( 'hideuser' )
652 ) {
653 // Cannot see hidden users, pretend they don't exist
654 return Status::newFatal( 'nosuchusershort', $username );
655 }
656
657 return Status::newGood( $user );
658 }
659
667 public function makeGroupNameList( $ids ) {
668 if ( empty( $ids ) ) {
669 return $this->msg( 'rightsnone' )->inContentLanguage()->text();
670 } else {
671 return implode( ', ', $ids );
672 }
673 }
674
678 protected function switchForm() {
679 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
680
681 $this->getOutput()->addHTML(
682 Html::openElement(
683 'form',
684 [
685 'method' => 'get',
686 'action' => wfScript(),
687 'name' => 'uluser',
688 'id' => 'mw-userrights-form1'
689 ]
690 ) .
691 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
692 Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
693 Xml::inputLabel(
694 $this->msg( 'userrights-user-editname' )->text(),
695 'user',
696 'username',
697 30,
698 $this->mTarget ? str_replace( '_', ' ', $this->mTarget ) : '',
699 [
700 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
701 ] + (
702 // Set autofocus on blank input and error input
703 $this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
704 )
705 ) . ' ' .
706 Xml::submitButton(
707 $this->msg( 'editusergroup' )->text()
708 ) .
709 Html::closeElement( 'fieldset' ) .
710 Html::closeElement( 'form' ) . "\n"
711 );
712 }
713
723 protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) {
724 $list = $membersList = $tempList = $tempMembersList = [];
725 foreach ( $groupMemberships as $ugm ) {
726 $linkG = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html' );
727 $linkM = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html',
728 $user->getName() );
729 if ( $ugm->getExpiry() ) {
730 $tempList[] = $linkG;
731 $tempMembersList[] = $linkM;
732 } else {
733 $list[] = $linkG;
734 $membersList[] = $linkM;
735
736 }
737 }
738
739 $autoList = [];
740 $autoMembersList = [];
741
742 if ( $user->getWikiId() === UserIdentity::LOCAL ) {
743 // Listing autopromote groups works only on the local wiki
744 foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) {
745 $autoList[] = UserGroupMembership::getLink( $group, $this->getContext(), 'html' );
746 $autoMembersList[] = UserGroupMembership::getLink( $group, $this->getContext(),
747 'html', $user->getName() );
748 }
749 }
750
751 $language = $this->getLanguage();
752 $displayedList = $this->msg( 'userrights-groupsmember-type' )
753 ->rawParams(
754 $language->commaList( array_merge( $tempList, $list ) ),
755 $language->commaList( array_merge( $tempMembersList, $membersList ) )
756 )->escaped();
757 $displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
758 ->rawParams(
759 $language->commaList( $autoList ),
760 $language->commaList( $autoMembersList )
761 )->escaped();
762
763 $grouplist = '';
764 $count = count( $list ) + count( $tempList );
765 if ( $count > 0 ) {
766 $grouplist = $this->msg( 'userrights-groupsmember' )
767 ->numParams( $count )
768 ->params( $user->getName() )
769 ->parse();
770 $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
771 }
772
773 $count = count( $autoList );
774 if ( $count > 0 ) {
775 $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
776 ->numParams( $count )
777 ->params( $user->getName() )
778 ->parse();
779 $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
780 }
781
782 $systemUser = $user->getWikiId() === UserIdentity::LOCAL
783 && $this->userFactory->newFromUserIdentity( $user )->isSystemUser();
784 if ( $systemUser ) {
785 $systemusernote = $this->msg( 'userrights-systemuser' )
786 ->params( $user->getName() )
787 ->parse();
788 $grouplist .= '<p>' . $systemusernote . "</p>\n";
789 }
790
791 // Only add an email link if the user is not a system user
792 $flags = $systemUser ? 0 : Linker::TOOL_LINKS_EMAIL;
793 $userToolLinks = Linker::userToolLinks(
794 $user->getId(),
795 $user->getName(),
796 false, /* default for redContribsWhenNoEdits */
797 $flags
798 );
799
800 [ $groupCheckboxes, $canChangeAny ] =
801 $this->groupCheckboxes( $groupMemberships, $user );
802 $this->getOutput()->addHTML(
803 Xml::openElement(
804 'form',
805 [
806 'method' => 'post',
807 'action' => $this->getPageTitle()->getLocalURL(),
808 'name' => 'editGroup',
809 'id' => 'mw-userrights-form2'
810 ]
811 ) .
812 Html::hidden( 'user', $this->mTarget ) .
813 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
814 Html::hidden(
815 'conflictcheck-originalgroups',
816 implode( ',', $this->userGroupManager->getUserGroups( $user ) )
817 ) . // Conflict detection
818 Xml::openElement( 'fieldset' ) .
819 Xml::element(
820 'legend',
821 [],
822 $this->msg(
823 $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
824 $user->getName()
825 )->text()
826 ) .
827 $this->msg(
828 $canChangeAny ? 'editinguser' : 'viewinguserrights'
829 )->params( wfEscapeWikiText( $user->getName() ) )
830 ->rawParams( $userToolLinks )->parse()
831 );
832 if ( $canChangeAny ) {
833 $this->getOutput()->addHTML(
834 $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
835 $grouplist .
836 $groupCheckboxes .
837 Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
838 "<tr>
839 <td class='mw-label'>" .
840 Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
841 "</td>
842 <td class='mw-input'>" .
843 Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason' ) ?? false, [
844 'id' => 'wpReason',
845 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
846 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
847 // Unicode codepoints.
848 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
849 ] ) .
850 "</td>
851 </tr>
852 <tr>
853 <td></td>
854 <td class='mw-submit'>" .
855 Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
856 [ 'name' => 'saveusergroups' ] +
857 Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
858 ) .
859 "</td>
860 </tr>" .
861 Xml::closeElement( 'table' ) . "\n"
862 );
863 } else {
864 $this->getOutput()->addHTML( $grouplist );
865 }
866 $this->getOutput()->addHTML(
867 Xml::closeElement( 'fieldset' ) .
868 Xml::closeElement( 'form' ) . "\n"
869 );
870 }
871
881 private function groupCheckboxes( $usergroups, $user ) {
882 $allgroups = $this->userGroupManager->listAllGroups();
883 $ret = '';
884
885 // Get the list of preset expiry times from the system message
886 $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage();
887 $expiryOptions = $expiryOptionsMsg->isDisabled()
888 ? []
889 : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() );
890
891 // Put all column info into an associative array so that extensions can
892 // more easily manage it.
893 $columns = [ 'unchangeable' => [], 'changeable' => [] ];
894
895 foreach ( $allgroups as $group ) {
896 $set = isset( $usergroups[$group] );
897 // Users who can add the group, but not remove it, can only lengthen
898 // expiries, not shorten them. So they should only see the expiry
899 // dropdown if the group currently has a finite expiry
900 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
901 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
902 // Should the checkbox be disabled?
903 $disabledCheckbox = !(
904 ( $set && $this->canRemove( $group ) ) ||
905 ( !$set && $this->canAdd( $group ) ) );
906 // Should the expiry elements be disabled?
907 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
908 // Do we need to point out that this action is irreversible?
909 $irreversible = !$disabledCheckbox && (
910 ( $set && !$this->canAdd( $group ) ) ||
911 ( !$set && !$this->canRemove( $group ) ) );
912
913 $checkbox = [
914 'set' => $set,
915 'disabled' => $disabledCheckbox,
916 'disabled-expiry' => $disabledExpiry,
917 'irreversible' => $irreversible
918 ];
919
920 if ( $disabledCheckbox && $disabledExpiry ) {
921 $columns['unchangeable'][$group] = $checkbox;
922 } else {
923 $columns['changeable'][$group] = $checkbox;
924 }
925 }
926
927 // Build the HTML table
928 $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
929 "<tr>\n";
930 foreach ( $columns as $name => $column ) {
931 if ( $column === [] ) {
932 continue;
933 }
934 // Messages: userrights-changeable-col, userrights-unchangeable-col
935 $ret .= Xml::element(
936 'th',
937 null,
938 $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
939 );
940 }
941
942 $ret .= "</tr>\n<tr>\n";
943 $uiLanguage = $this->getLanguage();
944 $userName = $user->getName();
945 foreach ( $columns as $column ) {
946 if ( $column === [] ) {
947 continue;
948 }
949 $ret .= "\t<td style='vertical-align:top;'>\n";
950 foreach ( $column as $group => $checkbox ) {
951 $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
952 if ( $checkbox['disabled'] ) {
953 $attr['disabled'] = 'disabled';
954 }
955
956 $member = $uiLanguage->getGroupMemberName( $group, $userName );
957 if ( $checkbox['irreversible'] ) {
958 $text = $this->msg( 'userrights-irreversible-marker', $member )->text();
959 } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
960 $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
961 } else {
962 $text = $member;
963 }
964 $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
965 "wpGroup-" . $group, $checkbox['set'], $attr );
966
967 if ( $this->canProcessExpiries() ) {
968 $uiUser = $this->getUser();
969
970 $currentExpiry = isset( $usergroups[$group] ) ?
971 $usergroups[$group]->getExpiry() :
972 null;
973
974 // If the user can't modify the expiry, print the current expiry below
975 // it in plain text. Otherwise provide UI to set/change the expiry
976 if ( $checkbox['set'] &&
977 ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
978 ) {
979 if ( $currentExpiry ) {
980 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
981 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
982 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
983 $expiryHtml = Xml::element( 'span', null,
984 $this->msg( 'userrights-expiry-current' )->params(
985 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
986 } else {
987 $expiryHtml = Xml::element( 'span', null,
988 $this->msg( 'userrights-expiry-none' )->text() );
989 }
990 // T171345: Add a hidden form element so that other groups can still be manipulated,
991 // otherwise saving errors out with an invalid expiry time for this group.
992 $expiryHtml .= Html::hidden( "wpExpiry-$group",
993 $currentExpiry ? 'existing' : 'infinite' );
994 $expiryHtml .= "<br />\n";
995 } else {
996 $expiryHtml = Xml::element( 'span', null,
997 $this->msg( 'userrights-expiry' )->text() );
998 $expiryHtml .= Xml::openElement( 'span' );
999
1000 // add a form element to set the expiry date
1001 $expiryFormOptions = new XmlSelect(
1002 "wpExpiry-$group",
1003 "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
1004 $currentExpiry ? 'existing' : 'infinite'
1005 );
1006 if ( $checkbox['disabled-expiry'] ) {
1007 $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
1008 }
1009
1010 if ( $currentExpiry ) {
1011 $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
1012 $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
1013 $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
1014 $existingExpiryMessage = $this->msg( 'userrights-expiry-existing',
1015 $timestamp, $d, $t );
1016 $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
1017 }
1018
1019 $expiryFormOptions->addOption(
1020 $this->msg( 'userrights-expiry-none' )->text(),
1021 'infinite'
1022 );
1023 $expiryFormOptions->addOption(
1024 $this->msg( 'userrights-expiry-othertime' )->text(),
1025 'other'
1026 );
1027
1028 $expiryFormOptions->addOptions( $expiryOptions );
1029
1030 // Add expiry dropdown
1031 $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
1032
1033 // Add custom expiry field
1034 $attribs = [
1035 'id' => "mw-input-wpExpiry-$group-other",
1036 'class' => 'mw-userrights-expiryfield',
1037 ];
1038 if ( $checkbox['disabled-expiry'] ) {
1039 $attribs['disabled'] = 'disabled';
1040 }
1041 $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
1042
1043 // If the user group is set but the checkbox is disabled, mimic a
1044 // checked checkbox in the form submission
1045 if ( $checkbox['set'] && $checkbox['disabled'] ) {
1046 $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
1047 }
1048
1049 $expiryHtml .= Xml::closeElement( 'span' );
1050 }
1051
1052 $divAttribs = [
1053 'id' => "mw-userrights-nested-wpGroup-$group",
1054 'class' => 'mw-userrights-nested',
1055 ];
1056 $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
1057 }
1058 $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
1059 ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
1060 : Xml::tags( 'div', [], $checkboxHtml )
1061 ) . "\n";
1062 }
1063 $ret .= "\t</td>\n";
1064 }
1065 $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
1066
1067 return [ $ret, (bool)$columns['changeable'] ];
1068 }
1069
1074 private function canRemove( $group ) {
1075 $groups = $this->changeableGroups();
1076
1077 return in_array(
1078 $group,
1079 $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
1080 );
1081 }
1082
1087 private function canAdd( $group ) {
1088 $groups = $this->changeableGroups();
1089
1090 return in_array(
1091 $group,
1092 $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
1093 );
1094 }
1095
1104 protected function changeableGroups() {
1105 return $this->userGroupManager->getGroupsChangeableBy( $this->getContext()->getAuthority() );
1106 }
1107
1114 protected function showLogFragment( $user, $output ) {
1115 $rightsLogPage = new LogPage( 'rights' );
1116 $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
1117 LogEventsList::showLogExtract( $output, 'rights', Title::makeTitle( NS_USER, $user->getName() ) );
1118 }
1119
1128 public function prefixSearchSubpages( $search, $limit, $offset ) {
1129 $search = $this->userNameUtils->getCanonical( $search );
1130 if ( !$search ) {
1131 // No prefix suggestion for invalid user
1132 return [];
1133 }
1134 // Autocomplete subpage as user list - public to allow caching
1135 return $this->userNamePrefixSearch
1136 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1137 }
1138
1139 protected function getGroupName() {
1140 return 'users';
1141 }
1142}
1143
1148class_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 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,...
Class to simplify the use of log pages.
Definition LogPage.php:41
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:55
Some internal bits split of from Skin.php.
Definition Linker.php:67
A class containing constants representing the names of configuration variables.
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.
Special page to allow managing user group membership.
execute( $par)
Manage forms to be shown according to posted data.
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.
saveUserGroups( $username, $reason, $user)
Save user groups changes in the database.
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.
null UserIdentity $mFetchedUser
The user object of the target username or null.
__construct(UserGroupManagerFactory $userGroupManagerFactory=null, UserNameUtils $userNameUtils=null, UserNamePrefixSearch $userNamePrefixSearch=null, UserFactory $userFactory=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 (or proxy) object for m...
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.
makeTitle( $ns, $title, $fragment='', $interwiki='')
Represents a title within MediaWiki.
Definition Title.php:82
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.
Handles searching prefixes of user names.
UserNameUtils service.
Helper tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:33
This is one of the Core classes and should be read at least once by any new developers.
Show an error when a user tries to do something they do not have the necessary permissions for.
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:46
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.
Cut-down copy of User interface for local-interwiki-database user rights manipulation.
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:71
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:902
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:31
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:122
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:113
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition Xml.php:281
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition Xml.php:426
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition Xml.php:135
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition Xml.php:44
getWikiId()
Get the ID of the wiki this page belongs to.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)