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