24 namespace MediaWiki\Specials;
73 private $userGroupManagerFactory;
76 private $userGroupManager =
null;
79 private $userNameUtils;
82 private $userNamePrefixSearch;
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();
126 $userGroupManager = $this->userGroupManagerFactory
127 ->getUserGroupManager( $targetUser->
getWikiId() );
129 if ( $targetUser->
getId() === 0 ) {
133 if ( $available[
'add'] || $available[
'remove'] ) {
138 if ( ( $available[
'add-self'] || $available[
'remove-self'] )
139 && (
$isself || !$checkIfSelf )
158 $session = $request->getSession();
161 $out->addModules( [
'mediawiki.special.userrights' ] );
163 $this->mTarget = $par ?? $request->getVal(
'user' );
165 if ( is_string( $this->mTarget ) ) {
166 $this->mTarget = trim( $this->mTarget );
169 if ( $this->mTarget !==
null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->getName() ) {
170 $this->isself =
true;
173 $fetchedStatus = $this->mTarget ===
null ?
Status::newFatal(
'nouserspecified' ) :
174 $this->
fetchUser( $this->mTarget,
true );
175 if ( $fetchedStatus->isOK() ) {
176 $this->mFetchedUser = $fetchedUser = $fetchedStatus->value;
178 '@phan-var UserIdentity $fetchedUser';
179 $wikiId = $fetchedUser->getWikiId();
180 if ( $wikiId === UserIdentity::LOCAL ) {
183 $this->
getSkin()->setRelevantUser( $this->mFetchedUser );
185 $this->userGroupManager = $this->userGroupManagerFactory
186 ->getUserGroupManager( $wikiId );
191 $session->get(
'specialUserrightsSaveSuccess' ) &&
192 $this->mFetchedUser !==
null
195 $session->remove(
'specialUserrightsSaveSuccess' );
197 $out->addModuleStyles(
'mediawiki.notification.convertmessagebox.styles' );
203 $this->
msg(
'savedrights', $this->mFetchedUser->getName() )->text()
213 $out->addModuleStyles(
'mediawiki.special' );
214 $this->
addHelpLink(
'Help:Assigning permissions' );
219 $request->wasPosted() &&
220 $request->getCheck(
'saveusergroups' ) &&
221 $this->mTarget !==
null &&
222 $user->matchEditToken( $request->getVal(
'wpEditToken' ), $this->mTarget )
229 if ( !$this->
getAuthority()->isAllowed(
'userrights' ) ) {
230 $block = $user->getBlock();
231 if ( $block && $block->isSitewide() ) {
244 if ( !$fetchedStatus->isOK() ) {
245 $this->
getOutput()->addWikiTextAsInterface(
246 $fetchedStatus->getWikiText(
false,
false, $this->getLanguage() )
253 $conflictCheck = $request->getVal(
'conflictcheck-originalgroups' );
254 $conflictCheck = ( $conflictCheck ===
'' ) ? [] : explode(
',', $conflictCheck );
255 $userGroups = $this->userGroupManager->getUserGroups( $targetUser, UserGroupManager::READ_LATEST );
257 if ( $userGroups !== $conflictCheck ) {
259 $this->
msg(
'userrights-conflict' )->parse()
264 $request->getVal(
'user-reason' ),
268 if ( $status->isOK() ) {
270 $session->set(
'specialUserrightsSaveSuccess', 1 );
272 $out->redirect( $this->getSuccessURL() );
276 $out->wrapWikiTextAsInterface(
277 'error', $status->getWikiText(
false,
false, $this->getLanguage() )
284 if ( $this->mTarget !==
null ) {
285 $this->editUserGroupsForm( $this->mTarget );
289 private function getSuccessURL() {
290 return $this->
getPageTitle( $this->mTarget )->getFullURL();
317 $unix = strtotime( $expiry );
319 if ( !$unix || $unix === -1 ) {
338 $allgroups = $this->userGroupManager->listAllGroups();
342 $existingUGMs = $this->userGroupManager->getUserGroupMemberships( $user );
346 foreach ( $allgroups as $group ) {
349 if ( $this->
getRequest()->getCheck(
"wpGroup-$group" ) ) {
350 $addgroup[] = $group;
354 $expiryDropdown = $this->
getRequest()->getVal(
"wpExpiry-$group" );
355 if ( $expiryDropdown ===
'existing' ) {
359 if ( $expiryDropdown ===
'other' ) {
360 $expiryValue = $this->
getRequest()->getVal(
"wpExpiry-$group-other" );
362 $expiryValue = $expiryDropdown;
368 if ( $groupExpiries[$group] ===
false ) {
373 if ( $groupExpiries[$group] && $groupExpiries[$group] <
wfTimestampNow() ) {
379 if ( !$this->canRemove( $group ) &&
380 isset( $existingUGMs[$group] ) &&
381 ( $existingUGMs[$group]->getExpiry() ?:
'infinity' ) >
382 ( $groupExpiries[$group] ?:
'infinity' )
388 $removegroup[] = $group;
392 $this->
doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
413 array $tags = [], array $groupExpiries = []
417 if ( $this->userGroupManager !==
null ) {
419 $userGroupManager = $this->userGroupManager;
422 $userGroupManager = $this->userGroupManagerFactory
423 ->getUserGroupManager( $user->getWikiId() );
428 $addable = array_merge( $changeable[
'add'],
$isself ? $changeable[
'add-self'] : [] );
429 $removable = array_merge( $changeable[
'remove'],
$isself ? $changeable[
'remove-self'] : [] );
431 $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
432 $add = array_intersect( $add, $addable );
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' )
447 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
452 $oldGroups = $groups;
454 $newGroups = $oldGroups;
458 foreach ( $remove as $index => $group ) {
460 unset( $remove[$index] );
463 $newGroups = array_diff( $newGroups, $remove );
466 foreach ( $add as $index => $group ) {
467 $expiry = $groupExpiries[$group] ??
null;
468 if ( !$userGroupManager->
addUserToGroup( $user, $group, $expiry,
true ) ) {
469 unset( $add[$index] );
472 $newGroups = array_merge( $newGroups, $add );
474 $newGroups = array_unique( $newGroups );
478 $this->userFactory->invalidateCache( $user );
481 $this->
getHookRunner()->onUserGroupsChanged( $user, $add, $remove,
482 $this->
getUser(), $reason, $oldUGMs, $newUGMs );
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 ) );
490 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
491 $this->
addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
494 return [ $add, $remove ];
508 return [
'expiry' => $ugm->getExpiry() ];
521 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
522 array $tags, array $oldUGMs, array $newUGMs
526 $oldUGMs = array_map(
function ( $group ) use ( $oldUGMs ) {
527 return isset( $oldUGMs[$group] ) ?
531 $newUGMs = array_map(
function ( $group ) use ( $newUGMs ) {
532 return isset( $newUGMs[$group] ) ?
538 $logEntry->setPerformer( $this->
getUser() );
540 $logEntry->setComment( is_string( $reason ) ? $reason :
"" );
541 $logEntry->setParameters( [
542 '4::oldgroups' => $oldGroups,
543 '5::newgroups' => $newGroups,
544 'oldmetadata' => $oldUGMs,
545 'newmetadata' => $newUGMs,
547 $logid = $logEntry->insert();
548 if ( count( $tags ) ) {
549 $logEntry->addTags( $tags );
551 $logEntry->publish( $logid );
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() )
569 $user = $status->value;
570 '@phan-var UserIdentity $user';
572 $groups = $this->userGroupManager->getUserGroups( $user );
573 $groupMemberships = $this->userGroupManager->getUserGroupMemberships( $user );
590 public function fetchUser( $username, $writing =
true ) {
593 if ( count( $parts ) < 2 ) {
594 $name = trim( $username );
597 [ $name, $dbDomain ] = array_map(
'trim', $parts );
603 !$this->
getAuthority()->isAllowed(
'userrights-interwiki' )
613 if ( $name ===
'' ) {
617 if ( $name[0] ==
'#' ) {
620 $id = intval( substr( $name, 1 ) );
622 if ( $dbDomain ==
'' ) {
632 $name = $this->userNameUtils->getCanonical( $name );
633 if ( $name ===
false ) {
639 if ( $dbDomain ==
'' ) {
640 $user = $this->userFactory->newFromName( $name );
645 if ( !$user || $user->isAnon() ) {
649 if ( $user->getWikiId() === UserIdentity::LOCAL &&
650 $this->userFactory->newFromUserIdentity( $user )->isHidden() &&
651 !$this->getAuthority()->isAllowed(
'hideuser' )
668 if ( empty( $ids ) ) {
669 return $this->
msg(
'rightsnone' )->inContentLanguage()->text();
671 return implode(
', ', $ids );
679 $this->
getOutput()->addModules(
'mediawiki.userSuggest' );
688 'id' =>
'mw-userrights-form1'
694 $this->
msg(
'userrights-user-editname' )->text(),
698 $this->mTarget ? str_replace(
'_',
' ', $this->mTarget ) :
'',
700 'class' =>
'mw-autocomplete-user',
703 $this->mFetchedUser ===
null ? [
'autofocus' =>
'' ] : []
707 $this->
msg(
'editusergroup' )->text()
724 $list = $membersList = $tempList = $tempMembersList = [];
725 foreach ( $groupMemberships as $ugm ) {
728 if ( $ugm->getExpiry() ) {
729 $tempList[] = $linkG;
730 $tempMembersList[] = $linkM;
733 $membersList[] = $linkM;
739 $autoMembersList = [];
741 if ( $user->getWikiId() === UserIdentity::LOCAL ) {
743 foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) {
750 $displayedList = $this->
msg(
'userrights-groupsmember-type' )
752 $language->commaList( array_merge( $tempList, $list ) ),
753 $language->commaList( array_merge( $tempMembersList, $membersList ) )
755 $displayedAutolist = $this->
msg(
'userrights-groupsmember-type' )
757 $language->commaList( $autoList ),
758 $language->commaList( $autoMembersList )
762 $count = count( $list ) + count( $tempList );
764 $grouplist = $this->
msg(
'userrights-groupsmember' )
765 ->numParams( $count )
766 ->params( $user->getName() )
768 $grouplist =
'<p>' . $grouplist .
' ' . $displayedList .
"</p>\n";
771 $count = count( $autoList );
773 $autogrouplistintro = $this->
msg(
'userrights-groupsmember-auto' )
774 ->numParams( $count )
775 ->params( $user->getName() )
777 $grouplist .=
'<p>' . $autogrouplistintro .
' ' . $displayedAutolist .
"</p>\n";
780 $systemUser = $user->getWikiId() === UserIdentity::LOCAL
781 && $this->userFactory->newFromUserIdentity( $user )->isSystemUser();
783 $systemusernote = $this->
msg(
'userrights-systemuser' )
784 ->params( $user->getName() )
786 $grouplist .=
'<p>' . $systemusernote .
"</p>\n";
798 [ $groupCheckboxes, $canChangeAny ] =
799 $this->groupCheckboxes( $groupMemberships, $user );
806 'name' =>
'editGroup',
807 'id' =>
'mw-userrights-form2'
813 'conflictcheck-originalgroups',
814 implode(
',', $this->userGroupManager->getUserGroups( $user ) )
821 $canChangeAny ?
'userrights-editusergroup' :
'userrights-viewusergroup',
826 $canChangeAny ?
'editinguser' :
'viewinguserrights'
828 ->rawParams( $userToolLinks )->parse()
830 if ( $canChangeAny ) {
832 $this->
msg(
'userrights-groups-help', $user->getName() )->parse() .
837 <td class='mw-label'>" .
838 Xml::label( $this->msg(
'userrights-reason' )->text(),
'wpReason' ) .
840 <td class='mw-input'>" .
841 Xml::input(
'user-reason', 60, $this->getRequest()->getVal(
'user-reason' ) ??
false, [
852 <td class='mw-submit'>" .
854 [
'name' =>
'saveusergroups' ] +
862 $this->
getOutput()->addHTML( $grouplist );
879 private function groupCheckboxes( $usergroups, $user ) {
880 $allgroups = $this->userGroupManager->listAllGroups();
884 $expiryOptionsMsg = $this->
msg(
'userrights-expiry-options' )->inContentLanguage();
885 $expiryOptions = $expiryOptionsMsg->isDisabled()
891 $columns = [
'unchangeable' => [],
'changeable' => [] ];
893 foreach ( $allgroups as $group ) {
894 $set = isset( $usergroups[$group] );
898 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
899 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
901 $disabledCheckbox = !(
902 ( $set && $this->canRemove( $group ) ) ||
903 ( !$set && $this->canAdd( $group ) ) );
905 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
907 $irreversible = !$disabledCheckbox && (
908 ( $set && !$this->canAdd( $group ) ) ||
909 ( !$set && !$this->canRemove( $group ) ) );
913 'disabled' => $disabledCheckbox,
914 'disabled-expiry' => $disabledExpiry,
915 'irreversible' => $irreversible
918 if ( $disabledCheckbox && $disabledExpiry ) {
919 $columns[
'unchangeable'][$group] = $checkbox;
921 $columns[
'changeable'][$group] = $checkbox;
926 $ret .=
Xml::openElement(
'table', [
'class' =>
'mw-userrights-groups' ] ) .
928 foreach ( $columns as $name => $column ) {
929 if ( $column === [] ) {
936 $this->
msg(
'userrights-' . $name .
'-col', count( $column ) )->text()
940 $ret .=
"</tr>\n<tr>\n";
942 $userName = $user->getName();
943 foreach ( $columns as $column ) {
944 if ( $column === [] ) {
947 $ret .=
"\t<td style='vertical-align:top;'>\n";
948 foreach ( $column as $group => $checkbox ) {
949 $attr = [
'class' =>
'mw-userrights-groupcheckbox' ];
950 if ( $checkbox[
'disabled'] ) {
951 $attr[
'disabled'] =
'disabled';
954 $member = $uiLanguage->getGroupMemberName( $group, $userName );
955 if ( $checkbox[
'irreversible'] ) {
956 $text = $this->
msg(
'userrights-irreversible-marker', $member )->text();
957 } elseif ( $checkbox[
'disabled'] && !$checkbox[
'disabled-expiry'] ) {
958 $text = $this->
msg(
'userrights-no-shorten-expiry-marker', $member )->text();
963 "wpGroup-" . $group, $checkbox[
'set'], $attr );
968 $currentExpiry = isset( $usergroups[$group] ) ?
969 $usergroups[$group]->getExpiry() :
974 if ( $checkbox[
'set'] &&
975 ( $checkbox[
'irreversible'] || $checkbox[
'disabled-expiry'] )
977 if ( $currentExpiry ) {
978 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
979 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
980 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
982 $this->
msg(
'userrights-expiry-current' )->params(
983 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
986 $this->
msg(
'userrights-expiry-none' )->text() );
991 $currentExpiry ?
'existing' :
'infinite' );
992 $expiryHtml .=
"<br />\n";
995 $this->
msg(
'userrights-expiry' )->text() );
1001 "mw-input-wpExpiry-$group",
1002 $currentExpiry ?
'existing' :
'infinite'
1004 if ( $checkbox[
'disabled-expiry'] ) {
1005 $expiryFormOptions->setAttribute(
'disabled',
'disabled' );
1008 if ( $currentExpiry ) {
1009 $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
1010 $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
1011 $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
1012 $existingExpiryMessage = $this->
msg(
'userrights-expiry-existing',
1013 $timestamp, $d,
$t );
1014 $expiryFormOptions->addOption( $existingExpiryMessage->text(),
'existing' );
1017 $expiryFormOptions->addOption(
1018 $this->
msg(
'userrights-expiry-none' )->text(),
1021 $expiryFormOptions->addOption(
1022 $this->
msg(
'userrights-expiry-othertime' )->text(),
1026 $expiryFormOptions->addOptions( $expiryOptions );
1029 $expiryHtml .= $expiryFormOptions->getHTML() .
'<br />';
1033 'id' =>
"mw-input-wpExpiry-$group-other",
1034 'class' =>
'mw-userrights-expiryfield',
1036 if ( $checkbox[
'disabled-expiry'] ) {
1037 $attribs[
'disabled'] =
'disabled';
1039 $expiryHtml .=
Xml::input(
"wpExpiry-$group-other", 30,
'', $attribs );
1043 if ( $checkbox[
'set'] && $checkbox[
'disabled'] ) {
1051 'id' =>
"mw-userrights-nested-wpGroup-$group",
1052 'class' =>
'mw-userrights-nested',
1054 $checkboxHtml .=
"\t\t\t" .
Xml::tags(
'div', $divAttribs, $expiryHtml ) .
"\n";
1056 $ret .=
"\t\t" . ( ( $checkbox[
'disabled'] && $checkbox[
'disabled-expiry'] )
1057 ?
Xml::tags(
'div', [
'class' =>
'mw-userrights-disabled' ], $checkboxHtml )
1061 $ret .=
"\t</td>\n";
1065 return [ $ret, (bool)$columns[
'changeable'] ];
1072 private function canRemove( $group ) {
1077 $groups[
'remove'] ) || ( $this->isself && in_array( $group, $groups[
'remove-self'] )
1085 private function canAdd( $group ) {
1090 $groups[
'add'] ) || ( $this->isself && in_array( $group, $groups[
'add-self'] )
1113 $rightsLogPage =
new LogPage(
'rights' );
1114 $output->addHTML(
Xml::element(
'h2',
null, $rightsLogPage->getName()->text() ) );
1127 $search = $this->userNameUtils->getCanonical( $search );
1133 return $this->userNamePrefixSearch
1134 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1146 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 path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
static 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 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.
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.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Show an error when the user tries to do something whilst blocked.
Represents a "user group membership" – a specific instance of a user belonging to a group.
static getLinkHTML( $ugm, IContextSource $context, $userName=null)
Gets a link for a user group, possibly including the expiry date if relevant.
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.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
static whoIs( $id)
Get the username corresponding to a given user ID.
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.