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