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