Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / tests / phpunit / includes / block / BlockManagerTest.php
1 <?php
2
3 use MediaWiki\Block\BlockManager;
4 use MediaWiki\Block\DatabaseBlock;
5 use MediaWiki\Block\CompositeBlock;
6 use MediaWiki\Block\SystemBlock;
7 use MediaWiki\Config\ServiceOptions;
8 use MediaWiki\MediaWikiServices;
9 use Wikimedia\TestingAccessWrapper;
10
11 /**
12 * @group Blocking
13 * @group Database
14 * @coversDefaultClass \MediaWiki\Block\BlockManager
15 */
16 class BlockManagerTest extends MediaWikiTestCase {
17
18 /** @var User */
19 protected $user;
20
21 /** @var int */
22 protected $sysopId;
23
24 protected function setUp() {
25 parent::setUp();
26
27 $this->user = $this->getTestUser()->getUser();
28 $this->sysopId = $this->getTestSysop()->getUser()->getId();
29 $this->blockManagerConfig = [
30 'wgApplyIpBlocksToXff' => true,
31 'wgCookieSetOnAutoblock' => true,
32 'wgCookieSetOnIpBlock' => true,
33 'wgDnsBlacklistUrls' => [],
34 'wgEnableDnsBlacklist' => true,
35 'wgProxyList' => [],
36 'wgProxyWhitelist' => [],
37 'wgSecretKey' => false,
38 'wgSoftBlockRanges' => [],
39 ];
40 }
41
42 private function getBlockManager( $overrideConfig ) {
43 return new BlockManager(
44 ...$this->getBlockManagerConstructorArgs( $overrideConfig )
45 );
46 }
47
48 private function getBlockManagerConstructorArgs( $overrideConfig ) {
49 $blockManagerConfig = array_merge( $this->blockManagerConfig, $overrideConfig );
50 $this->setMwGlobals( $blockManagerConfig );
51 $this->overrideMwServices();
52 return [
53 new ServiceOptions(
54 BlockManager::$constructorOptions,
55 MediaWikiServices::getInstance()->getMainConfig()
56 ),
57 $this->user,
58 $this->user->getRequest()
59 ];
60 }
61
62 /**
63 * @dataProvider provideGetBlockFromCookieValue
64 * @covers ::getBlockFromCookieValue
65 * @covers ::shouldApplyCookieBlock
66 */
67 public function testGetBlockFromCookieValue( $options, $expected ) {
68 $blockManager = TestingAccessWrapper::newFromObject(
69 $this->getBlockManager( [
70 'wgCookieSetOnAutoblock' => true,
71 'wgCookieSetOnIpBlock' => true,
72 ] )
73 );
74
75 $block = new DatabaseBlock( array_merge( [
76 'address' => $options['target'] ?: $this->user,
77 'by' => $this->sysopId,
78 ], $options['blockOptions'] ) );
79 $block->insert();
80
81 $user = $options['loggedIn'] ? $this->user : new User();
82 $user->getRequest()->setCookie( 'BlockID', $block->getCookieValue() );
83
84 $this->assertSame( $expected, (bool)$blockManager->getBlockFromCookieValue(
85 $user,
86 $user->getRequest()
87 ) );
88
89 $block->delete();
90 }
91
92 public static function provideGetBlockFromCookieValue() {
93 return [
94 'Autoblocking user block' => [
95 [
96 'target' => '',
97 'loggedIn' => true,
98 'blockOptions' => [
99 'enableAutoblock' => true
100 ],
101 ],
102 true,
103 ],
104 'Non-autoblocking user block' => [
105 [
106 'target' => '',
107 'loggedIn' => true,
108 'blockOptions' => [],
109 ],
110 false,
111 ],
112 'IP block for anonymous user' => [
113 [
114 'target' => '127.0.0.1',
115 'loggedIn' => false,
116 'blockOptions' => [],
117 ],
118 true,
119 ],
120 'IP block for logged in user' => [
121 [
122 'target' => '127.0.0.1',
123 'loggedIn' => true,
124 'blockOptions' => [],
125 ],
126 false,
127 ],
128 'IP range block for anonymous user' => [
129 [
130 'target' => '127.0.0.0/8',
131 'loggedIn' => false,
132 'blockOptions' => [],
133 ],
134 true,
135 ],
136 ];
137 }
138
139 /**
140 * @dataProvider provideIsLocallyBlockedProxy
141 * @covers ::isLocallyBlockedProxy
142 */
143 public function testIsLocallyBlockedProxy( $proxyList, $expected ) {
144 $blockManager = TestingAccessWrapper::newFromObject(
145 $this->getBlockManager( [
146 'wgProxyList' => $proxyList
147 ] )
148 );
149
150 $ip = '1.2.3.4';
151 $this->assertSame( $expected, $blockManager->isLocallyBlockedProxy( $ip ) );
152 }
153
154 public static function provideIsLocallyBlockedProxy() {
155 return [
156 'Proxy list is empty' => [ [], false ],
157 'Proxy list contains IP' => [ [ '1.2.3.4' ], true ],
158 'Proxy list contains IP as value' => [ [ 'test' => '1.2.3.4' ], true ],
159 'Proxy list contains range that covers IP' => [ [ '1.2.3.0/16' ], true ],
160 ];
161 }
162
163 /**
164 * @dataProvider provideIsDnsBlacklisted
165 * @covers ::isDnsBlacklisted
166 * @covers ::inDnsBlacklist
167 */
168 public function testIsDnsBlacklisted( $options, $expected ) {
169 $blockManagerConfig = [
170 'wgEnableDnsBlacklist' => true,
171 'wgDnsBlacklistUrls' => $options['blacklist'],
172 'wgProxyWhitelist' => $options['whitelist'],
173 ];
174
175 $blockManager = $this->getMockBuilder( BlockManager::class )
176 ->setConstructorArgs( $this->getBlockManagerConstructorArgs( $blockManagerConfig ) )
177 ->setMethods( [ 'checkHost' ] )
178 ->getMock();
179 $blockManager->method( 'checkHost' )
180 ->will( $this->returnValueMap( [ [
181 $options['dnsblQuery'],
182 $options['dnsblResponse'],
183 ] ] ) );
184
185 $this->assertSame(
186 $expected,
187 $blockManager->isDnsBlacklisted( $options['ip'], $options['checkWhitelist'] )
188 );
189 }
190
191 public static function provideIsDnsBlacklisted() {
192 $dnsblFound = [ '127.0.0.2' ];
193 $dnsblNotFound = false;
194 return [
195 'IP is blacklisted' => [
196 [
197 'blacklist' => [ 'dnsbl.test' ],
198 'ip' => '127.0.0.1',
199 'dnsblQuery' => '1.0.0.127.dnsbl.test',
200 'dnsblResponse' => $dnsblFound,
201 'whitelist' => [],
202 'checkWhitelist' => false,
203 ],
204 true,
205 ],
206 'IP is blacklisted; blacklist has key' => [
207 [
208 'blacklist' => [ [ 'dnsbl.test', 'key' ] ],
209 'ip' => '127.0.0.1',
210 'dnsblQuery' => 'key.1.0.0.127.dnsbl.test',
211 'dnsblResponse' => $dnsblFound,
212 'whitelist' => [],
213 'checkWhitelist' => false,
214 ],
215 true,
216 ],
217 'IP is blacklisted; blacklist is array' => [
218 [
219 'blacklist' => [ [ 'dnsbl.test' ] ],
220 'ip' => '127.0.0.1',
221 'dnsblQuery' => '1.0.0.127.dnsbl.test',
222 'dnsblResponse' => $dnsblFound,
223 'whitelist' => [],
224 'checkWhitelist' => false,
225 ],
226 true,
227 ],
228 'IP is not blacklisted' => [
229 [
230 'blacklist' => [ 'dnsbl.test' ],
231 'ip' => '1.2.3.4',
232 'dnsblQuery' => '4.3.2.1.dnsbl.test',
233 'dnsblResponse' => $dnsblNotFound,
234 'whitelist' => [],
235 'checkWhitelist' => false,
236 ],
237 false,
238 ],
239 'Blacklist is empty' => [
240 [
241 'blacklist' => [],
242 'ip' => '127.0.0.1',
243 'dnsblQuery' => '1.0.0.127.dnsbl.test',
244 'dnsblResponse' => $dnsblFound,
245 'whitelist' => [],
246 'checkWhitelist' => false,
247 ],
248 false,
249 ],
250 'IP is blacklisted and whitelisted; whitelist is not checked' => [
251 [
252 'blacklist' => [ 'dnsbl.test' ],
253 'ip' => '127.0.0.1',
254 'dnsblQuery' => '1.0.0.127.dnsbl.test',
255 'dnsblResponse' => $dnsblFound,
256 'whitelist' => [ '127.0.0.1' ],
257 'checkWhitelist' => false,
258 ],
259 true,
260 ],
261 'IP is blacklisted and whitelisted; whitelist is checked' => [
262 [
263 'blacklist' => [ 'dnsbl.test' ],
264 'ip' => '127.0.0.1',
265 'dnsblQuery' => '1.0.0.127.dnsbl.test',
266 'dnsblResponse' => $dnsblFound,
267 'whitelist' => [ '127.0.0.1' ],
268 'checkWhitelist' => true,
269 ],
270 false,
271 ],
272 ];
273 }
274
275 /**
276 * @covers ::getUniqueBlocks
277 */
278 public function testGetUniqueBlocks() {
279 $blockId = 100;
280
281 $blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
282
283 $block = $this->getMockBuilder( DatabaseBlock::class )
284 ->setMethods( [ 'getId' ] )
285 ->getMock();
286 $block->method( 'getId' )
287 ->willReturn( $blockId );
288
289 $autoblock = $this->getMockBuilder( DatabaseBlock::class )
290 ->setMethods( [ 'getParentBlockId', 'getType' ] )
291 ->getMock();
292 $autoblock->method( 'getParentBlockId' )
293 ->willReturn( $blockId );
294 $autoblock->method( 'getType' )
295 ->willReturn( DatabaseBlock::TYPE_AUTO );
296
297 $blocks = [ $block, $block, $autoblock, new SystemBlock() ];
298
299 $this->assertSame( 2, count( $blockManager->getUniqueBlocks( $blocks ) ) );
300 }
301
302 /**
303 * @dataProvider provideTrackBlockWithCookie
304 * @covers ::trackBlockWithCookie
305 */
306 public function testTrackBlockWithCookie( $options, $expectedVal ) {
307 $this->setMwGlobals( 'wgCookiePrefix', '' );
308
309 $request = new FauxRequest();
310 if ( $options['cookieSet'] ) {
311 $request->setCookie( 'BlockID', 'the value does not matter' );
312 }
313
314 $user = $this->getMockBuilder( User::class )
315 ->setMethods( [ 'getBlock', 'getRequest' ] )
316 ->getMock();
317 $user->method( 'getBlock' )
318 ->willReturn( $options['block'] );
319 $user->method( 'getRequest' )
320 ->willReturn( $request );
321
322 // Although the block cookie is set via DeferredUpdates, in command line mode updates are
323 // processed immediately
324 $blockManager = $this->getBlockManager( [
325 'wgSecretKey' => '',
326 'wgCookieSetOnIpBlock' => true,
327 ] );
328 $blockManager->trackBlockWithCookie( $user );
329
330 /** @var FauxResponse $response */
331 $response = $request->response();
332 $this->assertCount( $expectedVal ? 1 : 0, $response->getCookies() );
333 $this->assertEquals( $expectedVal ?: null, $response->getCookie( 'BlockID' ) );
334 }
335
336 public function provideTrackBlockWithCookie() {
337 $blockId = 123;
338 return [
339 'Block cookie is already set; there is a trackable block' => [
340 [
341 'cookieSet' => true,
342 'block' => $this->getTrackableBlock( $blockId ),
343 ],
344 null,
345 ],
346 'Block cookie is already set; there is no block' => [
347 [
348 'cookieSet' => true,
349 'block' => null,
350 ],
351 null,
352 ],
353 'Block cookie is not yet set; there is no block' => [
354 [
355 'cookieSet' => false,
356 'block' => null,
357 ],
358 null,
359 ],
360 'Block cookie is not yet set; there is a trackable block' => [
361 [
362 'cookieSet' => false,
363 'block' => $this->getTrackableBlock( $blockId ),
364 ],
365 $blockId,
366 ],
367 'Block cookie is not yet set; there is a composite block with a trackable block' => [
368 [
369 'cookieSet' => false,
370 'block' => new CompositeBlock( [
371 'originalBlocks' => [
372 new SystemBlock(),
373 $this->getTrackableBlock( $blockId ),
374 ]
375 ] ),
376 ],
377 $blockId,
378 ],
379 'Block cookie is not yet set; there is a composite block but no trackable block' => [
380 [
381 'cookieSet' => false,
382 'block' => new CompositeBlock( [
383 'originalBlocks' => [
384 new SystemBlock(),
385 new SystemBlock(),
386 ]
387 ] ),
388 ],
389 null,
390 ],
391 ];
392 }
393
394 private function getTrackableBlock( $blockId ) {
395 $block = $this->getMockBuilder( DatabaseBlock::class )
396 ->setMethods( [ 'getType', 'getId' ] )
397 ->getMock();
398 $block->method( 'getType' )
399 ->willReturn( DatabaseBlock::TYPE_IP );
400 $block->method( 'getId' )
401 ->willReturn( $blockId );
402 return $block;
403 }
404
405 /**
406 * @dataProvider provideSetBlockCookie
407 * @covers ::setBlockCookie
408 */
409 public function testSetBlockCookie( $expiryDelta, $expectedExpiryDelta ) {
410 $this->setMwGlobals( [
411 'wgCookiePrefix' => '',
412 ] );
413
414 $request = new FauxRequest();
415 $response = $request->response();
416
417 $blockManager = $this->getBlockManager( [
418 'wgSecretKey' => '',
419 'wgCookieSetOnIpBlock' => true,
420 ] );
421
422 $now = wfTimestamp();
423
424 $block = new DatabaseBlock( [
425 'expiry' => $expiryDelta === '' ? '' : $now + $expiryDelta
426 ] );
427 $blockManager->setBlockCookie( $block, $response );
428 $cookies = $response->getCookies();
429
430 $this->assertEquals(
431 $now + $expectedExpiryDelta,
432 $cookies['BlockID']['expire'],
433 '',
434 60 // Allow actual to be up to 60 seconds later than expected
435 );
436 }
437
438 public static function provideSetBlockCookie() {
439 // Maximum length of a block cookie, defined in BlockManager::setBlockCookie
440 $maxExpiryDelta = ( 24 * 60 * 60 );
441
442 $longExpiryDelta = ( 48 * 60 * 60 );
443 $shortExpiryDelta = ( 12 * 60 * 60 );
444
445 return [
446 'Block has indefinite expiry' => [
447 '',
448 $maxExpiryDelta,
449 ],
450 'Block expiry is later than maximum cookie block expiry' => [
451 $longExpiryDelta,
452 $maxExpiryDelta,
453 ],
454 'Block expiry is sooner than maximum cookie block expiry' => [
455 $shortExpiryDelta,
456 $shortExpiryDelta,
457 ],
458 ];
459 }
460
461 /**
462 * @covers ::shouldTrackBlockWithCookie
463 */
464 public function testShouldTrackBlockWithCookieSystemBlock() {
465 $blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
466 $this->assertFalse( $blockManager->shouldTrackBlockWithCookie(
467 new SystemBlock(),
468 true
469 ) );
470 }
471
472 /**
473 * @dataProvider provideShouldTrackBlockWithCookie
474 * @covers ::shouldTrackBlockWithCookie
475 */
476 public function testShouldTrackBlockWithCookie( $options, $expected ) {
477 $block = $this->getMockBuilder( DatabaseBlock::class )
478 ->setMethods( [ 'getType', 'isAutoblocking' ] )
479 ->getMock();
480 $block->method( 'getType' )
481 ->willReturn( $options['type'] );
482 if ( isset( $options['autoblocking'] ) ) {
483 $block->method( 'isAutoblocking' )
484 ->willReturn( $options['autoblocking'] );
485 }
486
487 $blockManager = TestingAccessWrapper::newFromObject(
488 $this->getBlockManager( $options['blockManagerConfig'] )
489 );
490
491 $this->assertSame(
492 $expected,
493 $blockManager->shouldTrackBlockWithCookie( $block, $options['isAnon'] )
494 );
495 }
496
497 public static function provideShouldTrackBlockWithCookie() {
498 return [
499 'IP block, anonymous user, IP block cookies enabled' => [
500 [
501 'type' => DatabaseBlock::TYPE_IP,
502 'isAnon' => true,
503 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
504 ],
505 true
506 ],
507 'IP range block, anonymous user, IP block cookies enabled' => [
508 [
509 'type' => DatabaseBlock::TYPE_RANGE,
510 'isAnon' => true,
511 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
512 ],
513 true
514 ],
515 'IP block, anonymous user, IP block cookies disabled' => [
516 [
517 'type' => DatabaseBlock::TYPE_IP,
518 'isAnon' => true,
519 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => false ],
520 ],
521 false
522 ],
523 'IP block, logged in user, IP block cookies enabled' => [
524 [
525 'type' => DatabaseBlock::TYPE_IP,
526 'isAnon' => false,
527 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
528 ],
529 false
530 ],
531 'User block, anonymous, autoblock cookies enabled, block is autoblocking' => [
532 [
533 'type' => DatabaseBlock::TYPE_USER,
534 'isAnon' => true,
535 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
536 'autoblocking' => true,
537 ],
538 false
539 ],
540 'User block, logged in, autoblock cookies enabled, block is autoblocking' => [
541 [
542 'type' => DatabaseBlock::TYPE_USER,
543 'isAnon' => false,
544 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
545 'autoblocking' => true,
546 ],
547 true
548 ],
549 'User block, logged in, autoblock cookies disabled, block is autoblocking' => [
550 [
551 'type' => DatabaseBlock::TYPE_USER,
552 'isAnon' => false,
553 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => false ],
554 'autoblocking' => true,
555 ],
556 false
557 ],
558 'User block, logged in, autoblock cookies enabled, block is not autoblocking' => [
559 [
560 'type' => DatabaseBlock::TYPE_USER,
561 'isAnon' => false,
562 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
563 'autoblocking' => false,
564 ],
565 false
566 ],
567 'Block type is autoblock' => [
568 [
569 'type' => DatabaseBlock::TYPE_AUTO,
570 'isAnon' => true,
571 'blockManagerConfig' => [],
572 ],
573 false
574 ]
575 ];
576 }
577
578 /**
579 * @covers ::clearBlockCookie
580 */
581 public function testClearBlockCookie() {
582 $this->setMwGlobals( [
583 'wgCookiePrefix' => '',
584 ] );
585
586 $request = new FauxRequest();
587 $response = $request->response();
588 $response->setCookie( 'BlockID', '100' );
589 $this->assertSame( '100', $response->getCookie( 'BlockID' ) );
590
591 BlockManager::clearBlockCookie( $response );
592 $this->assertSame( '', $response->getCookie( 'BlockID' ) );
593 }
594
595 /**
596 * @dataProvider provideGetIdFromCookieValue
597 * @covers ::getIdFromCookieValue
598 */
599 public function testGetIdFromCookieValue( $options, $expected ) {
600 $blockManager = $this->getBlockManager( [
601 'wgSecretKey' => $options['secretKey']
602 ] );
603 $this->assertEquals(
604 $expected,
605 $blockManager->getIdFromCookieValue( $options['cookieValue'] )
606 );
607 }
608
609 public static function provideGetIdFromCookieValue() {
610 $blockId = 100;
611 $secretKey = '123';
612 $hmac = MWCryptHash::hmac( $blockId, $secretKey, false );
613 return [
614 'No secret key is set' => [
615 [
616 'secretKey' => '',
617 'cookieValue' => $blockId,
618 'calculatedHmac' => MWCryptHash::hmac( $blockId, '', false ),
619 ],
620 $blockId,
621 ],
622 'Secret key is set and stored hmac is correct' => [
623 [
624 'secretKey' => $secretKey,
625 'cookieValue' => $blockId . '!' . $hmac,
626 'calculatedHmac' => $hmac,
627 ],
628 $blockId,
629 ],
630 'Secret key is set and stored hmac is incorrect' => [
631 [
632 'secretKey' => $secretKey,
633 'cookieValue' => $blockId . '!xyz',
634 'calculatedHmac' => $hmac,
635 ],
636 null,
637 ],
638 ];
639 }
640
641 /**
642 * @dataProvider provideGetCookieValue
643 * @covers ::getCookieValue
644 */
645 public function testGetCookieValue( $options, $expected ) {
646 $blockManager = $this->getBlockManager( [
647 'wgSecretKey' => $options['secretKey']
648 ] );
649
650 $block = $this->getMockBuilder( DatabaseBlock::class )
651 ->setMethods( [ 'getId' ] )
652 ->getMock();
653 $block->method( 'getId' )
654 ->willReturn( $options['blockId'] );
655
656 $this->assertEquals(
657 $expected,
658 $blockManager->getCookieValue( $block )
659 );
660 }
661
662 public static function provideGetCookieValue() {
663 $blockId = 100;
664 return [
665 'Secret key not set' => [
666 [
667 'secretKey' => '',
668 'blockId' => $blockId,
669 'hmac' => MWCryptHash::hmac( $blockId, '', false ),
670 ],
671 $blockId,
672 ],
673 'Secret key set' => [
674 [
675 'secretKey' => '123',
676 'blockId' => $blockId,
677 'hmac' => MWCryptHash::hmac( $blockId, '123', false ),
678 ],
679 $blockId . '!' . MWCryptHash::hmac( $blockId, '123', false ) ],
680 ];
681 }
682
683 }