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