24 namespace MediaWiki\Specials;
75 private $userGroupManager =
null;
99 parent::__construct(
'Userrights' );
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();
128 $userGroupManager = $this->userGroupManagerFactory
129 ->getUserGroupManager( $targetUser->
getWikiId() );
135 if ( $available[
'add'] || $available[
'remove'] ) {
140 if ( ( $available[
'add-self'] || $available[
'remove-self'] )
141 && (
$isself || !$checkIfSelf )
160 $session = $request->getSession();
163 $out->addModules( [
'mediawiki.special.userrights' ] );
165 $this->mTarget = $par ?? $request->getVal(
'user' );
167 if ( is_string( $this->mTarget ) ) {
168 $this->mTarget = trim( $this->mTarget );
171 if ( $this->mTarget !==
null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->
getName() ) {
172 $this->isself =
true;
175 $fetchedStatus = $this->mTarget ===
null ?
Status::newFatal(
'nouserspecified' ) :
176 $this->
fetchUser( $this->mTarget,
true );
177 if ( $fetchedStatus->isOK() ) {
178 $this->mFetchedUser = $fetchedUser = $fetchedStatus->value;
180 '@phan-var UserIdentity $fetchedUser';
181 $wikiId = $fetchedUser->getWikiId();
182 if ( $wikiId === UserIdentity::LOCAL ) {
185 $this->
getSkin()->setRelevantUser( $this->mFetchedUser );
187 $this->userGroupManager = $this->userGroupManagerFactory
188 ->getUserGroupManager( $wikiId );
193 $session->get(
'specialUserrightsSaveSuccess' ) &&
194 $this->mFetchedUser !==
null
197 $session->remove(
'specialUserrightsSaveSuccess' );
199 $out->addModuleStyles(
'mediawiki.notification.convertmessagebox.styles' );
205 $this->
msg(
'savedrights', $this->getDisplayUsername( $this->mFetchedUser ) )->text()
215 $out->addModuleStyles(
'mediawiki.special' );
216 $this->
addHelpLink(
'Help:Assigning permissions' );
221 $request->wasPosted() &&
222 $request->getCheck(
'saveusergroups' ) &&
223 $this->mTarget !==
null &&
224 $user->matchEditToken( $request->getVal(
'wpEditToken' ), $this->mTarget )
231 if ( !$this->
getAuthority()->isAllowed(
'userrights' ) ) {
232 $block = $user->getBlock();
233 if ( $block && $block->isSitewide() ) {
246 if ( !$fetchedStatus->isOK() ) {
247 $this->
getOutput()->addWikiTextAsInterface(
248 $fetchedStatus->getWikiText(
false,
false, $this->getLanguage() )
255 $conflictCheck = $request->getVal(
'conflictcheck-originalgroups' );
256 $conflictCheck = ( $conflictCheck ===
'' ) ? [] : explode(
',', $conflictCheck );
257 $userGroups = $this->userGroupManager->getUserGroups( $targetUser, UserGroupManager::READ_LATEST );
259 if ( $userGroups !== $conflictCheck ) {
261 $this->
msg(
'userrights-conflict' )->parse()
265 $request->getVal(
'user-reason' ),
269 if ( $status->isOK() ) {
271 $session->set(
'specialUserrightsSaveSuccess', 1 );
273 $out->redirect( $this->getSuccessURL() );
277 $out->wrapWikiTextAsInterface(
278 'error', $status->getWikiText(
false,
false, $this->getLanguage() )
285 if ( $this->mTarget !==
null ) {
286 $this->editUserGroupsForm( $this->mTarget );
290 private function getSuccessURL() {
291 return $this->
getPageTitle( $this->mTarget )->getFullURL();
318 $unix = strtotime( $expiry );
320 if ( !$unix || $unix === -1 ) {
338 if ( $this->userNameUtils->isTemp( $user->
getName() ) ) {
341 $allgroups = $this->userGroupManager->listAllGroups();
345 $existingUGMs = $this->userGroupManager->getUserGroupMemberships( $user );
349 foreach ( $allgroups as $group ) {
352 if ( $this->
getRequest()->getCheck(
"wpGroup-$group" ) ) {
353 $addgroup[] = $group;
357 $expiryDropdown = $this->
getRequest()->getVal(
"wpExpiry-$group" );
358 if ( $expiryDropdown ===
'existing' ) {
362 if ( $expiryDropdown ===
'other' ) {
363 $expiryValue = $this->
getRequest()->getVal(
"wpExpiry-$group-other" );
365 $expiryValue = $expiryDropdown;
371 if ( $groupExpiries[$group] ===
false ) {
376 if ( $groupExpiries[$group] && $groupExpiries[$group] <
wfTimestampNow() ) {
382 if ( !$this->canRemove( $group ) &&
383 isset( $existingUGMs[$group] ) &&
384 ( $existingUGMs[$group]->getExpiry() ?:
'infinity' ) >
385 ( $groupExpiries[$group] ?:
'infinity' )
391 $removegroup[] = $group;
395 $this->
doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
397 if ( $user->
getWikiId() === UserIdentity::LOCAL && $this->getRequest()->getCheck(
'wpWatch' ) ) {
398 $this->watchlistManager->addWatchIgnoringRights(
423 array $tags = [], array $groupExpiries = []
427 if ( $this->userGroupManager !==
null ) {
429 $userGroupManager = $this->userGroupManager;
432 $userGroupManager = $this->userGroupManagerFactory
433 ->getUserGroupManager( $user->
getWikiId() );
438 $addable = array_merge( $changeable[
'add'],
$isself ? $changeable[
'add-self'] : [] );
439 $removable = array_merge( $changeable[
'remove'],
$isself ? $changeable[
'remove-self'] : [] );
441 $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
442 $add = array_intersect( $add, $addable );
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' )
457 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
460 if ( $user->
getWikiId() === UserIdentity::LOCAL ) {
462 $hookUser = $this->userFactory->newFromUserIdentity( $user );
469 $oldGroups = $groups;
471 $newGroups = $oldGroups;
475 foreach ( $remove as $index => $group ) {
477 unset( $remove[$index] );
480 $newGroups = array_diff( $newGroups, $remove );
483 foreach ( $add as $index => $group ) {
484 $expiry = $groupExpiries[$group] ??
null;
485 if ( !$userGroupManager->
addUserToGroup( $user, $group, $expiry,
true ) ) {
486 unset( $add[$index] );
489 $newGroups = array_merge( $newGroups, $add );
491 $newGroups = array_unique( $newGroups );
495 $this->userFactory->invalidateCache( $user );
498 $this->
getHookRunner()->onUserGroupsChanged( $hookUser, $add, $remove,
499 $this->
getUser(), $reason, $oldUGMs, $newUGMs );
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 ) );
507 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
508 $this->
addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
511 return [ $add, $remove ];
525 return [
'expiry' => $ugm->getExpiry() ];
538 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
539 array $tags, array $oldUGMs, array $newUGMs
543 $oldUGMs = array_map(
static function ( $group ) use ( $oldUGMs ) {
544 return isset( $oldUGMs[$group] ) ?
548 $newUGMs = array_map(
static function ( $group ) use ( $newUGMs ) {
549 return isset( $newUGMs[$group] ) ?
555 $logEntry->setPerformer( $this->
getUser() );
557 $logEntry->setComment( is_string( $reason ) ? $reason :
"" );
558 $logEntry->setParameters( [
559 '4::oldgroups' => $oldGroups,
560 '5::newgroups' => $newGroups,
561 'oldmetadata' => $oldUGMs,
562 'newmetadata' => $newUGMs,
564 $logid = $logEntry->insert();
565 if ( count( $tags ) ) {
566 $logEntry->addTags( $tags );
568 $logEntry->publish( $logid );
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() )
586 $user = $status->value;
587 '@phan-var UserIdentity $user';
589 $groups = $this->userGroupManager->getUserGroups( $user );
590 $groupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
607 public function fetchUser( $username, $writing =
true ) {
610 if ( count( $parts ) < 2 ) {
611 $name = trim( $username );
612 $wikiId = UserIdentity::LOCAL;
614 [ $name, $wikiId ] = array_map(
'trim', $parts );
617 $wikiId = UserIdentity::LOCAL;
620 !$this->
getAuthority()->isAllowed(
'userrights-interwiki' )
625 if ( !in_array( $wikiId, $localDatabases ) ) {
631 if ( $name ===
'' ) {
635 $userIdentityLookup = $this->actorStoreFactory->getUserIdentityLookup( $wikiId );
636 if ( $name[0] ==
'#' ) {
638 $id = intval( substr( $name, 1 ) );
640 $user = $userIdentityLookup->getUserIdentityByUserId( $id );
647 $name = $this->userNameUtils->getCanonical( $name );
648 if ( $name ===
false ) {
652 $user = $userIdentityLookup->getUserIdentityByName( $name );
655 if ( $this->userNameUtils->isTemp( $name ) ) {
663 if ( $user->
getWikiId() === UserIdentity::LOCAL &&
664 $this->userFactory->newFromUserIdentity( $user )->isHidden() &&
665 !$this->getAuthority()->isAllowed(
'hideuser' )
683 return $this->
msg(
'rightsnone' )->inContentLanguage()->text();
685 return implode(
', ', $ids );
693 $this->
getOutput()->addModules(
'mediawiki.userSuggest' );
702 'id' =>
'mw-userrights-form1'
708 $this->
msg(
'userrights-user-editname' )->text(),
712 $this->mTarget ? str_replace(
'_',
' ', $this->mTarget ) :
'',
714 'class' =>
'mw-autocomplete-user',
717 $this->mFetchedUser ===
null ? [
'autofocus' =>
'' ] : []
721 $this->
msg(
'editusergroup' )->text()
738 $list = $membersList = $tempList = $tempMembersList = [];
739 foreach ( $groupMemberships as $ugm ) {
742 if ( $ugm->getExpiry() ) {
743 $tempList[] = $linkG;
744 $tempMembersList[] = $linkM;
747 $membersList[] = $linkM;
753 $autoMembersList = [];
755 if ( $user->
getWikiId() === UserIdentity::LOCAL ) {
757 foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) {
764 $displayedList = $this->
msg(
'userrights-groupsmember-type' )
766 $language->commaList( array_merge( $tempList, $list ) ),
767 $language->commaList( array_merge( $tempMembersList, $membersList ) )
769 $displayedAutolist = $this->
msg(
'userrights-groupsmember-type' )
771 $language->commaList( $autoList ),
772 $language->commaList( $autoMembersList )
776 $count = count( $list ) + count( $tempList );
778 $grouplist = $this->
msg(
'userrights-groupsmember' )
779 ->numParams( $count )
782 $grouplist =
'<p>' . $grouplist .
' ' . $displayedList .
"</p>\n";
785 $count = count( $autoList );
787 $autogrouplistintro = $this->
msg(
'userrights-groupsmember-auto' )
788 ->numParams( $count )
791 $grouplist .=
'<p>' . $autogrouplistintro .
' ' . $displayedAutolist .
"</p>\n";
794 $systemUser = $user->
getWikiId() === UserIdentity::LOCAL
795 && $this->userFactory->newFromUserIdentity( $user )->isSystemUser();
797 $systemusernote = $this->
msg(
'userrights-systemuser' )
800 $grouplist .=
'<p>' . $systemusernote .
"</p>\n";
807 $this->getDisplayUsername( $user ),
812 [ $groupCheckboxes, $canChangeAny ] =
813 $this->groupCheckboxes( $groupMemberships, $user );
820 'name' =>
'editGroup',
821 'id' =>
'mw-userrights-form2'
827 'conflictcheck-originalgroups',
828 implode(
',', $this->userGroupManager->getUserGroups( $user ) )
835 $canChangeAny ?
'userrights-editusergroup' :
'userrights-viewusergroup',
840 $canChangeAny ?
'editinguser' :
'viewinguserrights'
842 ->rawParams( $userToolLinks )->parse()
844 if ( $canChangeAny ) {
846 $this->
msg(
'userrights-groups-help', $user->
getName() )->parse() .
851 <td class='mw-label'>" .
852 Xml::label( $this->msg(
'userrights-reason' )->text(),
'wpReason' ) .
854 <td class='mw-input'>" .
855 Xml::input(
'user-reason', 60, $this->getRequest()->getVal(
'user-reason' ) ??
false, [
866 <td class='mw-submit'>" .
868 [
'name' =>
'saveusergroups' ] +
875 <td class='mw-input'>" .
876 Xml::checkLabel( $this->msg(
'userrights-watchuser' )->text(),
'wpWatch',
'wpWatch' ) .
882 $this->
getOutput()->addHTML( $grouplist );
899 private function groupCheckboxes( $usergroups, $user ) {
900 $allgroups = $this->userGroupManager->listAllGroups();
904 $expiryOptionsMsg = $this->
msg(
'userrights-expiry-options' )->inContentLanguage();
905 $expiryOptions = $expiryOptionsMsg->isDisabled()
911 $columns = [
'unchangeable' => [],
'changeable' => [] ];
913 foreach ( $allgroups as $group ) {
914 $set = isset( $usergroups[$group] );
918 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
919 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
921 $disabledCheckbox = !(
922 ( $set && $this->canRemove( $group ) ) ||
923 ( !$set && $this->canAdd( $group ) ) );
925 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
927 $irreversible = !$disabledCheckbox && (
928 ( $set && !$this->canAdd( $group ) ) ||
929 ( !$set && !$this->canRemove( $group ) ) );
933 'disabled' => $disabledCheckbox,
934 'disabled-expiry' => $disabledExpiry,
935 'irreversible' => $irreversible
938 if ( $disabledCheckbox && $disabledExpiry ) {
939 $columns[
'unchangeable'][$group] = $checkbox;
941 $columns[
'changeable'][$group] = $checkbox;
946 $ret .=
Xml::openElement(
'table', [
'class' =>
'mw-userrights-groups' ] ) .
948 foreach ( $columns as $name => $column ) {
949 if ( $column === [] ) {
956 $this->
msg(
'userrights-' . $name .
'-col', count( $column ) )->text()
960 $ret .=
"</tr>\n<tr>\n";
963 foreach ( $columns as $column ) {
964 if ( $column === [] ) {
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';
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();
983 "wpGroup-" . $group, $checkbox[
'set'], $attr );
988 $currentExpiry = isset( $usergroups[$group] ) ?
989 $usergroups[$group]->getExpiry() :
994 if ( $checkbox[
'set'] &&
995 ( $checkbox[
'irreversible'] || $checkbox[
'disabled-expiry'] )
997 if ( $currentExpiry ) {
998 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
999 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
1000 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
1002 $this->
msg(
'userrights-expiry-current' )->params(
1003 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
1006 $this->
msg(
'userrights-expiry-none' )->text() );
1011 $currentExpiry ?
'existing' :
'infinite' );
1012 $expiryHtml .=
"<br />\n";
1015 $this->
msg(
'userrights-expiry' )->text() );
1021 "mw-input-wpExpiry-$group",
1022 $currentExpiry ?
'existing' :
'infinite'
1024 if ( $checkbox[
'disabled-expiry'] ) {
1025 $expiryFormOptions->setAttribute(
'disabled',
'disabled' );
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' );
1037 $expiryFormOptions->addOption(
1038 $this->
msg(
'userrights-expiry-none' )->text(),
1041 $expiryFormOptions->addOption(
1042 $this->
msg(
'userrights-expiry-othertime' )->text(),
1046 $expiryFormOptions->addOptions( $expiryOptions );
1049 $expiryHtml .= $expiryFormOptions->getHTML() .
'<br />';
1053 'id' =>
"mw-input-wpExpiry-$group-other",
1054 'class' =>
'mw-userrights-expiryfield',
1056 if ( $checkbox[
'disabled-expiry'] ) {
1057 $attribs[
'disabled'] =
'disabled';
1059 $expiryHtml .=
Xml::input(
"wpExpiry-$group-other", 30,
'', $attribs );
1063 if ( $checkbox[
'set'] && $checkbox[
'disabled'] ) {
1071 'id' =>
"mw-userrights-nested-wpGroup-$group",
1072 'class' =>
'mw-userrights-nested',
1074 $checkboxHtml .=
"\t\t\t" .
Xml::tags(
'div', $divAttribs, $expiryHtml ) .
"\n";
1076 $ret .=
"\t\t" . ( ( $checkbox[
'disabled'] && $checkbox[
'disabled-expiry'] )
1077 ?
Xml::tags(
'div', [
'class' =>
'mw-userrights-disabled' ], $checkboxHtml )
1081 $ret .=
"\t</td>\n";
1085 return [ $ret, (bool)$columns[
'changeable'] ];
1092 private function canRemove( $group ) {
1097 $groups[
'remove'] ) || ( $this->isself && in_array( $group, $groups[
'remove-self'] )
1105 private function canAdd( $group ) {
1110 $groups[
'add'] ) || ( $this->isself && in_array( $group, $groups[
'add-self'] )
1134 private function getDisplayUsername(
UserIdentity $user ) {
1136 if ( $user->
getWikiId() !== UserIdentity::LOCAL ) {
1150 $rightsLogPage =
new LogPage(
'rights' );
1151 $output->addHTML(
Xml::element(
'h2',
null, $rightsLogPage->getName()->text() ) );
1165 $search = $this->userNameUtils->getCanonical( $search );
1171 return $this->userNamePrefixSearch
1172 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1184 class_alias( SpecialUserRights::class,
'UserrightsPage' );
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,...
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Class for creating new log entries and inserting them into the database.
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()
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.
Show an error when a user tries to do something they do not have the necessary permissions for.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Show an error when the user tries to do something whilst blocked.
Class for generating HTML <select> or <datalist> elements.
static parseOptionsMessage(string $msg)
Parse labels and values out of a comma- and colon-separated list of options, such as is used for expi...
Module of static functions for generating XML.
static closeElement( $element)
Shortcut to close an XML element.
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
static openElement( $element, $attribs=null)
This opens an XML element.
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
static inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field with a label.
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.