Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.03% covered (warning)
85.03%
125 / 147
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiUserrights
85.62% covered (warning)
85.62%
125 / 146
50.00% covered (danger)
50.00%
5 / 10
33.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 execute
98.39% covered (success)
98.39%
61 / 62
0.00% covered (danger)
0.00%
0 / 1
16
 getUrUser
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
5.20
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
82.61% covered (warning)
82.61%
38 / 46
0.00% covered (danger)
0.00%
0 / 1
3.05
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWebUITokenSalt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * API userrights module
5 *
6 * Copyright © 2009 Roan Kattouw <roan.kattouw@gmail.com>
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 */
11
12namespace MediaWiki\Api;
13
14use MediaWiki\ChangeTags\ChangeTags;
15use MediaWiki\MainConfigNames;
16use MediaWiki\ParamValidator\TypeDef\UserDef;
17use MediaWiki\Title\Title;
18use MediaWiki\User\MultiFormatUserIdentityLookup;
19use MediaWiki\User\Options\UserOptionsLookup;
20use MediaWiki\User\UserGroupAssignmentService;
21use MediaWiki\User\UserGroupManager;
22use MediaWiki\User\UserIdentity;
23use MediaWiki\Watchlist\WatchedItemStoreInterface;
24use MediaWiki\Watchlist\WatchlistManager;
25use Wikimedia\ParamValidator\ParamValidator;
26use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
27use Wikimedia\Rdbms\IDBAccessObject;
28
29/**
30 * @ingroup API
31 */
32class ApiUserrights extends ApiBase {
33
34    use ApiWatchlistTrait;
35
36    /** @var UserIdentity|null */
37    private $mUser = null;
38
39    private UserGroupManager $userGroupManager;
40    private WatchedItemStoreInterface $watchedItemStore;
41    private UserGroupAssignmentService $userGroupAssignmentService;
42    private MultiFormatUserIdentityLookup $multiFormatUserIdentityLookup;
43
44    public function __construct(
45        ApiMain $mainModule,
46        string $moduleName,
47        UserGroupManager $userGroupManager,
48        WatchedItemStoreInterface $watchedItemStore,
49        WatchlistManager $watchlistManager,
50        UserOptionsLookup $userOptionsLookup,
51        UserGroupAssignmentService $userGroupAssignmentService,
52        MultiFormatUserIdentityLookup $multiFormatUserIdentityLookup,
53    ) {
54        parent::__construct( $mainModule, $moduleName );
55        $this->userGroupManager = $userGroupManager;
56        $this->watchedItemStore = $watchedItemStore;
57
58        // Variables needed in ApiWatchlistTrait trait
59        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
60        $this->watchlistMaxDuration =
61            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
62        $this->watchlistManager = $watchlistManager;
63        $this->userOptionsLookup = $userOptionsLookup;
64        $this->userGroupAssignmentService = $userGroupAssignmentService;
65        $this->multiFormatUserIdentityLookup = $multiFormatUserIdentityLookup;
66    }
67
68    public function execute() {
69        $pUser = $this->getUser();
70
71        // Deny if the user is blocked and doesn't have the full 'userrights' permission.
72        // This matches what Special:UserRights does for the web UI.
73        if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) {
74            $block = $pUser->getBlock( IDBAccessObject::READ_LATEST );
75            if ( $block && $block->isSitewide() ) {
76                $this->dieBlocked( $block );
77            }
78        }
79
80        $params = $this->extractRequestParams();
81
82        // Figure out expiry times from the input
83        $expiry = (array)$params['expiry'];
84        $add = (array)$params['add'];
85        if ( !$add ) {
86            $expiry = [];
87        } elseif ( count( $expiry ) !== count( $add ) ) {
88            if ( count( $expiry ) === 1 ) {
89                $expiry = array_fill( 0, count( $add ), $expiry[0] );
90            } else {
91                $this->dieWithError( [
92                    'apierror-toofewexpiries',
93                    count( $expiry ),
94                    count( $add )
95                ] );
96            }
97        }
98
99        // Validate the expiries
100        $groupExpiries = [];
101        foreach ( $expiry as $index => $expiryValue ) {
102            $group = $add[$index];
103            $groupExpiries[$group] = UserGroupAssignmentService::expiryToTimestamp( $expiryValue );
104
105            if ( $groupExpiries[$group] === false ) {
106                $this->dieWithError( [ 'apierror-invalidexpiry', wfEscapeWikiText( $expiryValue ) ] );
107            }
108
109            // not allowed to have things expiring in the past
110            if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
111                $this->dieWithError( [ 'apierror-pastexpiry', wfEscapeWikiText( $expiryValue ) ] );
112            }
113        }
114
115        $user = $this->getUrUser( $params );
116
117        $tags = $params['tags'];
118
119        // Check if user can add tags
120        if ( $tags !== null ) {
121            $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $tags, $this->getAuthority() );
122            if ( !$ableToTag->isOK() ) {
123                $this->dieStatus( $ableToTag );
124            }
125        }
126
127        $r = [];
128        $r['user'] = $user->getName();
129        $r['userid'] = $user->getId( $user->getWikiId() );
130        [ $r['added'], $r['removed'] ] = $this->userGroupAssignmentService->saveChangesToUserGroups(
131            $this->getUser(),
132            $user,
133            $add,
134            // Don't pass null to saveChangesToUserGroups() for array params, cast to empty array
135            (array)$params['remove'],
136            $groupExpiries,
137            $params['reason'],
138            (array)$tags
139        );
140
141        $userPage = Title::makeTitle( NS_USER, $user->getName() );
142        $watchlistExpiry = $this->getExpiryFromParams( $params, $userPage, $this->getUser() );
143        $watchuser = $params['watchuser'];
144        if ( $watchuser && $user->getWikiId() === UserIdentity::LOCAL ) {
145            $this->setWatch( 'watch', $userPage, $this->getUser(), null, $watchlistExpiry );
146        } else {
147            $watchuser = false;
148            $watchlistExpiry = null;
149        }
150        $r['watchuser'] = $watchuser;
151        if ( $watchlistExpiry !== null ) {
152            $r['watchlistexpiry'] = $this->getWatchlistExpiry(
153                $this->watchedItemStore,
154                $userPage,
155                $this->getUser()
156            );
157        }
158
159        $result = $this->getResult();
160        ApiResult::setIndexedTagName( $r['added'], 'group' );
161        ApiResult::setIndexedTagName( $r['removed'], 'group' );
162        $result->addValue( null, $this->getModuleName(), $r );
163    }
164
165    /**
166     * @param array $params
167     * @return UserIdentity
168     */
169    private function getUrUser( array $params ) {
170        if ( $this->mUser !== null ) {
171            return $this->mUser;
172        }
173
174        $this->requireOnlyOneParameter( $params, 'user', 'userid' );
175
176        $userDesignator = $params['user'] ?? '#' . $params['userid'];
177        $status = $this->multiFormatUserIdentityLookup->getUserIdentity( $userDesignator, $this->getAuthority() );
178        if ( !$status->isOK() ) {
179            $this->dieStatus( $status );
180        }
181
182        $user = $status->value;
183        $canHaveRights = $this->userGroupAssignmentService->targetCanHaveUserGroups( $user );
184        if ( !$canHaveRights ) {
185            // Return different errors for anons and temp. accounts to keep consistent behavior
186            $this->dieWithError(
187                $user->isRegistered() ? [ 'userrights-no-group', $user->getName() ] : 'nosuchusershort'
188            );
189        }
190
191        $this->mUser = $user;
192
193        return $user;
194    }
195
196    /** @inheritDoc */
197    public function mustBePosted() {
198        return true;
199    }
200
201    /** @inheritDoc */
202    public function isWriteMode() {
203        return true;
204    }
205
206    /** @inheritDoc */
207    public function getAllowedParams( $flags = 0 ) {
208        $allGroups = $this->userGroupManager->listAllGroups();
209
210        if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
211            sort( $allGroups );
212        }
213
214        $params = [
215            'user' => [
216                ParamValidator::PARAM_TYPE => 'user',
217                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
218            ],
219            'userid' => [
220                ParamValidator::PARAM_TYPE => 'integer',
221                ParamValidator::PARAM_DEPRECATED => true,
222            ],
223            'add' => [
224                ParamValidator::PARAM_TYPE => $allGroups,
225                ParamValidator::PARAM_ISMULTI => true
226            ],
227            'expiry' => [
228                ParamValidator::PARAM_ISMULTI => true,
229                ParamValidator::PARAM_ALLOW_DUPLICATES => true,
230                ParamValidator::PARAM_DEFAULT => 'infinite',
231            ],
232            'remove' => [
233                ParamValidator::PARAM_TYPE => $allGroups,
234                ParamValidator::PARAM_ISMULTI => true
235            ],
236            'reason' => [
237                ParamValidator::PARAM_DEFAULT => ''
238            ],
239            'token' => [
240                // Standard definition automatically inserted
241                ApiBase::PARAM_HELP_MSG_APPEND => [ 'api-help-param-token-webui' ],
242            ],
243            'tags' => [
244                ParamValidator::PARAM_TYPE => 'tags',
245                ParamValidator::PARAM_ISMULTI => true
246            ],
247            'watchuser' => false,
248        ];
249
250        // Params appear in the docs in the order they are defined,
251        // which is why this is here and not at the bottom.
252        // @todo Find better way to support insertion at arbitrary position
253        if ( $this->watchlistExpiryEnabled ) {
254            $params += [
255                'watchlistexpiry' => [
256                    ParamValidator::PARAM_TYPE => 'expiry',
257                    ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
258                    ExpiryDef::PARAM_USE_MAX => true,
259                ]
260            ];
261        }
262
263        return $params;
264    }
265
266    /** @inheritDoc */
267    public function needsToken() {
268        return 'userrights';
269    }
270
271    /** @inheritDoc */
272    protected function getWebUITokenSalt( array $params ) {
273        return $this->getUrUser( $params )->getName();
274    }
275
276    /** @inheritDoc */
277    protected function getExamplesMessages() {
278        return [
279            'action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC'
280                => 'apihelp-userrights-example-user',
281            'action=userrights&userid=123&add=bot&remove=sysop|bureaucrat&token=123ABC'
282                => 'apihelp-userrights-example-userid',
283            'action=userrights&user=SometimeSysop&add=sysop&expiry=1%20month&token=123ABC'
284                => 'apihelp-userrights-example-expiry',
285        ];
286    }
287
288    /** @inheritDoc */
289    public function getHelpUrls() {
290        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:User_group_membership';
291    }
292}
293
294/** @deprecated class alias since 1.43 */
295class_alias( ApiUserrights::class, 'ApiUserrights' );