wiki.techinc.nl/tests/phpunit/includes/block/BlockManagerTest.php
Tim Starling 68c433bd23 Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.

General principles:
* Use DI if it is already used. We're not changing the way state is
  managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
  is a service, it's a more generic interface, it is the only
  thing that provides isRegistered() which is needed in some cases,
  and a HookRunner can be efficiently constructed from it
  (confirmed by benchmark). Because HookContainer is needed
  for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
  SpecialPage and ApiBase have getHookContainer() and getHookRunner()
  methods in the base class, and classes that extend that base class
  are not expected to know or care where the base class gets its
  HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
  getHookRunner() methods, getting them from the global service
  container. The point of this is to ease migration to DI by ensuring
  that call sites ask their local friendly base class rather than
  getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
  methods did not seem warranted, there is a private HookRunner property
  which is accessed directly. Very rarely (two cases), there is a
  protected property, for consistency with code that conventionally
  assumes protected=private, but in cases where the class might actually
  be overridden, a protected accessor is preferred over a protected
  property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
  global code. In a few cases it was used for objects with broken
  construction schemes, out of horror or laziness.

Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore

Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router

setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine

Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-05-30 14:23:28 +00:00

755 lines
19 KiB
PHP

<?php
use MediaWiki\Block\BlockManager;
use MediaWiki\Block\CompositeBlock;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\SystemBlock;
use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;
use Wikimedia\TestingAccessWrapper;
/**
* @group Blocking
* @group Database
* @coversDefaultClass \MediaWiki\Block\BlockManager
*/
class BlockManagerTest extends MediaWikiTestCase {
use TestAllServiceOptionsUsed;
/** @var User */
protected $user;
/** @var int */
protected $sysopId;
protected function setUp() : void {
parent::setUp();
$this->user = $this->getTestUser()->getUser();
$this->sysopId = $this->getTestSysop()->getUser()->getId();
$this->blockManagerConfig = [
'wgApplyIpBlocksToXff' => true,
'wgCookieSetOnAutoblock' => true,
'wgCookieSetOnIpBlock' => true,
'wgDnsBlacklistUrls' => [],
'wgEnableDnsBlacklist' => true,
'wgProxyList' => [],
'wgProxyWhitelist' => [],
'wgSecretKey' => false,
'wgSoftBlockRanges' => [],
];
}
private function getBlockManager( $overrideConfig ) {
return new BlockManager(
...$this->getBlockManagerConstructorArgs( $overrideConfig )
);
}
private function getBlockManagerConstructorArgs( $overrideConfig ) {
$blockManagerConfig = array_merge( $this->blockManagerConfig, $overrideConfig );
$this->setMwGlobals( $blockManagerConfig );
$logger = $this->getMockBuilder( LoggerInterface::class )->getMock();
return [
new LoggedServiceOptions(
self::$serviceOptionsAccessLog,
BlockManager::CONSTRUCTOR_OPTIONS,
MediaWikiServices::getInstance()->getMainConfig()
),
MediaWikiServices::getInstance()->getPermissionManager(),
$logger,
MediaWikiServices::getInstance()->getHookContainer()
];
}
/**
* @dataProvider provideBlocksForShouldApplyCookieBlock
* @covers ::getBlockFromCookieValue
* @covers ::shouldApplyCookieBlock
*/
public function testGetBlockFromCookieValue( $options, $expected ) {
$blockManager = TestingAccessWrapper::newFromObject(
$this->getBlockManager( [
'wgCookieSetOnAutoblock' => true,
'wgCookieSetOnIpBlock' => true,
] )
);
$block = new DatabaseBlock( array_merge( [
'address' => $options['target'] ?: $this->user,
'by' => $this->sysopId,
], $options['blockOptions'] ) );
$block->insert();
$user = $options['loggedIn'] ? $this->user : new User();
$user->getRequest()->setCookie( 'BlockID', $blockManager->getCookieValue( $block ) );
$this->assertSame( $expected, (bool)$blockManager->getBlockFromCookieValue(
$user,
$user->getRequest()
) );
$block->delete();
}
/**
* @dataProvider provideBlocksForShouldApplyCookieBlock
* @covers ::trackBlockWithCookie
* @covers ::shouldApplyCookieBlock
*/
public function testTrackBlockWithCookieRemovesBlocks( $options, $expectKeepCookie ) {
$blockManager = TestingAccessWrapper::newFromObject(
$this->getBlockManager( [
'wgCookieSetOnAutoblock' => true,
'wgCookieSetOnIpBlock' => true,
] )
);
$block = new DatabaseBlock( array_merge( [
'address' => $options['target'] ?: $this->user,
'by' => $this->sysopId,
], $options['blockOptions'] ) );
$block->insert();
$user = $options['loggedIn'] ? $this->user : new User();
$user->getRequest()->setCookie( 'BlockID', $blockManager->getCookieValue( $block ) );
$blockManager->trackBlockWithCookie(
$user,
$user->getRequest()->response()
);
$this->assertCount(
$expectKeepCookie ? 0 : 1,
$user->getRequest()->response()->getCookies()
);
$block->delete();
}
public static function provideBlocksForShouldApplyCookieBlock() {
return [
'Autoblocking user block' => [
[
'target' => '',
'loggedIn' => true,
'blockOptions' => [
'enableAutoblock' => true
],
],
true,
],
'Autoblocking user block for anonymous user' => [
[
'target' => '',
'loggedIn' => false,
'blockOptions' => [
'enableAutoblock' => true
],
],
true,
],
'Non-autoblocking user block' => [
[
'target' => '',
'loggedIn' => true,
'blockOptions' => [],
],
false,
],
'IP block for anonymous user' => [
[
'target' => '127.0.0.1',
'loggedIn' => false,
'blockOptions' => [],
],
true,
],
'IP block for logged in user' => [
[
'target' => '127.0.0.1',
'loggedIn' => true,
'blockOptions' => [],
],
false,
],
'IP range block for anonymous user' => [
[
'target' => '127.0.0.0/8',
'loggedIn' => false,
'blockOptions' => [],
],
true,
],
];
}
/**
* @dataProvider provideIsLocallyBlockedProxy
* @covers ::isLocallyBlockedProxy
*/
public function testIsLocallyBlockedProxy( $proxyList, $expected ) {
$blockManager = TestingAccessWrapper::newFromObject(
$this->getBlockManager( [
'wgProxyList' => $proxyList
] )
);
$ip = '1.2.3.4';
$this->assertSame( $expected, $blockManager->isLocallyBlockedProxy( $ip ) );
}
public static function provideIsLocallyBlockedProxy() {
return [
'Proxy list is empty' => [ [], false ],
'Proxy list contains IP' => [ [ '1.2.3.4' ], true ],
'Proxy list contains IP as value' => [ [ 'test' => '1.2.3.4' ], true ],
'Proxy list contains range that covers IP' => [ [ '1.2.3.0/16' ], true ],
];
}
/**
* @dataProvider provideIsDnsBlacklisted
* @covers ::isDnsBlacklisted
* @covers ::inDnsBlacklist
*/
public function testIsDnsBlacklisted( $options, $expected ) {
$blockManagerConfig = [
'wgEnableDnsBlacklist' => true,
'wgDnsBlacklistUrls' => $options['blacklist'],
'wgProxyWhitelist' => $options['whitelist'],
];
$blockManager = $this->getMockBuilder( BlockManager::class )
->setConstructorArgs( $this->getBlockManagerConstructorArgs( $blockManagerConfig ) )
->setMethods( [ 'checkHost' ] )
->getMock();
$blockManager->method( 'checkHost' )
->will( $this->returnValueMap( [ [
$options['dnsblQuery'],
$options['dnsblResponse'],
] ] ) );
$this->assertSame(
$expected,
$blockManager->isDnsBlacklisted( $options['ip'], $options['checkWhitelist'] )
);
}
public static function provideIsDnsBlacklisted() {
$dnsblFound = [ '127.0.0.2' ];
$dnsblNotFound = false;
return [
'IP is blacklisted' => [
[
'blacklist' => [ 'dnsbl.test' ],
'ip' => '127.0.0.1',
'dnsblQuery' => '1.0.0.127.dnsbl.test',
'dnsblResponse' => $dnsblFound,
'whitelist' => [],
'checkWhitelist' => false,
],
true,
],
'IP is blacklisted; blacklist has key' => [
[
'blacklist' => [ [ 'dnsbl.test', 'key' ] ],
'ip' => '127.0.0.1',
'dnsblQuery' => 'key.1.0.0.127.dnsbl.test',
'dnsblResponse' => $dnsblFound,
'whitelist' => [],
'checkWhitelist' => false,
],
true,
],
'IP is blacklisted; blacklist is array' => [
[
'blacklist' => [ [ 'dnsbl.test' ] ],
'ip' => '127.0.0.1',
'dnsblQuery' => '1.0.0.127.dnsbl.test',
'dnsblResponse' => $dnsblFound,
'whitelist' => [],
'checkWhitelist' => false,
],
true,
],
'IP is not blacklisted' => [
[
'blacklist' => [ 'dnsbl.test' ],
'ip' => '1.2.3.4',
'dnsblQuery' => '4.3.2.1.dnsbl.test',
'dnsblResponse' => $dnsblNotFound,
'whitelist' => [],
'checkWhitelist' => false,
],
false,
],
'Blacklist is empty' => [
[
'blacklist' => [],
'ip' => '127.0.0.1',
'dnsblQuery' => '1.0.0.127.dnsbl.test',
'dnsblResponse' => $dnsblFound,
'whitelist' => [],
'checkWhitelist' => false,
],
false,
],
'IP is blacklisted and whitelisted; whitelist is not checked' => [
[
'blacklist' => [ 'dnsbl.test' ],
'ip' => '127.0.0.1',
'dnsblQuery' => '1.0.0.127.dnsbl.test',
'dnsblResponse' => $dnsblFound,
'whitelist' => [ '127.0.0.1' ],
'checkWhitelist' => false,
],
true,
],
'IP is blacklisted and whitelisted; whitelist is checked' => [
[
'blacklist' => [ 'dnsbl.test' ],
'ip' => '127.0.0.1',
'dnsblQuery' => '1.0.0.127.dnsbl.test',
'dnsblResponse' => $dnsblFound,
'whitelist' => [ '127.0.0.1' ],
'checkWhitelist' => true,
],
false,
],
];
}
/**
* @covers ::getUniqueBlocks
*/
public function testGetUniqueBlocks() {
$blockId = 100;
$blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
$block = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getId' ] )
->getMock();
$block->method( 'getId' )
->willReturn( $blockId );
$autoblock = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getParentBlockId', 'getType' ] )
->getMock();
$autoblock->method( 'getParentBlockId' )
->willReturn( $blockId );
$autoblock->method( 'getType' )
->willReturn( DatabaseBlock::TYPE_AUTO );
$blocks = [ $block, $block, $autoblock, new SystemBlock() ];
$this->assertCount( 2, $blockManager->getUniqueBlocks( $blocks ) );
}
/**
* @dataProvider provideTrackBlockWithCookie
* @covers ::trackBlockWithCookie
*/
public function testTrackBlockWithCookie( $options, $expected ) {
$this->setMwGlobals( 'wgCookiePrefix', '' );
$request = new FauxRequest();
if ( $options['cookieSet'] ) {
$request->setCookie( 'BlockID', 'the value does not matter' );
}
/** @var FauxResponse $response */
$response = $request->response();
$user = $this->getMockBuilder( User::class )
->setMethods( [ 'getBlock', 'getRequest' ] )
->getMock();
$user->method( 'getBlock' )
->willReturn( $options['block'] );
$user->method( 'getRequest' )
->willReturn( $request );
// Although the block cookie is set via DeferredUpdates, in command line mode updates are
// processed immediately
$blockManager = $this->getBlockManager( [
'wgSecretKey' => '',
'wgCookieSetOnIpBlock' => true,
] );
$blockManager->trackBlockWithCookie( $user, $response );
$this->assertCount( $expected['count'], $response->getCookies() );
$this->assertEquals( $expected['value'], $response->getCookie( 'BlockID' ) );
}
public function provideTrackBlockWithCookie() {
$blockId = 123;
return [
'Block cookie is already set; there is a trackable block' => [
[
'cookieSet' => true,
'block' => $this->getTrackableBlock( $blockId ),
],
[
'count' => 1,
'value' => $blockId,
]
],
'Block cookie is already set; there is no block' => [
[
'cookieSet' => true,
'block' => null,
],
[
// Cookie is cleared by setting it to empty value
'count' => 1,
'value' => '',
]
],
'Block cookie is not yet set; there is no block' => [
[
'cookieSet' => false,
'block' => null,
],
[
'count' => 0,
'value' => null,
]
],
'Block cookie is not yet set; there is a trackable block' => [
[
'cookieSet' => false,
'block' => $this->getTrackableBlock( $blockId ),
],
[
'count' => 1,
'value' => $blockId,
]
],
'Block cookie is not yet set; there is a composite block with a trackable block' => [
[
'cookieSet' => false,
'block' => new CompositeBlock( [
'originalBlocks' => [
new SystemBlock(),
$this->getTrackableBlock( $blockId ),
]
] ),
],
[
'count' => 1,
'value' => $blockId,
]
],
'Block cookie is not yet set; there is a composite block but no trackable block' => [
[
'cookieSet' => false,
'block' => new CompositeBlock( [
'originalBlocks' => [
new SystemBlock(),
new SystemBlock(),
]
] ),
],
[
'count' => 0,
'value' => null,
]
],
];
}
private function getTrackableBlock( $blockId ) {
$block = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getType', 'getId' ] )
->getMock();
$block->method( 'getType' )
->willReturn( DatabaseBlock::TYPE_IP );
$block->method( 'getId' )
->willReturn( $blockId );
return $block;
}
/**
* @dataProvider provideSetBlockCookie
* @covers ::setBlockCookie
*/
public function testSetBlockCookie( $expiryDelta, $expectedExpiryDelta ) {
$this->setMwGlobals( [
'wgCookiePrefix' => '',
] );
$request = new FauxRequest();
$response = $request->response();
$blockManager = $this->getBlockManager( [
'wgSecretKey' => '',
'wgCookieSetOnIpBlock' => true,
] );
$now = wfTimestamp();
$block = new DatabaseBlock( [
'expiry' => $expiryDelta === '' ? '' : $now + $expiryDelta
] );
$blockManager->setBlockCookie( $block, $response );
$cookies = $response->getCookies();
$this->assertEqualsWithDelta(
$now + $expectedExpiryDelta,
$cookies['BlockID']['expire'],
60 // Allow actual to be up to 60 seconds later than expected
);
}
public static function provideSetBlockCookie() {
// Maximum length of a block cookie, defined in BlockManager::setBlockCookie
$maxExpiryDelta = ( 24 * 60 * 60 );
$longExpiryDelta = ( 48 * 60 * 60 );
$shortExpiryDelta = ( 12 * 60 * 60 );
return [
'Block has indefinite expiry' => [
'',
$maxExpiryDelta,
],
'Block expiry is later than maximum cookie block expiry' => [
$longExpiryDelta,
$maxExpiryDelta,
],
'Block expiry is sooner than maximum cookie block expiry' => [
$shortExpiryDelta,
$shortExpiryDelta,
],
];
}
/**
* @covers ::shouldTrackBlockWithCookie
*/
public function testShouldTrackBlockWithCookieSystemBlock() {
$blockManager = TestingAccessWrapper::newFromObject( $this->getBlockManager( [] ) );
$this->assertFalse( $blockManager->shouldTrackBlockWithCookie(
new SystemBlock(),
true
) );
}
/**
* @dataProvider provideShouldTrackBlockWithCookie
* @covers ::shouldTrackBlockWithCookie
*/
public function testShouldTrackBlockWithCookie( $options, $expected ) {
$block = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getType', 'isAutoblocking' ] )
->getMock();
$block->method( 'getType' )
->willReturn( $options['type'] );
if ( isset( $options['autoblocking'] ) ) {
$block->method( 'isAutoblocking' )
->willReturn( $options['autoblocking'] );
}
$blockManager = TestingAccessWrapper::newFromObject(
$this->getBlockManager( $options['blockManagerConfig'] )
);
$this->assertSame(
$expected,
$blockManager->shouldTrackBlockWithCookie( $block, $options['isAnon'] )
);
}
public static function provideShouldTrackBlockWithCookie() {
return [
'IP block, anonymous user, IP block cookies enabled' => [
[
'type' => DatabaseBlock::TYPE_IP,
'isAnon' => true,
'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
],
true
],
'IP range block, anonymous user, IP block cookies enabled' => [
[
'type' => DatabaseBlock::TYPE_RANGE,
'isAnon' => true,
'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
],
true
],
'IP block, anonymous user, IP block cookies disabled' => [
[
'type' => DatabaseBlock::TYPE_IP,
'isAnon' => true,
'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => false ],
],
false
],
'IP block, logged in user, IP block cookies enabled' => [
[
'type' => DatabaseBlock::TYPE_IP,
'isAnon' => false,
'blockManagerConfig' => [ 'wgCookieSetOnIpBlock' => true ],
],
false
],
'User block, anonymous, autoblock cookies enabled, block is autoblocking' => [
[
'type' => DatabaseBlock::TYPE_USER,
'isAnon' => true,
'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
'autoblocking' => true,
],
false
],
'User block, logged in, autoblock cookies enabled, block is autoblocking' => [
[
'type' => DatabaseBlock::TYPE_USER,
'isAnon' => false,
'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
'autoblocking' => true,
],
true
],
'User block, logged in, autoblock cookies disabled, block is autoblocking' => [
[
'type' => DatabaseBlock::TYPE_USER,
'isAnon' => false,
'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => false ],
'autoblocking' => true,
],
false
],
'User block, logged in, autoblock cookies enabled, block is not autoblocking' => [
[
'type' => DatabaseBlock::TYPE_USER,
'isAnon' => false,
'blockManagerConfig' => [ 'wgCookieSetOnAutoblock' => true ],
'autoblocking' => false,
],
false
],
'Block type is autoblock' => [
[
'type' => DatabaseBlock::TYPE_AUTO,
'isAnon' => true,
'blockManagerConfig' => [],
],
false
]
];
}
/**
* @covers ::clearBlockCookie
*/
public function testClearBlockCookie() {
$this->setMwGlobals( [
'wgCookiePrefix' => '',
] );
$request = new FauxRequest();
$response = $request->response();
$response->setCookie( 'BlockID', '100' );
$this->assertSame( '100', $response->getCookie( 'BlockID' ) );
BlockManager::clearBlockCookie( $response );
$this->assertSame( '', $response->getCookie( 'BlockID' ) );
}
/**
* @dataProvider provideGetIdFromCookieValue
* @covers ::getIdFromCookieValue
*/
public function testGetIdFromCookieValue( $options, $expected ) {
$blockManager = $this->getBlockManager( [
'wgSecretKey' => $options['secretKey']
] );
$this->assertEquals(
$expected,
$blockManager->getIdFromCookieValue( $options['cookieValue'] )
);
}
public static function provideGetIdFromCookieValue() {
$blockId = 100;
$secretKey = '123';
$hmac = MWCryptHash::hmac( $blockId, $secretKey, false );
return [
'No secret key is set' => [
[
'secretKey' => '',
'cookieValue' => $blockId,
'calculatedHmac' => MWCryptHash::hmac( $blockId, '', false ),
],
$blockId,
],
'Secret key is set and stored hmac is correct' => [
[
'secretKey' => $secretKey,
'cookieValue' => $blockId . '!' . $hmac,
'calculatedHmac' => $hmac,
],
$blockId,
],
'Secret key is set and stored hmac is incorrect' => [
[
'secretKey' => $secretKey,
'cookieValue' => $blockId . '!xyz',
'calculatedHmac' => $hmac,
],
null,
],
];
}
/**
* @dataProvider provideGetCookieValue
* @covers ::getCookieValue
*/
public function testGetCookieValue( $options, $expected ) {
$blockManager = $this->getBlockManager( [
'wgSecretKey' => $options['secretKey']
] );
$block = $this->getMockBuilder( DatabaseBlock::class )
->setMethods( [ 'getId' ] )
->getMock();
$block->method( 'getId' )
->willReturn( $options['blockId'] );
$this->assertEquals(
$expected,
$blockManager->getCookieValue( $block )
);
}
public static function provideGetCookieValue() {
$blockId = 100;
return [
'Secret key not set' => [
[
'secretKey' => '',
'blockId' => $blockId,
'hmac' => MWCryptHash::hmac( $blockId, '', false ),
],
$blockId,
],
'Secret key set' => [
[
'secretKey' => '123',
'blockId' => $blockId,
'hmac' => MWCryptHash::hmac( $blockId, '123', false ),
],
$blockId . '!' . MWCryptHash::hmac( $blockId, '123', false ) ],
];
}
/**
* @coversNothing
*/
public function testAllServiceOptionsUsed() {
$this->assertAllServiceOptionsUsed( [ 'ApplyIpBlocksToXff', 'SoftBlockRanges' ] );
}
}