Merge "Exclude redirects from Special:Fewestrevisions"
[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 * @covers ::isLocallyBlockedProxy
165 */
166 public function testIsLocallyBlockedProxyDeprecated() {
167 $proxy = '1.2.3.4';
168
169 $this->hideDeprecated(
170 'IP addresses in the keys of $wgProxyList (found the following IP ' .
171 'addresses in keys: ' . $proxy . ', please move them to values)'
172 );
173
174 $blockManager = TestingAccessWrapper::newFromObject(
175 $this->getBlockManager( [
176 'wgProxyList' => [ $proxy => 'test' ]
177 ] )
178 );
179
180 $ip = '1.2.3.4';
181 $this->assertTrue( $blockManager->isLocallyBlockedProxy( $ip ) );
182 }
183
184 /**
185 * @dataProvider provideIsDnsBlacklisted
186 * @covers ::isDnsBlacklisted
187 * @covers ::inDnsBlacklist
188 */
189 public function testIsDnsBlacklisted( $options, $expected ) {
190 $blockManagerConfig = [
191 'wgEnableDnsBlacklist' => true,
192 'wgDnsBlacklistUrls' => $options['blacklist'],
193 'wgProxyWhitelist' => $options['whitelist'],
194 ];
195
196 $blockManager = $this->getMockBuilder( BlockManager::class )
197 ->setConstructorArgs( $this->getBlockManagerConstructorArgs( $blockManagerConfig ) )
198 ->setMethods( [ 'checkHost' ] )
199 ->getMock();
200 $blockManager->method( 'checkHost' )
201 ->will( $this->returnValueMap( [ [
202 $options['dnsblQuery'],
203 $options['dnsblResponse'],
204 ] ] ) );
205
206 $this->assertSame(
207 $expected,
208 $blockManager->isDnsBlacklisted( $options['ip'], $options['checkWhitelist'] )
209 );
210 }
211
212 public static function provideIsDnsBlacklisted() {
213 $dnsblFound = [ '127.0.0.2' ];
214 $dnsblNotFound = false;
215 return [
216 'IP is blacklisted' => [
217 [
218 'blacklist' => [ 'dnsbl.test' ],
219 'ip' => '127.0.0.1',
220 'dnsblQuery' => '1.0.0.127.dnsbl.test',
221 'dnsblResponse' => $dnsblFound,
222 'whitelist' => [],
223 'checkWhitelist' => false,
224 ],
225 true,
226 ],
227 'IP is blacklisted; blacklist has key' => [
228 [
229 'blacklist' => [ [ 'dnsbl.test', 'key' ] ],
230 'ip' => '127.0.0.1',
231 'dnsblQuery' => 'key.1.0.0.127.dnsbl.test',
232 'dnsblResponse' => $dnsblFound,
233 'whitelist' => [],
234 'checkWhitelist' => false,
235 ],
236 true,
237 ],
238 'IP is blacklisted; blacklist is array' => [
239 [
240 'blacklist' => [ [ 'dnsbl.test' ] ],
241 'ip' => '127.0.0.1',
242 'dnsblQuery' => '1.0.0.127.dnsbl.test',
243 'dnsblResponse' => $dnsblFound,
244 'whitelist' => [],
245 'checkWhitelist' => false,
246 ],
247 true,
248 ],
249 'IP is not blacklisted' => [
250 [
251 'blacklist' => [ 'dnsbl.test' ],
252 'ip' => '1.2.3.4',
253 'dnsblQuery' => '4.3.2.1.dnsbl.test',
254 'dnsblResponse' => $dnsblNotFound,
255 'whitelist' => [],
256 'checkWhitelist' => false,
257 ],
258 false,
259 ],
260 'Blacklist is empty' => [
261 [
262 'blacklist' => [],
263 'ip' => '127.0.0.1',
264 'dnsblQuery' => '1.0.0.127.dnsbl.test',
265 'dnsblResponse' => $dnsblFound,
266 'whitelist' => [],
267 'checkWhitelist' => false,
268 ],
269 false,
270 ],
271 'IP is blacklisted and whitelisted; whitelist is not checked' => [
272 [
273 'blacklist' => [ 'dnsbl.test' ],
274 'ip' => '127.0.0.1',
275 'dnsblQuery' => '1.0.0.127.dnsbl.test',
276 'dnsblResponse' => $dnsblFound,
277 'whitelist' => [ '127.0.0.1' ],
278 'checkWhitelist' => false,
279 ],
280 true,
281 ],
282 'IP is blacklisted and whitelisted; whitelist is checked' => [
283 [
284 'blacklist' => [ 'dnsbl.test' ],
285 'ip' => '127.0.0.1',
286 'dnsblQuery' => '1.0.0.127.dnsbl.test',
287 'dnsblResponse' => $dnsblFound,
288 'whitelist' => [ '127.0.0.1' ],
289 'checkWhitelist' => true,
290 ],
291 false,
292 ],
293 ];
294 }
295
296 /**
297 * @covers ::getUniqueBlocks
298 */
299 public function testGetUniqueBlocks() {
300 $blockId = 100;
301
302 $blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
303
304 $block = $this->getMockBuilder( DatabaseBlock::class )
305 ->setMethods( [ 'getId' ] )
306 ->getMock();
307 $block->method( 'getId' )
308 ->willReturn( $blockId );
309
310 $autoblock = $this->getMockBuilder( DatabaseBlock::class )
311 ->setMethods( [ 'getParentBlockId', 'getType' ] )
312 ->getMock();
313 $autoblock->method( 'getParentBlockId' )
314 ->willReturn( $blockId );
315 $autoblock->method( 'getType' )
316 ->willReturn( DatabaseBlock::TYPE_AUTO );
317
318 $blocks = [ $block, $block, $autoblock, new SystemBlock() ];
319
320 $this->assertSame( 2, count( $blockManager->getUniqueBlocks( $blocks ) ) );
321 }
322
323 /**
324 * @dataProvider provideTrackBlockWithCookie
325 * @covers ::trackBlockWithCookie
326 */
327 public function testTrackBlockWithCookie( $options, $expectedVal ) {
328 $this->setMwGlobals( 'wgCookiePrefix', '' );
329
330 $request = new FauxRequest();
331 if ( $options['cookieSet'] ) {
332 $request->setCookie( 'BlockID', 'the value does not matter' );
333 }
334
335 $user = $this->getMockBuilder( User::class )
336 ->setMethods( [ 'getBlock', 'getRequest' ] )
337 ->getMock();
338 $user->method( 'getBlock' )
339 ->willReturn( $options['block'] );
340 $user->method( 'getRequest' )
341 ->willReturn( $request );
342
343 // Although the block cookie is set via DeferredUpdates, in command line mode updates are
344 // processed immediately
345 $blockManager = $this->getBlockManager( [
346 'wgSecretKey' => '',
347 'wgCookieSetOnIpBlock' => true,
348 ] );
349 $blockManager->trackBlockWithCookie( $user );
350
351 /** @var FauxResponse $response */
352 $response = $request->response();
353 $this->assertCount( $expectedVal ? 1 : 0, $response->getCookies() );
354 $this->assertEquals( $expectedVal ?: null, $response->getCookie( 'BlockID' ) );
355 }
356
357 public function provideTrackBlockWithCookie() {
358 $blockId = 123;
359 return [
360 'Block cookie is already set; there is a trackable block' => [
361 [
362 'cookieSet' => true,
363 'block' => $this->getTrackableBlock( $blockId ),
364 ],
365 null,
366 ],
367 'Block cookie is already set; there is no block' => [
368 [
369 'cookieSet' => true,
370 'block' => null,
371 ],
372 null,
373 ],
374 'Block cookie is not yet set; there is no block' => [
375 [
376 'cookieSet' => false,
377 'block' => null,
378 ],
379 null,
380 ],
381 'Block cookie is not yet set; there is a trackable block' => [
382 [
383 'cookieSet' => false,
384 'block' => $this->getTrackableBlock( $blockId ),
385 ],
386 $blockId,
387 ],
388 'Block cookie is not yet set; there is a composite block with a trackable block' => [
389 [
390 'cookieSet' => false,
391 'block' => new CompositeBlock( [
392 'originalBlocks' => [
393 new SystemBlock(),
394 $this->getTrackableBlock( $blockId ),
395 ]
396 ] ),
397 ],
398 $blockId,
399 ],
400 'Block cookie is not yet set; there is a composite block but no trackable block' => [
401 [
402 'cookieSet' => false,
403 'block' => new CompositeBlock( [
404 'originalBlocks' => [
405 new SystemBlock(),
406 new SystemBlock(),
407 ]
408 ] ),
409 ],
410 null,
411 ],
412 ];
413 }
414
415 private function getTrackableBlock( $blockId ) {
416 $block = $this->getMockBuilder( DatabaseBlock::class )
417 ->setMethods( [ 'getType', 'getId' ] )
418 ->getMock();
419 $block->method( 'getType' )
420 ->willReturn( DatabaseBlock::TYPE_IP );
421 $block->method( 'getId' )
422 ->willReturn( $blockId );
423 return $block;
424 }
425
426 /**
427 * @dataProvider provideSetBlockCookie
428 * @covers ::setBlockCookie
429 */
430 public function testSetBlockCookie( $expiryDelta, $expectedExpiryDelta ) {
431 $this->setMwGlobals( [
432 'wgCookiePrefix' => '',
433 ] );
434
435 $request = new FauxRequest();
436 $response = $request->response();
437
438 $blockManager = $this->getBlockManager( [
439 'wgSecretKey' => '',
440 'wgCookieSetOnIpBlock' => true,
441 ] );
442
443 $now = wfTimestamp();
444
445 $block = new DatabaseBlock( [
446 'expiry' => $expiryDelta === '' ? '' : $now + $expiryDelta
447 ] );
448 $blockManager->setBlockCookie( $block, $response );
449 $cookies = $response->getCookies();
450
451 $this->assertEquals(
452 $now + $expectedExpiryDelta,
453 $cookies['BlockID']['expire'],
454 '',
455 60 // Allow actual to be up to 60 seconds later than expected
456 );
457 }
458
459 public static function provideSetBlockCookie() {
460 // Maximum length of a block cookie, defined in BlockManager::setBlockCookie
461 $maxExpiryDelta = ( 24 * 60 * 60 );
462
463 $longExpiryDelta = ( 48 * 60 * 60 );
464 $shortExpiryDelta = ( 12 * 60 * 60 );
465
466 return [
467 'Block has indefinite expiry' => [
468 '',
469 $maxExpiryDelta,
470 ],
471 'Block expiry is later than maximum cookie block expiry' => [
472 $longExpiryDelta,
473 $maxExpiryDelta,
474 ],
475 'Block expiry is sooner than maximum cookie block expiry' => [
476 $shortExpiryDelta,
477 $shortExpiryDelta,
478 ],
479 ];
480 }
481
482 /**
483 * @covers ::shouldTrackBlockWithCookie
484 */
485 public function testShouldTrackBlockWithCookieSystemBlock() {
486 $blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
487 $this->assertFalse( $blockManager->shouldTrackBlockWithCookie(
488 new SystemBlock(),
489 true
490 ) );
491 }
492
493 /**
494 * @dataProvider provideShouldTrackBlockWithCookie
495 * @covers ::shouldTrackBlockWithCookie
496 */
497 public function testShouldTrackBlockWithCookie( $options, $expected ) {
498 $block = $this->getMockBuilder( DatabaseBlock::class )
499 ->setMethods( [ 'getType', 'isAutoblocking' ] )
500 ->getMock();
501 $block->method( 'getType' )
502 ->willReturn( $options['type'] );
503 if ( isset( $options['autoblocking'] ) ) {
504 $block->method( 'isAutoblocking' )
505 ->willReturn( $options['autoblocking'] );
506 }
507
508 $blockManager = TestingAccessWrapper::newFromObject(
509 $this->getBlockManager( $options['blockManagerConfig'] )
510 );
511
512 $this->assertSame(
513 $expected,
514 $blockManager->shouldTrackBlockWithCookie( $block, $options['isAnon'] )
515 );
516 }
517
518 public static function provideShouldTrackBlockWithCookie() {
519 return [
520 'IP block, anonymous user, IP block cookies enabled' => [
521 [
522 'type' => DatabaseBlock::TYPE_IP,
523 'isAnon' => true,
524 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
525 ],
526 true
527 ],
528 'IP range block, anonymous user, IP block cookies enabled' => [
529 [
530 'type' => DatabaseBlock::TYPE_RANGE,
531 'isAnon' => true,
532 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
533 ],
534 true
535 ],
536 'IP block, anonymous user, IP block cookies disabled' => [
537 [
538 'type' => DatabaseBlock::TYPE_IP,
539 'isAnon' => true,
540 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => false ],
541 ],
542 false
543 ],
544 'IP block, logged in user, IP block cookies enabled' => [
545 [
546 'type' => DatabaseBlock::TYPE_IP,
547 'isAnon' => false,
548 'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
549 ],
550 false
551 ],
552 'User block, anonymous, autoblock cookies enabled, block is autoblocking' => [
553 [
554 'type' => DatabaseBlock::TYPE_USER,
555 'isAnon' => true,
556 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
557 'autoblocking' => true,
558 ],
559 false
560 ],
561 'User block, logged in, autoblock cookies enabled, block is autoblocking' => [
562 [
563 'type' => DatabaseBlock::TYPE_USER,
564 'isAnon' => false,
565 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
566 'autoblocking' => true,
567 ],
568 true
569 ],
570 'User block, logged in, autoblock cookies disabled, block is autoblocking' => [
571 [
572 'type' => DatabaseBlock::TYPE_USER,
573 'isAnon' => false,
574 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => false ],
575 'autoblocking' => true,
576 ],
577 false
578 ],
579 'User block, logged in, autoblock cookies enabled, block is not autoblocking' => [
580 [
581 'type' => DatabaseBlock::TYPE_USER,
582 'isAnon' => false,
583 'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
584 'autoblocking' => false,
585 ],
586 false
587 ],
588 'Block type is autoblock' => [
589 [
590 'type' => DatabaseBlock::TYPE_AUTO,
591 'isAnon' => true,
592 'blockManagerConfig' => [],
593 ],
594 false
595 ]
596 ];
597 }
598
599 /**
600 * @covers ::clearBlockCookie
601 */
602 public function testClearBlockCookie() {
603 $this->setMwGlobals( [
604 'wgCookiePrefix' => '',
605 ] );
606
607 $request = new FauxRequest();
608 $response = $request->response();
609 $response->setCookie( 'BlockID', '100' );
610 $this->assertSame( '100', $response->getCookie( 'BlockID' ) );
611
612 BlockManager::clearBlockCookie( $response );
613 $this->assertSame( '', $response->getCookie( 'BlockID' ) );
614 }
615
616 /**
617 * @dataProvider provideGetIdFromCookieValue
618 * @covers ::getIdFromCookieValue
619 */
620 public function testGetIdFromCookieValue( $options, $expected ) {
621 $blockManager = $this->getBlockManager( [
622 'wgSecretKey' => $options['secretKey']
623 ] );
624 $this->assertEquals(
625 $expected,
626 $blockManager->getIdFromCookieValue( $options['cookieValue'] )
627 );
628 }
629
630 public static function provideGetIdFromCookieValue() {
631 $blockId = 100;
632 $secretKey = '123';
633 $hmac = MWCryptHash::hmac( $blockId, $secretKey, false );
634 return [
635 'No secret key is set' => [
636 [
637 'secretKey' => '',
638 'cookieValue' => $blockId,
639 'calculatedHmac' => MWCryptHash::hmac( $blockId, '', false ),
640 ],
641 $blockId,
642 ],
643 'Secret key is set and stored hmac is correct' => [
644 [
645 'secretKey' => $secretKey,
646 'cookieValue' => $blockId . '!' . $hmac,
647 'calculatedHmac' => $hmac,
648 ],
649 $blockId,
650 ],
651 'Secret key is set and stored hmac is incorrect' => [
652 [
653 'secretKey' => $secretKey,
654 'cookieValue' => $blockId . '!xyz',
655 'calculatedHmac' => $hmac,
656 ],
657 null,
658 ],
659 ];
660 }
661
662 /**
663 * @dataProvider provideGetCookieValue
664 * @covers ::getCookieValue
665 */
666 public function testGetCookieValue( $options, $expected ) {
667 $blockManager = $this->getBlockManager( [
668 'wgSecretKey' => $options['secretKey']
669 ] );
670
671 $block = $this->getMockBuilder( DatabaseBlock::class )
672 ->setMethods( [ 'getId' ] )
673 ->getMock();
674 $block->method( 'getId' )
675 ->willReturn( $options['blockId'] );
676
677 $this->assertEquals(
678 $expected,
679 $blockManager->getCookieValue( $block )
680 );
681 }
682
683 public static function provideGetCookieValue() {
684 $blockId = 100;
685 return [
686 'Secret key not set' => [
687 [
688 'secretKey' => '',
689 'blockId' => $blockId,
690 'hmac' => MWCryptHash::hmac( $blockId, '', false ),
691 ],
692 $blockId,
693 ],
694 'Secret key set' => [
695 [
696 'secretKey' => '123',
697 'blockId' => $blockId,
698 'hmac' => MWCryptHash::hmac( $blockId, '123', false ),
699 ],
700 $blockId . '!' . MWCryptHash::hmac( $blockId, '123', false ) ],
701 ];
702 }
703
704 }