MediaWiki master
ApiUserrights.php
Go to the documentation of this file.
1<?php
2
12namespace MediaWiki\Api;
13
29
33class ApiUserrights extends ApiBase {
34
36
38 private $mUser = null;
39
40 public function __construct(
41 ApiMain $mainModule,
42 string $moduleName,
43 private readonly UserGroupManager $userGroupManager,
44 WatchedItemStoreInterface $watchedItemStore,
45 WatchlistManager $watchlistManager,
46 UserOptionsLookup $userOptionsLookup,
47 private readonly UserGroupAssignmentService $userGroupAssignmentService,
48 private readonly MultiFormatUserIdentityLookup $multiFormatUserIdentityLookup,
49 ) {
50 parent::__construct( $mainModule, $moduleName );
51 $this->watchedItemStore = $watchedItemStore;
52
53 // Variables needed in ApiWatchlistTrait trait
54 $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
55 $this->watchlistMaxDuration =
57 $this->watchlistManager = $watchlistManager;
58 $this->userOptionsLookup = $userOptionsLookup;
59 }
60
61 public function execute() {
62 $pUser = $this->getUser();
63
64 // Deny if the user is blocked and doesn't have the full 'userrights' permission.
65 // This matches what Special:UserRights does for the web UI.
66 if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) {
67 $block = $pUser->getBlock( IDBAccessObject::READ_LATEST );
68 if ( $block && $block->isSitewide() ) {
69 $this->dieBlocked( $block );
70 }
71 }
72
73 $params = $this->extractRequestParams();
74
75 // Figure out expiry times from the input
76 $expiry = (array)$params['expiry'];
77 $add = (array)$params['add'];
78 if ( !$add ) {
79 $expiry = [];
80 } elseif ( count( $expiry ) !== count( $add ) ) {
81 if ( count( $expiry ) === 1 ) {
82 $expiry = array_fill( 0, count( $add ), $expiry[0] );
83 } else {
84 $this->dieWithError( [
85 'apierror-toofewexpiries',
86 count( $expiry ),
87 count( $add )
88 ] );
89 }
90 }
91
92 // Validate the expiries
93 $groupExpiries = [];
94 foreach ( $expiry as $index => $expiryValue ) {
95 $group = $add[$index];
96 $groupExpiries[$group] = UserGroupAssignmentService::expiryToTimestamp( $expiryValue );
97
98 if ( $groupExpiries[$group] === false ) {
99 $this->dieWithError( [ 'apierror-invalidexpiry', wfEscapeWikiText( $expiryValue ) ] );
100 }
101
102 // not allowed to have things expiring in the past
103 if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
104 $this->dieWithError( [ 'apierror-pastexpiry', wfEscapeWikiText( $expiryValue ) ] );
105 }
106 }
107
108 $user = $this->getUrUser( $params );
109
110 $tags = $params['tags'];
111
112 // Check if user can add tags
113 if ( $tags !== null ) {
114 $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $tags, $this->getAuthority() );
115 if ( !$ableToTag->isOK() ) {
116 $this->dieStatus( $ableToTag );
117 }
118 }
119
120 $r = [];
121 $r['user'] = $user->getName();
122 $r['userid'] = $user->getId( $user->getWikiId() );
123 [ $r['added'], $r['removed'] ] = $this->userGroupAssignmentService->saveChangesToUserGroups(
124 $this->getUser(),
125 $user,
126 $add,
127 // Don't pass null to saveChangesToUserGroups() for array params, cast to empty array
128 (array)$params['remove'],
129 $groupExpiries,
130 $params['reason'],
131 (array)$tags
132 );
133
134 $userPage = Title::makeTitle( NS_USER, $user->getName() );
135 $watchlistExpiry = $this->getExpiryFromParams( $params, $userPage, $this->getUser() );
136 $watchuser = $params['watchuser'];
137 if ( $watchuser && $user->getWikiId() === UserIdentity::LOCAL ) {
138 $this->setWatch( 'watch', $userPage, $this->getUser(), null, $watchlistExpiry );
139 } else {
140 $watchuser = false;
141 $watchlistExpiry = null;
142 }
143 $r['watchuser'] = $watchuser;
144 if ( $watchlistExpiry !== null ) {
145 $r['watchlistexpiry'] = $this->getWatchlistExpiry(
146 $this->watchedItemStore,
147 $userPage,
148 $this->getUser()
149 );
150 }
151
152 $result = $this->getResult();
153 ApiResult::setIndexedTagName( $r['added'], 'group' );
154 ApiResult::setIndexedTagName( $r['removed'], 'group' );
155 $result->addValue( null, $this->getModuleName(), $r );
156 }
157
162 private function getUrUser( array $params ) {
163 if ( $this->mUser !== null ) {
164 return $this->mUser;
165 }
166
167 $this->requireOnlyOneParameter( $params, 'user', 'userid' );
168
169 $userDesignator = $params['user'] ?? '#' . $params['userid'];
170
171 // T422085: Check userrights-interwiki permission BEFORE looking up the user,
172 // to prevent user enumeration on private/remote wikis.
173 // This mirrors the fix applied to SpecialUserRights in Gerrit change I15306e63.
174 if ( isset( $params['user'] ) ) {
175 $interwikiDelimiter = $this->getConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter );
176 if ( str_contains( $userDesignator, $interwikiDelimiter ) ) {
177 $targetParts = explode( $interwikiDelimiter, $userDesignator );
178 $remoteWikiId = $targetParts[1] ?? '';
179 if (
180 $remoteWikiId !== '' &&
181 !WikiMap::isCurrentWikiId( $remoteWikiId ) &&
182 !$this->getAuthority()->isAllowed( 'userrights-interwiki' )
183 ) {
184 $this->dieWithError( 'apierror-permissiondenied-userrights-interwiki', 'permissiondenied' );
185 }
186 }
187 }
188
189 $status = $this->multiFormatUserIdentityLookup->getUserIdentity( $userDesignator, $this->getAuthority() );
190 if ( !$status->isOK() ) {
191 $this->dieStatus( $status );
192 }
193
194 $user = $status->value;
195 $canHaveRights = $this->userGroupAssignmentService->targetCanHaveUserGroups( $user );
196 if ( !$canHaveRights ) {
197 // Return different errors for anons and temp. accounts to keep consistent behavior
198 $this->dieWithError(
199 $user->isRegistered() ? [ 'userrights-no-group', $user->getName() ] : 'nosuchusershort'
200 );
201 }
202
203 $this->mUser = $user;
204
205 return $user;
206 }
207
209 public function mustBePosted() {
210 return true;
211 }
212
214 public function isWriteMode() {
215 return true;
216 }
217
219 public function getAllowedParams( $flags = 0 ) {
220 $allGroups = $this->userGroupManager->listAllGroups();
221
222 if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
223 sort( $allGroups );
224 }
225
226 $params = [
227 'user' => [
228 ParamValidator::PARAM_TYPE => 'user',
229 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
230 ],
231 'userid' => [
232 ParamValidator::PARAM_TYPE => 'integer',
233 ParamValidator::PARAM_DEPRECATED => true,
234 ],
235 'add' => [
236 ParamValidator::PARAM_TYPE => $allGroups,
237 ParamValidator::PARAM_ISMULTI => true
238 ],
239 'expiry' => [
240 ParamValidator::PARAM_ISMULTI => true,
241 ParamValidator::PARAM_ALLOW_DUPLICATES => true,
242 ParamValidator::PARAM_DEFAULT => 'infinite',
243 ],
244 'remove' => [
245 ParamValidator::PARAM_TYPE => $allGroups,
246 ParamValidator::PARAM_ISMULTI => true
247 ],
248 'reason' => [
249 ParamValidator::PARAM_DEFAULT => ''
250 ],
251 'token' => [
252 // Standard definition automatically inserted
253 ApiBase::PARAM_HELP_MSG_APPEND => [ 'api-help-param-token-webui' ],
254 ],
255 'tags' => [
256 ParamValidator::PARAM_TYPE => 'tags',
257 ParamValidator::PARAM_ISMULTI => true
258 ],
259 'watchuser' => false,
260 ];
261
262 // Params appear in the docs in the order they are defined,
263 // which is why this is here and not at the bottom.
264 // @todo Find better way to support insertion at arbitrary position
265 if ( $this->watchlistExpiryEnabled ) {
266 $params += [
267 'watchlistexpiry' => [
268 ParamValidator::PARAM_TYPE => 'expiry',
269 ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
270 ExpiryDef::PARAM_USE_MAX => true,
271 ]
272 ];
273 }
274
275 return $params;
276 }
277
279 public function needsToken() {
280 return 'userrights';
281 }
282
284 protected function getWebUITokenSalt( array $params ) {
285 return $this->getUrUser( $params )->getName();
286 }
287
289 protected function getExamplesMessages() {
290 return [
291 'action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC'
292 => 'apihelp-userrights-example-user',
293 'action=userrights&userid=123&add=bot&remove=sysop|bureaucrat&token=123ABC'
294 => 'apihelp-userrights-example-userid',
295 'action=userrights&user=SometimeSysop&add=sysop&expiry=1%20month&token=123ABC'
296 => 'apihelp-userrights-example-expiry',
297 ];
298 }
299
301 public function getHelpUrls() {
302 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:User_group_membership';
303 }
304}
305
307class_alias( ApiUserrights::class, 'ApiUserrights' );
const NS_USER
Definition Defines.php:53
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:60
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1522
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:557
getResult()
Get the result object.
Definition ApiBase.php:696
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:174
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition ApiBase.php:1550
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1573
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:837
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:975
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When this is set, the result could take longer to generate,...
Definition ApiBase.php:244
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:66
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
__construct(ApiMain $mainModule, string $moduleName, private readonly UserGroupManager $userGroupManager, WatchedItemStoreInterface $watchedItemStore, WatchlistManager $watchlistManager, UserOptionsLookup $userOptionsLookup, private readonly UserGroupAssignmentService $userGroupAssignmentService, private readonly MultiFormatUserIdentityLookup $multiFormatUserIdentityLookup,)
getWebUITokenSalt(array $params)
Fetch the salt used in the Web UI corresponding to this module.Only override this if the Web UI uses ...
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
isWriteMode()
Indicates whether this module requires write access to the wiki.API modules must override this method...
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
needsToken()
Returns the token type this module requires in order to execute.Modules are strongly encouraged to us...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
mustBePosted()
Indicates whether this module must be called with a POST request.Implementations of this method must ...
Recent changes tagging.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
A class containing constants representing the names of configuration variables.
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
const UserrightsInterwikiDelimiter
Name constant for the UserrightsInterwikiDelimiter setting, for use with Config::get()
const WatchlistExpiryMaxDuration
Name constant for the WatchlistExpiryMaxDuration setting, for use with Config::get()
Type definition for user types.
Definition UserDef.php:27
Represents a title within MediaWiki.
Definition Title.php:69
A service to look up user identities based on the user input.
Provides access to user options.
This class represents a service that provides high-level operations on user groups.
Manage user group memberships.
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Service for formatting and validating API parameters.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
Interface for objects representing user identity.
Interface for database access objects.
setWatch(string $watch, PageIdentity $page, User $user, ?string $userOption=null, ?string $expiry=null)
Set a watch (or unwatch) based the based on a watchlist parameter.
getExpiryFromParams(array $params, ?PageIdentity $page=null, ?UserIdentity $user=null, string $userOption='watchdefault-expiry')
Get formatted expiry from the given parameters.
getWatchlistExpiry(WatchedItemStoreInterface $store, PageIdentity $page, UserIdentity $user)
Get existing expiry from the database.