Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiUserrightsTest.php
1 <?php
2
3 use MediaWiki\Block\DatabaseBlock;
4 use MediaWiki\MediaWikiServices;
5
6 /**
7 * @group API
8 * @group Database
9 * @group medium
10 *
11 * @covers ApiUserrights
12 */
13 class ApiUserrightsTest extends ApiTestCase {
14
15 protected function setUp() {
16 parent::setUp();
17 $this->tablesUsed = array_merge(
18 $this->tablesUsed,
19 [ 'change_tag', 'change_tag_def', 'logging' ]
20 );
21 }
22
23 /**
24 * Unsets $wgGroupPermissions['bureaucrat']['userrights'], and sets
25 * $wgAddGroups['bureaucrat'] and $wgRemoveGroups['bureaucrat'] to the
26 * specified values.
27 *
28 * @param array|bool $add Groups bureaucrats should be allowed to add, true for all
29 * @param array|bool $remove Groups bureaucrats should be allowed to remove, true for all
30 */
31 protected function setPermissions( $add = [], $remove = [] ) {
32 $this->setGroupPermissions( 'bureaucrat', 'userrights', false );
33
34 if ( $add ) {
35 $this->mergeMwGlobalArrayValue( 'wgAddGroups', [ 'bureaucrat' => $add ] );
36 }
37 if ( $remove ) {
38 $this->mergeMwGlobalArrayValue( 'wgRemoveGroups', [ 'bureaucrat' => $remove ] );
39 }
40
41 $this->resetServices();
42 }
43
44 /**
45 * Perform an API userrights request that's expected to be successful.
46 *
47 * @param array|string $expectedGroups Group(s) that the user is expected
48 * to have after the API request
49 * @param array $params Array to pass to doApiRequestWithToken(). 'action'
50 * => 'userrights' is implicit. If no 'user' or 'userid' is specified,
51 * we add a 'user' parameter. If no 'add' or 'remove' is specified, we
52 * add 'add' => 'sysop'.
53 * @param User|null $user The user that we're modifying. The user must be
54 * mutable, because we're going to change its groups! null means that
55 * we'll make up our own user to modify, and doesn't make sense if 'user'
56 * or 'userid' is specified in $params.
57 */
58 protected function doSuccessfulRightsChange(
59 $expectedGroups = 'sysop', array $params = [], User $user = null
60 ) {
61 $expectedGroups = (array)$expectedGroups;
62 $params['action'] = 'userrights';
63
64 if ( !$user ) {
65 $user = $this->getMutableTestUser()->getUser();
66 }
67
68 $this->assertTrue( TestUserRegistry::isMutable( $user ),
69 'Immutable user passed to doSuccessfulRightsChange!' );
70
71 if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
72 $params['user'] = $user->getName();
73 }
74 if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
75 $params['add'] = 'sysop';
76 }
77
78 $res = $this->doApiRequestWithToken( $params );
79
80 $user->clearInstanceCache();
81 MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
82 $this->assertSame( $expectedGroups, $user->getGroups() );
83
84 $this->assertArrayNotHasKey( 'warnings', $res[0] );
85 }
86
87 /**
88 * Perform an API userrights request that's expected to fail.
89 *
90 * @param string $expectedException Expected exception text
91 * @param array $params As for doSuccessfulRightsChange()
92 * @param User|null $user As for doSuccessfulRightsChange(). If there's no
93 * user who will possibly be affected (such as if an invalid username is
94 * provided in $params), pass null.
95 */
96 protected function doFailedRightsChange(
97 $expectedException, array $params = [], User $user = null
98 ) {
99 $params['action'] = 'userrights';
100
101 $this->setExpectedException( ApiUsageException::class, $expectedException );
102
103 if ( !$user ) {
104 // If 'user' or 'userid' is specified and $user was not specified,
105 // the user we're creating now will have nothing to do with the API
106 // request, but that's okay, since we're just testing that it has
107 // no groups.
108 $user = $this->getMutableTestUser()->getUser();
109 }
110
111 $this->assertTrue( TestUserRegistry::isMutable( $user ),
112 'Immutable user passed to doFailedRightsChange!' );
113
114 if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
115 $params['user'] = $user->getName();
116 }
117 if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
118 $params['add'] = 'sysop';
119 }
120 $expectedGroups = $user->getGroups();
121
122 try {
123 $this->doApiRequestWithToken( $params );
124 } finally {
125 $user->clearInstanceCache();
126 $this->assertSame( $expectedGroups, $user->getGroups() );
127 }
128 }
129
130 public function testAdd() {
131 $this->doSuccessfulRightsChange();
132 }
133
134 public function testBlockedWithUserrights() {
135 global $wgUser;
136
137 $block = new DatabaseBlock( [ 'address' => $wgUser, 'by' => $wgUser->getId(), ] );
138 $block->insert();
139
140 try {
141 $this->doSuccessfulRightsChange();
142 } finally {
143 $block->delete();
144 $wgUser->clearInstanceCache();
145 }
146 }
147
148 public function testBlockedWithoutUserrights() {
149 $user = $this->getTestSysop()->getUser();
150
151 $this->setPermissions( true, true );
152
153 $block = new DatabaseBlock( [ 'address' => $user, 'by' => $user->getId() ] );
154 $block->insert();
155
156 try {
157 $this->doFailedRightsChange( 'You have been blocked from editing.' );
158 } finally {
159 $block->delete();
160 $user->clearInstanceCache();
161 }
162 }
163
164 public function testAddMultiple() {
165 $this->doSuccessfulRightsChange(
166 [ 'bureaucrat', 'sysop' ],
167 [ 'add' => 'bureaucrat|sysop' ]
168 );
169 }
170
171 public function testTooFewExpiries() {
172 $this->doFailedRightsChange(
173 '2 expiry timestamps were provided where 3 were needed.',
174 [ 'add' => 'sysop|bureaucrat|bot', 'expiry' => 'infinity|tomorrow' ]
175 );
176 }
177
178 public function testTooManyExpiries() {
179 $this->doFailedRightsChange(
180 '3 expiry timestamps were provided where 2 were needed.',
181 [ 'add' => 'sysop|bureaucrat', 'expiry' => 'infinity|tomorrow|never' ]
182 );
183 }
184
185 public function testInvalidExpiry() {
186 $this->doFailedRightsChange( 'Invalid expiry time', [ 'expiry' => 'yummy lollipops!' ] );
187 }
188
189 public function testMultipleInvalidExpiries() {
190 $this->doFailedRightsChange(
191 'Invalid expiry time "foo".',
192 [ 'add' => 'sysop|bureaucrat', 'expiry' => 'foo|bar' ]
193 );
194 }
195
196 public function testWithTag() {
197 ChangeTags::defineTag( 'custom tag' );
198
199 $user = $this->getMutableTestUser()->getUser();
200
201 $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
202
203 $dbr = wfGetDB( DB_REPLICA );
204 $this->assertSame(
205 'custom tag',
206 $dbr->selectField(
207 [ 'change_tag', 'logging', 'change_tag_def' ],
208 'ctd_name',
209 [
210 'ct_log_id = log_id',
211 'log_namespace' => NS_USER,
212 'log_title' => strtr( $user->getName(), ' ', '_' )
213 ],
214 __METHOD__,
215 [ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ]
216 )
217 );
218 }
219
220 public function testWithoutTagPermission() {
221 ChangeTags::defineTag( 'custom tag' );
222
223 $this->setGroupPermissions( 'user', 'applychangetags', false );
224 $this->resetServices();
225
226 $this->doFailedRightsChange(
227 'You do not have permission to apply change tags along with your changes.',
228 [ 'tags' => 'custom tag' ]
229 );
230 }
231
232 public function testNonexistentUser() {
233 $this->doFailedRightsChange(
234 'There is no user by the name "Nonexistent user". Check your spelling.',
235 [ 'user' => 'Nonexistent user' ]
236 );
237 }
238
239 public function testWebToken() {
240 $sysop = $this->getTestSysop()->getUser();
241 $user = $this->getMutableTestUser()->getUser();
242
243 $token = $sysop->getEditToken( $user->getName() );
244
245 $res = $this->doApiRequest( [
246 'action' => 'userrights',
247 'user' => $user->getName(),
248 'add' => 'sysop',
249 'token' => $token,
250 ] );
251
252 $user->clearInstanceCache();
253 $this->assertSame( [ 'sysop' ], $user->getGroups() );
254
255 $this->assertArrayNotHasKey( 'warnings', $res[0] );
256 }
257
258 /**
259 * Helper for testCanProcessExpiries that returns a mock ApiUserrights that either can or cannot
260 * process expiries. Although the regular page can process expiries, we use a mock here to
261 * ensure that it's the result of canProcessExpiries() that makes a difference, and not some
262 * error in the way we construct the mock.
263 *
264 * @param bool $canProcessExpiries
265 */
266 private function getMockForProcessingExpiries( $canProcessExpiries ) {
267 $sysop = $this->getTestSysop()->getUser();
268 $user = $this->getMutableTestUser()->getUser();
269
270 $token = $sysop->getEditToken( 'userrights' );
271
272 $main = new ApiMain( new FauxRequest( [
273 'action' => 'userrights',
274 'user' => $user->getName(),
275 'add' => 'sysop',
276 'token' => $token,
277 ] ) );
278
279 $mockUserRightsPage = $this->getMockBuilder( UserrightsPage::class )
280 ->setMethods( [ 'canProcessExpiries' ] )
281 ->getMock();
282 $mockUserRightsPage->method( 'canProcessExpiries' )->willReturn( $canProcessExpiries );
283
284 $mockApi = $this->getMockBuilder( ApiUserrights::class )
285 ->setConstructorArgs( [ $main, 'userrights' ] )
286 ->setMethods( [ 'getUserRightsPage' ] )
287 ->getMock();
288 $mockApi->method( 'getUserRightsPage' )->willReturn( $mockUserRightsPage );
289
290 return $mockApi;
291 }
292
293 public function testCanProcessExpiries() {
294 $mock1 = $this->getMockForProcessingExpiries( true );
295 $this->assertArrayHasKey( 'expiry', $mock1->getAllowedParams() );
296
297 $mock2 = $this->getMockForProcessingExpiries( false );
298 $this->assertArrayNotHasKey( 'expiry', $mock2->getAllowedParams() );
299 }
300
301 /**
302 * Tests adding and removing various groups with various permissions.
303 *
304 * @dataProvider addAndRemoveGroupsProvider
305 * @param array|null $permissions [ [ $wgAddGroups, $wgRemoveGroups ] ] or null for 'userrights'
306 * to be set in $wgGroupPermissions
307 * @param array $groupsToChange [ [ groups to add ], [ groups to remove ] ]
308 * @param array $expectedGroups Array of expected groups
309 */
310 public function testAddAndRemoveGroups(
311 array $permissions = null, array $groupsToChange, array $expectedGroups
312 ) {
313 if ( $permissions !== null ) {
314 $this->setPermissions( $permissions[0], $permissions[1] );
315 }
316
317 $params = [
318 'add' => implode( '|', $groupsToChange[0] ),
319 'remove' => implode( '|', $groupsToChange[1] ),
320 ];
321
322 // We'll take a bot so we have a group to remove
323 $user = $this->getMutableTestUser( [ 'bot' ] )->getUser();
324
325 $this->doSuccessfulRightsChange( $expectedGroups, $params, $user );
326 }
327
328 public function addAndRemoveGroupsProvider() {
329 return [
330 'Simple add' => [
331 [ [ 'sysop' ], [] ],
332 [ [ 'sysop' ], [] ],
333 [ 'bot', 'sysop' ]
334 ], 'Add with only remove permission' => [
335 [ [], [ 'sysop' ] ],
336 [ [ 'sysop' ], [] ],
337 [ 'bot' ],
338 ], 'Add with global remove permission' => [
339 [ [], true ],
340 [ [ 'sysop' ], [] ],
341 [ 'bot' ],
342 ], 'Simple remove' => [
343 [ [], [ 'bot' ] ],
344 [ [], [ 'bot' ] ],
345 [],
346 ], 'Remove with only add permission' => [
347 [ [ 'bot' ], [] ],
348 [ [], [ 'bot' ] ],
349 [ 'bot' ],
350 ], 'Remove with global add permission' => [
351 [ true, [] ],
352 [ [], [ 'bot' ] ],
353 [ 'bot' ],
354 ], 'Add and remove same new group' => [
355 null,
356 [ [ 'sysop' ], [ 'sysop' ] ],
357 // The userrights code does removals before adds, so it doesn't remove the sysop
358 // group here and only adds it.
359 [ 'bot', 'sysop' ],
360 ], 'Add and remove same existing group' => [
361 null,
362 [ [ 'bot' ], [ 'bot' ] ],
363 // But here it first removes the existing group and then re-adds it.
364 [ 'bot' ],
365 ],
366 ];
367 }
368 }