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