Add a DatabaseBlockStore service
The new service replaces the following public DatabaseBlock methods: * ::delete * ::insert * ::update * ::purgeExpired Bug: T221075 Change-Id: I5f95db14b1189fd62d2c2bd5dae2cab0a368f401
This commit is contained in:
parent
3d64bdacf5
commit
23c3c70d7f
5 changed files with 1073 additions and 286 deletions
|
|
@ -30,6 +30,7 @@ use MediaWiki\Block\BlockErrorFormatter;
|
|||
use MediaWiki\Block\BlockManager;
|
||||
use MediaWiki\Block\BlockPermissionCheckerFactory;
|
||||
use MediaWiki\Block\BlockRestrictionStore;
|
||||
use MediaWiki\Block\DatabaseBlockStore;
|
||||
use MediaWiki\Cache\LinkBatchFactory;
|
||||
use MediaWiki\Config\ConfigRepository;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
|
|
@ -655,6 +656,14 @@ class MediaWikiServices extends ServiceContainer {
|
|||
return $this->getService( 'CryptHKDF' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.36
|
||||
* @return DatabaseBlockStore
|
||||
*/
|
||||
public function getDatabaseBlockStore() : DatabaseBlockStore {
|
||||
return $this->getService( 'DatabaseBlockStore' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.33
|
||||
* @return DateFormatterFactory
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ use MediaWiki\Block\BlockErrorFormatter;
|
|||
use MediaWiki\Block\BlockManager;
|
||||
use MediaWiki\Block\BlockPermissionCheckerFactory;
|
||||
use MediaWiki\Block\BlockRestrictionStore;
|
||||
use MediaWiki\Block\DatabaseBlockStore;
|
||||
use MediaWiki\Cache\LinkBatchFactory;
|
||||
use MediaWiki\Config\ConfigRepository;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
|
|
@ -273,6 +274,22 @@ return [
|
|||
return new CryptHKDF( $secret, $config->get( 'HKDFAlgorithm' ), $cache, $context );
|
||||
},
|
||||
|
||||
'DatabaseBlockStore' => function ( MediaWikiServices $services ) : DatabaseBlockStore {
|
||||
return new DatabaseBlockStore(
|
||||
new ServiceOptions(
|
||||
DatabaseBlockStore::CONSTRUCTOR_OPTIONS,
|
||||
$services->getMainConfig()
|
||||
),
|
||||
LoggerFactory::getInstance( 'DatabaseBlockStore' ),
|
||||
$services->getActorMigration(),
|
||||
$services->getBlockRestrictionStore(),
|
||||
$services->getCommentStore(),
|
||||
$services->getHookContainer(),
|
||||
$services->getDBLoadBalancer(),
|
||||
$services->getReadOnlyMode()
|
||||
);
|
||||
},
|
||||
|
||||
'DateFormatterFactory' => function ( MediaWikiServices $services ) : DateFormatterFactory {
|
||||
return new DateFormatterFactory();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@
|
|||
namespace MediaWiki\Block;
|
||||
|
||||
use ActorMigration;
|
||||
use AutoCommitUpdate;
|
||||
use CommentStore;
|
||||
use DeferredUpdates;
|
||||
use Hooks;
|
||||
use Html;
|
||||
use MediaWiki\Block\Restriction\NamespaceRestriction;
|
||||
|
|
@ -487,25 +485,9 @@ class DatabaseBlock extends AbstractBlock {
|
|||
* @return bool
|
||||
*/
|
||||
public function delete() {
|
||||
if ( wfReadOnly() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !$this->getId() ) {
|
||||
throw new MWException(
|
||||
__METHOD__ . " requires that the mId member be filled\n"
|
||||
);
|
||||
}
|
||||
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
|
||||
$this->getBlockRestrictionStore()->deleteByParentBlockId( $this->getId() );
|
||||
$dbw->delete( 'ipblocks', [ 'ipb_parent_block_id' => $this->getId() ], __METHOD__ );
|
||||
|
||||
$this->getBlockRestrictionStore()->deleteByBlockId( $this->getId() );
|
||||
$dbw->delete( 'ipblocks', [ 'ipb_id' => $this->getId() ], __METHOD__ );
|
||||
|
||||
return $dbw->affectedRows() > 0;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getDatabaseBlockStore()
|
||||
->deleteBlock( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -517,70 +499,9 @@ class DatabaseBlock extends AbstractBlock {
|
|||
* ('id' => block ID, 'autoIds' => array of autoblock IDs)
|
||||
*/
|
||||
public function insert( IDatabase $dbw = null ) {
|
||||
global $wgBlockDisablesLogin;
|
||||
|
||||
if ( !$this->getBlocker() || $this->getBlocker()->getName() === '' ) {
|
||||
throw new MWException( 'Cannot insert a block without a blocker set' );
|
||||
}
|
||||
|
||||
wfDebug( __METHOD__ . "; timestamp {$this->mTimestamp}" );
|
||||
|
||||
if ( $dbw === null ) {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
}
|
||||
|
||||
self::purgeExpired();
|
||||
|
||||
$row = $this->getDatabaseArray( $dbw );
|
||||
|
||||
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
|
||||
$affected = $dbw->affectedRows();
|
||||
if ( $affected ) {
|
||||
$this->setId( $dbw->insertId() );
|
||||
if ( $this->restrictions ) {
|
||||
$this->getBlockRestrictionStore()->insert( $this->restrictions );
|
||||
}
|
||||
}
|
||||
|
||||
# Don't collide with expired blocks.
|
||||
# Do this after trying to insert to avoid locking.
|
||||
if ( !$affected ) {
|
||||
# T96428: The ipb_address index uses a prefix on a field, so
|
||||
# use a standard SELECT + DELETE to avoid annoying gap locks.
|
||||
$ids = $dbw->selectFieldValues( 'ipblocks',
|
||||
'ipb_id',
|
||||
[
|
||||
'ipb_address' => $row['ipb_address'],
|
||||
'ipb_user' => $row['ipb_user'],
|
||||
'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
|
||||
],
|
||||
__METHOD__
|
||||
);
|
||||
if ( $ids ) {
|
||||
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
|
||||
$this->getBlockRestrictionStore()->deleteByBlockId( $ids );
|
||||
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
|
||||
$affected = $dbw->affectedRows();
|
||||
$this->setId( $dbw->insertId() );
|
||||
if ( $this->restrictions ) {
|
||||
$this->getBlockRestrictionStore()->insert( $this->restrictions );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $affected ) {
|
||||
$auto_ipd_ids = $this->doRetroactiveAutoblock();
|
||||
|
||||
if ( $wgBlockDisablesLogin && $this->target instanceof User ) {
|
||||
// Change user login token to force them to be logged out.
|
||||
$this->target->setToken();
|
||||
$this->target->saveSettings();
|
||||
}
|
||||
|
||||
return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
|
||||
}
|
||||
|
||||
return false;
|
||||
return MediaWikiServices::getInstance()
|
||||
->getDatabaseBlockStore()
|
||||
->insertBlock( $this, $dbw );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -591,185 +512,9 @@ class DatabaseBlock extends AbstractBlock {
|
|||
* ('id' => block ID, 'autoIds' => array of autoblock IDs)
|
||||
*/
|
||||
public function update() {
|
||||
wfDebug( __METHOD__ . "; timestamp {$this->mTimestamp}" );
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
|
||||
$dbw->startAtomic( __METHOD__ );
|
||||
|
||||
$result = $dbw->update(
|
||||
'ipblocks',
|
||||
$this->getDatabaseArray( $dbw ),
|
||||
[ 'ipb_id' => $this->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
// Only update the restrictions if they have been modified.
|
||||
if ( $this->restrictions !== null ) {
|
||||
// An empty array should remove all of the restrictions.
|
||||
if ( empty( $this->restrictions ) ) {
|
||||
$success = $this->getBlockRestrictionStore()->deleteByBlockId( $this->getId() );
|
||||
} else {
|
||||
$success = $this->getBlockRestrictionStore()->update( $this->restrictions );
|
||||
}
|
||||
// Update the result. The first false is the result, otherwise, true.
|
||||
$result = $result && $success;
|
||||
}
|
||||
|
||||
if ( $this->isAutoblocking() ) {
|
||||
// update corresponding autoblock(s) (T50813)
|
||||
$dbw->update(
|
||||
'ipblocks',
|
||||
$this->getAutoblockUpdateArray( $dbw ),
|
||||
[ 'ipb_parent_block_id' => $this->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
// Only update the restrictions if they have been modified.
|
||||
if ( $this->restrictions !== null ) {
|
||||
$this->getBlockRestrictionStore()->updateByParentBlockId( $this->getId(), $this->restrictions );
|
||||
}
|
||||
} else {
|
||||
// autoblock no longer required, delete corresponding autoblock(s)
|
||||
$this->getBlockRestrictionStore()->deleteByParentBlockId( $this->getId() );
|
||||
$dbw->delete(
|
||||
'ipblocks',
|
||||
[ 'ipb_parent_block_id' => $this->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
$dbw->endAtomic( __METHOD__ );
|
||||
|
||||
if ( $result ) {
|
||||
$auto_ipd_ids = $this->doRetroactiveAutoblock();
|
||||
return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array suitable for passing to $dbw->insert() or $dbw->update()
|
||||
* @param IDatabase $dbw
|
||||
* @return array
|
||||
*/
|
||||
protected function getDatabaseArray( IDatabase $dbw ) {
|
||||
$expiry = $dbw->encodeExpiry( $this->getExpiry() );
|
||||
|
||||
if ( $this->forcedTargetID ) {
|
||||
$uid = $this->forcedTargetID;
|
||||
} else {
|
||||
$uid = $this->target instanceof User ? $this->target->getId() : 0;
|
||||
}
|
||||
|
||||
$a = [
|
||||
'ipb_address' => (string)$this->target,
|
||||
'ipb_user' => $uid,
|
||||
'ipb_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
|
||||
'ipb_auto' => $this->mAuto,
|
||||
'ipb_anon_only' => !$this->isHardblock(),
|
||||
'ipb_create_account' => $this->isCreateAccountBlocked(),
|
||||
'ipb_enable_autoblock' => $this->isAutoblocking(),
|
||||
'ipb_expiry' => $expiry,
|
||||
'ipb_range_start' => $this->getRangeStart(),
|
||||
'ipb_range_end' => $this->getRangeEnd(),
|
||||
'ipb_deleted' => intval( $this->getHideName() ), // typecast required for SQLite
|
||||
'ipb_block_email' => $this->isEmailBlocked(),
|
||||
'ipb_allow_usertalk' => $this->isUsertalkEditAllowed(),
|
||||
'ipb_parent_block_id' => $this->mParentBlockId,
|
||||
'ipb_sitewide' => $this->isSitewide(),
|
||||
] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->getReasonComment() )
|
||||
+ ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IDatabase $dbw
|
||||
* @return array
|
||||
*/
|
||||
protected function getAutoblockUpdateArray( IDatabase $dbw ) {
|
||||
return [
|
||||
'ipb_create_account' => $this->isCreateAccountBlocked(),
|
||||
'ipb_deleted' => (int)$this->getHideName(), // typecast required for SQLite
|
||||
'ipb_allow_usertalk' => $this->isUsertalkEditAllowed(),
|
||||
'ipb_sitewide' => $this->isSitewide(),
|
||||
] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->getReasonComment() )
|
||||
+ ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retroactively autoblocks the last IP used by the user (if it is a user)
|
||||
* blocked by this block.
|
||||
*
|
||||
* @return array IDs of retroactive autoblocks made
|
||||
*/
|
||||
protected function doRetroactiveAutoblock() {
|
||||
$blockIds = [];
|
||||
# If autoblock is enabled, autoblock the LAST IP(s) used
|
||||
if ( $this->isAutoblocking() && $this->getType() == self::TYPE_USER ) {
|
||||
wfDebug( "Doing retroactive autoblocks for " . $this->getTarget() );
|
||||
|
||||
$continue = Hooks::runner()->onPerformRetroactiveAutoblock( $this, $blockIds );
|
||||
|
||||
if ( $continue ) {
|
||||
self::defaultRetroactiveAutoblock( $this, $blockIds );
|
||||
}
|
||||
}
|
||||
return $blockIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retroactively autoblocks the last IP used by the user (if it is a user)
|
||||
* blocked by this block. This will use the recentchanges table.
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @param array &$blockIds
|
||||
*/
|
||||
protected static function defaultRetroactiveAutoblock( DatabaseBlock $block, array &$blockIds ) {
|
||||
global $wgPutIPinRC;
|
||||
|
||||
// No IPs are in recentchanges table, so nothing to select
|
||||
if ( !$wgPutIPinRC ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Autoblocks only apply to TYPE_USER
|
||||
if ( $block->getType() !== self::TYPE_USER ) {
|
||||
return;
|
||||
}
|
||||
$target = $block->getTarget(); // TYPE_USER => always a User object
|
||||
|
||||
$dbr = wfGetDB( DB_REPLICA );
|
||||
$rcQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $target, false );
|
||||
|
||||
$options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
|
||||
|
||||
// Just the last IP used.
|
||||
$options['LIMIT'] = 1;
|
||||
|
||||
$res = $dbr->select(
|
||||
[ 'recentchanges' ] + $rcQuery['tables'],
|
||||
[ 'rc_ip' ],
|
||||
$rcQuery['conds'],
|
||||
__METHOD__,
|
||||
$options,
|
||||
$rcQuery['joins']
|
||||
);
|
||||
|
||||
if ( !$res->numRows() ) {
|
||||
# No results, don't autoblock anything
|
||||
wfDebug( "No IP found to retroactively autoblock" );
|
||||
} else {
|
||||
foreach ( $res as $row ) {
|
||||
if ( $row->rc_ip ) {
|
||||
$id = $block->doAutoblock( $row->rc_ip );
|
||||
if ( $id ) {
|
||||
$blockIds[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return MediaWikiServices::getInstance()
|
||||
->getDatabaseBlockStore()
|
||||
->updateBlock( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1014,10 +759,11 @@ class DatabaseBlock extends AbstractBlock {
|
|||
/**
|
||||
* Set the block ID
|
||||
*
|
||||
* @internal Only for use in DatabaseBlockStore; private until 1.36
|
||||
* @param int $blockId
|
||||
* @return self
|
||||
*/
|
||||
private function setId( $blockId ) {
|
||||
public function setId( $blockId ) {
|
||||
$this->mId = (int)$blockId;
|
||||
|
||||
if ( is_array( $this->restrictions ) ) {
|
||||
|
|
@ -1109,27 +855,7 @@ class DatabaseBlock extends AbstractBlock {
|
|||
* Purge expired blocks from the ipblocks table
|
||||
*/
|
||||
public static function purgeExpired() {
|
||||
if ( wfReadOnly() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
DeferredUpdates::addUpdate( new AutoCommitUpdate(
|
||||
wfGetDB( DB_MASTER ),
|
||||
__METHOD__,
|
||||
function ( IDatabase $dbw, $fname ) {
|
||||
$ids = $dbw->selectFieldValues( 'ipblocks',
|
||||
'ipb_id',
|
||||
[ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
|
||||
$fname
|
||||
);
|
||||
if ( $ids ) {
|
||||
$blockRestrictionStore = MediaWikiServices::getInstance()->getBlockRestrictionStore();
|
||||
$blockRestrictionStore->deleteByBlockId( $ids );
|
||||
|
||||
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
|
||||
}
|
||||
}
|
||||
) );
|
||||
MediaWikiServices::getInstance()->getDatabaseBlockStore()->purgeExpiredBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1470,6 +1196,16 @@ class DatabaseBlock extends AbstractBlock {
|
|||
return $this->restrictions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restrictions without loading from database if not yet loaded
|
||||
*
|
||||
* @internal
|
||||
* @return ?Restriction[]
|
||||
*/
|
||||
public function getRawRestrictions() : ?array {
|
||||
return $this->restrictions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Restrictions.
|
||||
*
|
||||
|
|
@ -1612,6 +1348,16 @@ class DatabaseBlock extends AbstractBlock {
|
|||
return $this->blocker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the forcedTargetID if set
|
||||
*
|
||||
* @internal
|
||||
* @return ?int
|
||||
*/
|
||||
public function getForcedTargetID() : ?int {
|
||||
return $this->forcedTargetID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the user who implemented (or will implement) this block
|
||||
*
|
||||
|
|
|
|||
497
includes/block/DatabaseBlockStore.php
Normal file
497
includes/block/DatabaseBlockStore.php
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for DatabaseBlock objects to interact with the database
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Block;
|
||||
|
||||
use ActorMigration;
|
||||
use AutoCommitUpdate;
|
||||
use CommentStore;
|
||||
use DeferredUpdates;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\HookContainer\HookContainer;
|
||||
use MediaWiki\HookContainer\HookRunner;
|
||||
use MWException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReadOnlyMode;
|
||||
use User;
|
||||
use Wikimedia\Rdbms\IDatabase;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
|
||||
/**
|
||||
* @since 1.36
|
||||
*
|
||||
* @author DannyS712
|
||||
*/
|
||||
class DatabaseBlockStore {
|
||||
|
||||
/** @var ServiceOptions */
|
||||
private $options;
|
||||
|
||||
public const CONSTRUCTOR_OPTIONS = [
|
||||
'PutIPinRC',
|
||||
'BlockDisablesLogin',
|
||||
];
|
||||
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
/** @var ActorMigration */
|
||||
private $actorMigration;
|
||||
|
||||
/** @var BlockRestrictionStore */
|
||||
private $blockRestrictionStore;
|
||||
|
||||
/** @var CommentStore */
|
||||
private $commentStore;
|
||||
|
||||
/** @var HookRunner */
|
||||
private $hookRunner;
|
||||
|
||||
/** @var ILoadBalancer */
|
||||
private $loadBalancer;
|
||||
|
||||
/** @var ReadOnlyMode */
|
||||
private $readOnlyMode;
|
||||
|
||||
/**
|
||||
* @param ServiceOptions $options
|
||||
* @param LoggerInterface $logger
|
||||
* @param ActorMigration $actorMigration
|
||||
* @param BlockRestrictionStore $blockRestrictionStore
|
||||
* @param CommentStore $commentStore
|
||||
* @param HookContainer $hookContainer
|
||||
* @param ILoadBalancer $loadBalancer
|
||||
* @param ReadOnlyMode $readOnlyMode
|
||||
*/
|
||||
public function __construct(
|
||||
ServiceOptions $options,
|
||||
LoggerInterface $logger,
|
||||
ActorMigration $actorMigration,
|
||||
BlockRestrictionStore $blockRestrictionStore,
|
||||
CommentStore $commentStore,
|
||||
HookContainer $hookContainer,
|
||||
ILoadBalancer $loadBalancer,
|
||||
ReadOnlyMode $readOnlyMode
|
||||
) {
|
||||
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
||||
|
||||
$this->options = $options;
|
||||
$this->logger = $logger;
|
||||
$this->actorMigration = $actorMigration;
|
||||
$this->blockRestrictionStore = $blockRestrictionStore;
|
||||
$this->commentStore = $commentStore;
|
||||
$this->hookRunner = new HookRunner( $hookContainer );
|
||||
$this->loadBalancer = $loadBalancer;
|
||||
$this->readOnlyMode = $readOnlyMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete expired blocks from the ipblocks table
|
||||
*
|
||||
* @internal only public for use in DatabaseBlock
|
||||
*/
|
||||
public function purgeExpiredBlocks() {
|
||||
if ( $this->readOnlyMode->isReadOnly() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
|
||||
$blockRestrictionStore = $this->blockRestrictionStore;
|
||||
|
||||
DeferredUpdates::addUpdate( new AutoCommitUpdate(
|
||||
$dbw,
|
||||
__METHOD__,
|
||||
function ( IDatabase $dbw, $fname ) use ( $blockRestrictionStore ) {
|
||||
$ids = $dbw->selectFieldValues(
|
||||
'ipblocks',
|
||||
'ipb_id',
|
||||
[ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
|
||||
$fname
|
||||
);
|
||||
if ( $ids ) {
|
||||
$blockRestrictionStore->deleteByBlockId( $ids );
|
||||
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
|
||||
}
|
||||
}
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a block into the block table. Will fail if there is a conflicting
|
||||
* block (same name and options) already in the database.
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @param IDatabase|null $database Database to use if not the same as the one in the load balancer
|
||||
* @return bool|array False on failure, assoc array on success:
|
||||
* ('id' => block ID, 'autoIds' => array of autoblock IDs)
|
||||
* @throws MWException
|
||||
*/
|
||||
public function insertBlock(
|
||||
DatabaseBlock $block,
|
||||
IDatabase $database = null
|
||||
) {
|
||||
if ( !$block->getBlocker() || $block->getBlocker()->getName() === '' ) {
|
||||
throw new MWException( 'Cannot insert a block without a blocker set' );
|
||||
}
|
||||
|
||||
$this->logger->debug( 'Inserting block; timestamp ' . $block->getTimestamp() );
|
||||
|
||||
// TODO T258866 - consider passing the database
|
||||
$this->purgeExpiredBlocks();
|
||||
|
||||
$dbw = $database ?: $this->loadBalancer->getConnectionRef( DB_MASTER );
|
||||
$row = $this->getArrayForDatabaseBlock( $block, $dbw );
|
||||
|
||||
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
|
||||
$affected = $dbw->affectedRows();
|
||||
|
||||
if ( $affected ) {
|
||||
$block->setId( $dbw->insertId() );
|
||||
if ( $block->getRawRestrictions() ) {
|
||||
$this->blockRestrictionStore->insert( $block->getRawRestrictions() );
|
||||
}
|
||||
}
|
||||
|
||||
// Don't collide with expired blocks.
|
||||
// Do this after trying to insert to avoid locking.
|
||||
if ( !$affected ) {
|
||||
// T96428: The ipb_address index uses a prefix on a field, so
|
||||
// use a standard SELECT + DELETE to avoid annoying gap locks.
|
||||
$ids = $dbw->selectFieldValues(
|
||||
'ipblocks',
|
||||
'ipb_id',
|
||||
[
|
||||
'ipb_address' => $row['ipb_address'],
|
||||
'ipb_user' => $row['ipb_user'],
|
||||
'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
|
||||
],
|
||||
__METHOD__
|
||||
);
|
||||
if ( $ids ) {
|
||||
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
|
||||
$this->blockRestrictionStore->deleteByBlockId( $ids );
|
||||
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
|
||||
$affected = $dbw->affectedRows();
|
||||
$block->setId( $dbw->insertId() );
|
||||
if ( $block->getRawRestrictions() ) {
|
||||
$this->blockRestrictionStore->insert( $block->getRawRestrictions() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $affected ) {
|
||||
$autoBlockIds = $this->doRetroactiveAutoblock( $block );
|
||||
|
||||
if ( $this->options->get( 'BlockDisablesLogin' ) ) {
|
||||
$target = $block->getTarget();
|
||||
if ( $target instanceof User ) {
|
||||
// Change user login token to force them to be logged out.
|
||||
$target->setToken();
|
||||
$target->saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
return [ 'id' => $block->getId(), 'autoIds' => $autoBlockIds ];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a block in the DB with new parameters.
|
||||
* The ID field needs to be loaded first.
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @return bool|array False on failure, array on success:
|
||||
* ('id' => block ID, 'autoIds' => array of autoblock IDs)
|
||||
*/
|
||||
public function updateBlock( DatabaseBlock $block ) {
|
||||
$this->logger->debug( 'Updating block; timestamp ' . $block->getTimestamp() );
|
||||
|
||||
$dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
|
||||
$row = $this->getArrayForDatabaseBlock( $block, $dbw );
|
||||
$dbw->startAtomic( __METHOD__ );
|
||||
|
||||
$result = $dbw->update(
|
||||
'ipblocks',
|
||||
$row,
|
||||
[ 'ipb_id' => $block->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
// Only update the restrictions if they have been modified.
|
||||
$restrictions = $block->getRawRestrictions();
|
||||
if ( $restrictions !== null ) {
|
||||
// An empty array should remove all of the restrictions.
|
||||
if ( empty( $restrictions ) ) {
|
||||
$success = $this->blockRestrictionStore->deleteByBlockId( $block->getId() );
|
||||
} else {
|
||||
$success = $this->blockRestrictionStore->update( $restrictions );
|
||||
}
|
||||
// Update the result. The first false is the result, otherwise, true.
|
||||
$result = $result && $success;
|
||||
}
|
||||
|
||||
if ( $block->isAutoblocking() ) {
|
||||
// update corresponding autoblock(s) (T50813)
|
||||
$dbw->update(
|
||||
'ipblocks',
|
||||
$this->getArrayForAutoblockUpdate( $block ),
|
||||
[ 'ipb_parent_block_id' => $block->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
// Only update the restrictions if they have been modified.
|
||||
if ( $restrictions !== null ) {
|
||||
$this->blockRestrictionStore->updateByParentBlockId(
|
||||
$block->getId(),
|
||||
$restrictions
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// autoblock no longer required, delete corresponding autoblock(s)
|
||||
$this->blockRestrictionStore->deleteByParentBlockId( $block->getId() );
|
||||
$dbw->delete(
|
||||
'ipblocks',
|
||||
[ 'ipb_parent_block_id' => $block->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
$dbw->endAtomic( __METHOD__ );
|
||||
|
||||
if ( $result ) {
|
||||
$autoBlockIds = $this->doRetroactiveAutoblock( $block );
|
||||
return [ 'id' => $block->getId(), 'autoIds' => $autoBlockIds ];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a DatabaseBlock from the database
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @return bool whether it was deleted
|
||||
* @throws MWException
|
||||
*/
|
||||
public function deleteBlock( DatabaseBlock $block ) : bool {
|
||||
if ( $this->readOnlyMode->isReadOnly() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$blockId = $block->getId();
|
||||
|
||||
if ( !$blockId ) {
|
||||
throw new MWException(
|
||||
__METHOD__ . " requires that a block id be set\n"
|
||||
);
|
||||
}
|
||||
$dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
|
||||
|
||||
$this->blockRestrictionStore->deleteByParentBlockId( $blockId );
|
||||
$dbw->delete(
|
||||
'ipblocks',
|
||||
[ 'ipb_parent_block_id' => $blockId ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
$this->blockRestrictionStore->deleteByBlockId( $blockId );
|
||||
$dbw->delete(
|
||||
'ipblocks',
|
||||
[ 'ipb_id' => $blockId ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
return $dbw->affectedRows() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array suitable for passing to $dbw->insert() or $dbw->update()
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @param IDatabase $dbw
|
||||
* @return array
|
||||
*/
|
||||
private function getArrayForDatabaseBlock(
|
||||
DatabaseBlock $block,
|
||||
IDatabase $dbw
|
||||
) : array {
|
||||
$expiry = $dbw->encodeExpiry( $block->getExpiry() );
|
||||
|
||||
$target = $block->getTarget();
|
||||
$forcedTargetId = $block->getForcedTargetId();
|
||||
if ( $forcedTargetId ) {
|
||||
$userId = $forcedTargetId;
|
||||
} elseif ( $target instanceof User ) {
|
||||
$userId = $target->getId();
|
||||
} else {
|
||||
$userId = 0;
|
||||
}
|
||||
|
||||
$blockArray = [
|
||||
'ipb_address' => (string)$target,
|
||||
'ipb_user' => $userId,
|
||||
'ipb_timestamp' => $dbw->timestamp( $block->getTimestamp() ),
|
||||
'ipb_auto' => $block->getType() === AbstractBlock::TYPE_AUTO,
|
||||
'ipb_anon_only' => !$block->isHardblock(),
|
||||
'ipb_create_account' => $block->isCreateAccountBlocked(),
|
||||
'ipb_enable_autoblock' => $block->isAutoblocking(),
|
||||
'ipb_expiry' => $expiry,
|
||||
'ipb_range_start' => $block->getRangeStart(),
|
||||
'ipb_range_end' => $block->getRangeEnd(),
|
||||
'ipb_deleted' => intval( $block->getHideName() ), // typecast required for SQLite
|
||||
'ipb_block_email' => $block->isEmailBlocked(),
|
||||
'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
|
||||
'ipb_parent_block_id' => $block->getParentBlockId(),
|
||||
'ipb_sitewide' => $block->isSitewide(),
|
||||
];
|
||||
$commentArray = $this->commentStore->insert(
|
||||
$dbw,
|
||||
'ipb_reason',
|
||||
$block->getReasonComment()
|
||||
);
|
||||
$actorArray = $this->actorMigration->getInsertValues(
|
||||
$dbw,
|
||||
'ipb_by',
|
||||
$block->getBlocker()
|
||||
);
|
||||
|
||||
$combinedArray = $blockArray + $commentArray + $actorArray;
|
||||
return $combinedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array suitable for autoblock updates
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @return array
|
||||
*/
|
||||
private function getArrayForAutoblockUpdate( DatabaseBlock $block ) : array {
|
||||
$blockArray = [
|
||||
'ipb_create_account' => $block->isCreateAccountBlocked(),
|
||||
'ipb_deleted' => (int)$block->getHideName(), // typecast required for SQLite
|
||||
'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
|
||||
'ipb_sitewide' => $block->isSitewide(),
|
||||
];
|
||||
|
||||
$dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
|
||||
$commentArray = $this->commentStore->insert(
|
||||
$dbw,
|
||||
'ipb_reason',
|
||||
$block->getReasonComment()
|
||||
);
|
||||
$actorArray = $this->actorMigration->getInsertValues(
|
||||
$dbw,
|
||||
'ipb_by',
|
||||
$block->getBlocker()
|
||||
);
|
||||
|
||||
$combinedArray = $blockArray + $commentArray + $actorArray;
|
||||
return $combinedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles retroactively autoblocking the last IP used by the user (if it is a user)
|
||||
* blocked by an auto block.
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @return array IDs of retroactive autoblocks made
|
||||
*/
|
||||
private function doRetroactiveAutoblock( DatabaseBlock $block ) : array {
|
||||
$autoBlockIds = [];
|
||||
// If autoblock is enabled, autoblock the LAST IP(s) used
|
||||
if ( $block->isAutoblocking() && $block->getType() == AbstractBlock::TYPE_USER ) {
|
||||
$this->logger->debug(
|
||||
'Doing retroactive autoblocks for ' . $block->getTarget()
|
||||
);
|
||||
|
||||
$hookAutoBlocked = [];
|
||||
$continue = $this->hookRunner->onPerformRetroactiveAutoblock(
|
||||
$block,
|
||||
$hookAutoBlocked
|
||||
);
|
||||
|
||||
if ( $continue ) {
|
||||
$coreAutoBlocked = $this->performRetroactiveAutoblock( $block );
|
||||
$autoBlockIds = array_merge( $hookAutoBlocked, $coreAutoBlocked );
|
||||
} else {
|
||||
$autoBlockIds = $hookAutoBlocked;
|
||||
}
|
||||
}
|
||||
return $autoBlockIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually retroactively autoblocks the last IP used by the user (if it is a user)
|
||||
* blocked by this block. This will use the recentchanges table.
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @return array
|
||||
*/
|
||||
private function performRetroactiveAutoblock( DatabaseBlock $block ) : array {
|
||||
if ( !$this->options->get( 'PutIPinRC' ) ) {
|
||||
// No IPs in the recent changes table to autoblock
|
||||
return [];
|
||||
}
|
||||
|
||||
list( $target, $type ) = $block->getTargetAndType();
|
||||
if ( $type !== AbstractBlock::TYPE_USER ) {
|
||||
// Autoblocks only apply to users
|
||||
return [];
|
||||
}
|
||||
|
||||
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
|
||||
$rcQuery = $this->actorMigration->getWhere( $dbr, 'rc_user', $target, false );
|
||||
|
||||
$options = [
|
||||
'ORDER BY' => 'rc_timestamp DESC',
|
||||
'LIMIT' => 1,
|
||||
];
|
||||
|
||||
$res = $dbr->select(
|
||||
[ 'recentchanges' ] + $rcQuery['tables'],
|
||||
[ 'rc_ip' ],
|
||||
$rcQuery['conds'],
|
||||
__METHOD__,
|
||||
$options,
|
||||
$rcQuery['joins']
|
||||
);
|
||||
|
||||
if ( !$res->numRows() ) {
|
||||
$this->logger->debug( 'No IP found to retroactively autoblock' );
|
||||
return [];
|
||||
}
|
||||
|
||||
$blockIds = [];
|
||||
foreach ( $res as $row ) {
|
||||
if ( $row->rc_ip ) {
|
||||
$id = $block->doAutoblock( $row->rc_ip );
|
||||
if ( $id ) {
|
||||
$blockIds[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $blockIds;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Block\DatabaseBlock;
|
||||
use MediaWiki\Block\DatabaseBlockStore;
|
||||
use MediaWiki\Block\Restriction\NamespaceRestriction;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\HookContainer\HookContainer;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* Integration tests for DatabaseBlockStore.
|
||||
*
|
||||
* @author DannyS712
|
||||
* @group Blocking
|
||||
* @group Database
|
||||
* @covers \MediaWiki\Block\DatabaseBlockStore
|
||||
*/
|
||||
class DatabaseBlockStoreTest extends MediaWikiIntegrationTestCase {
|
||||
/** @var User */
|
||||
private $sysop;
|
||||
|
||||
/** @var integer */
|
||||
private $expiredBlockId = 11111;
|
||||
|
||||
/** @var integer */
|
||||
private $unexpiredBlockId = 22222;
|
||||
|
||||
/** @var integer */
|
||||
private $autoblockId = 33333;
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* - config: Override the ServiceOptions config
|
||||
* - constructorArgs: Override the constructor arguments
|
||||
* @return DatabaseBlockStore
|
||||
*/
|
||||
private function getStore( array $options = [] ) : DatabaseBlockStore {
|
||||
$overrideConfig = $options['config'] ?? [];
|
||||
$overrideConstructorArgs = $options['constructorArgs'] ?? [];
|
||||
|
||||
$defaultConfig = [
|
||||
'PutIPinRC' => true,
|
||||
'BlockDisablesLogin' => false,
|
||||
];
|
||||
$config = array_merge( $defaultConfig, $overrideConfig );
|
||||
|
||||
// This ensures continuation after hooks
|
||||
$hookContainer = $this->createMock( HookContainer::class );
|
||||
$hookContainer->method( 'run' )
|
||||
->willReturn( true );
|
||||
|
||||
// Most tests need read only to be false
|
||||
$readOnlyMode = $this->createMock( ReadOnlyMode::class );
|
||||
$readOnlyMode->method( 'isReadOnly' )
|
||||
->willReturn( false );
|
||||
|
||||
$services = MediaWikiServices::getInstance();
|
||||
$defaultConstructorArgs = [
|
||||
'serviceOptions' => new ServiceOptions(
|
||||
DatabaseBlockStore::CONSTRUCTOR_OPTIONS,
|
||||
$config
|
||||
),
|
||||
'logger' => new NullLogger(),
|
||||
'actorMigration' => $services->getActorMigration(),
|
||||
'blockRestrictionStore' => $services->getBlockRestrictionStore(),
|
||||
'commentStore' => $services->getCommentStore(),
|
||||
'hookContainer' => $hookContainer,
|
||||
'loadBalancer' => $services->getDBLoadBalancer(),
|
||||
'readOnlyMode' => $readOnlyMode,
|
||||
];
|
||||
$constructorArgs = array_merge( $defaultConstructorArgs, $overrideConstructorArgs );
|
||||
|
||||
return new DatabaseBlockStore( ...array_values( $constructorArgs ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* - target: The intended target, an unblocked user by default
|
||||
* - autoblock: Whether this block is autoblocking
|
||||
* @return DatabaseBlock
|
||||
*/
|
||||
private function getBlock( array $options = [] ) : DatabaseBlock {
|
||||
$target = $options['target'] ?? $this->getTestUser()->getUser();
|
||||
$autoblock = $options['autoblock'] ?? false;
|
||||
|
||||
return new DatabaseBlock( [
|
||||
'by' => $this->sysop->getId(),
|
||||
'address' => $target,
|
||||
'enableAutoblock' => $autoblock,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an autoblock corresponds to a parent block. The following are not
|
||||
* required to be equal, so are not tested:
|
||||
* - target
|
||||
* - type
|
||||
* - expiry
|
||||
* - autoblocking
|
||||
*
|
||||
* @param DatabaseBlock $block
|
||||
* @param DatabaseBlock $autoblock
|
||||
*/
|
||||
private function assertAutoblockEqualsBlock(
|
||||
DatabaseBlock $block,
|
||||
DatabaseBlock $autoblock
|
||||
) {
|
||||
$this->assertSame( $autoblock->getParentBlockId(), $block->getId() );
|
||||
$this->assertSame( $autoblock->isHardblock(), $block->isHardblock() );
|
||||
$this->assertSame( $autoblock->isCreateAccountBlocked(), $block->isCreateAccountBlocked() );
|
||||
$this->assertSame( $autoblock->getHideName(), $block->getHideName() );
|
||||
$this->assertSame( $autoblock->isEmailBlocked(), $block->isEmailBlocked() );
|
||||
$this->assertSame( $autoblock->isUsertalkEditAllowed(), $block->isUsertalkEditAllowed() );
|
||||
$this->assertSame( $autoblock->isSitewide(), $block->isSitewide() );
|
||||
|
||||
$restrictionStore = MediaWikiServices::getInstance()->getBlockRestrictionStore();
|
||||
$this->assertTrue(
|
||||
$restrictionStore->equals(
|
||||
$autoblock->getRestrictions(),
|
||||
$block->getRestrictions()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideInsertBlockSuccess
|
||||
*/
|
||||
public function testInsertBlockSuccess( $options ) {
|
||||
$block = $this->getBlock( $options['block'] ?? [] );
|
||||
$block->setRestrictions( [
|
||||
new NamespaceRestriction( 0, NS_MAIN ),
|
||||
] );
|
||||
|
||||
$store = $this->getStore( $options['store'] ?? [] );
|
||||
$result = $store->insertBlock( $block );
|
||||
|
||||
$this->assertIsArray( $result );
|
||||
$this->assertArrayHasKey( 'id', $result );
|
||||
$this->assertArrayHasKey( 'autoIds', $result );
|
||||
$this->assertSame( 0, count( $result['autoIds'] ) );
|
||||
|
||||
$retrievedBlock = DatabaseBlock::newFromId( $result['id'] );
|
||||
$this->assertTrue( $block->equals( $retrievedBlock ) );
|
||||
}
|
||||
|
||||
public function provideInsertBlockSuccess() {
|
||||
return [
|
||||
'No conflicting block, not autoblocking' => [
|
||||
'block' => [
|
||||
'autoblock' => false,
|
||||
],
|
||||
],
|
||||
'No conflicting block, autoblocking but IP not in recent changes' => [
|
||||
[
|
||||
'block' => [
|
||||
'autoblock' => true,
|
||||
],
|
||||
'store' => [
|
||||
'constructorArgs' => [
|
||||
'PutIPinRC' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'No conflicting block, autoblocking but no recent edits' => [
|
||||
'block' => [
|
||||
'autoblock' => true,
|
||||
],
|
||||
],
|
||||
'Conflicting block, expired' => [
|
||||
'block' => [
|
||||
// Blocked with expired block in addDBData
|
||||
'target' => '1.1.1.1',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testInsertBlockConflict() {
|
||||
$block = $this->getBlock( [ 'target' => $this->sysop ] );
|
||||
|
||||
$store = $this->getStore();
|
||||
$result = $store->insertBlock( $block );
|
||||
|
||||
$this->assertFalse( $result );
|
||||
$this->assertNull( $block->getId() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideInsertBlockLogout
|
||||
*/
|
||||
public function testInsertBlockLogout( $options, $expectTokenEqual ) {
|
||||
$block = $this->getBlock();
|
||||
$targetToken = $block->getTarget()->getToken();
|
||||
|
||||
$store = $this->getStore( $options );
|
||||
$result = $store->insertBlock( $block );
|
||||
|
||||
$this->assertSame(
|
||||
$expectTokenEqual,
|
||||
$targetToken === $block->getTarget()->getToken()
|
||||
);
|
||||
}
|
||||
|
||||
public function provideInsertBlockLogout() {
|
||||
return [
|
||||
'Blocked user can log in' => [
|
||||
[
|
||||
'config' => [
|
||||
'BlockDisablesLogin' => false,
|
||||
],
|
||||
],
|
||||
true,
|
||||
],
|
||||
'Blocked user cannot log in' => [
|
||||
[
|
||||
'config' => [
|
||||
'BlockDisablesLogin' => true,
|
||||
],
|
||||
],
|
||||
false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testInsertBlockAutoblock() {
|
||||
// This is quicker than adding a recent change for an unblocked user.
|
||||
// See addDBDataOnce documentation for more details.
|
||||
$target = $this->sysop;
|
||||
$this->db->delete(
|
||||
'ipblocks',
|
||||
[ 'ipb_address' => $target->getName() ]
|
||||
);
|
||||
$block = $this->getBlock( [
|
||||
'autoblock' => true,
|
||||
'target' => $target,
|
||||
] );
|
||||
|
||||
$store = $this->getStore();
|
||||
$result = $store->insertBlock( $block );
|
||||
|
||||
$this->assertIsArray( $result );
|
||||
$this->assertArrayHasKey( 'autoIds', $result );
|
||||
$this->assertCount( 1, $result['autoIds'] );
|
||||
|
||||
$retrievedBlock = DatabaseBlock::newFromId( $result['autoIds'][0] );
|
||||
$this->assertSame( $block->getId(), $retrievedBlock->getParentBlockId() );
|
||||
$this->assertAutoblockEqualsBlock( $block, $retrievedBlock );
|
||||
}
|
||||
|
||||
public function testInsertBlockError() {
|
||||
$block = $this->createMock( DatabaseBlock::class );
|
||||
|
||||
$this->expectException( MWException::class );
|
||||
$this->expectExceptionMessage( 'insert' );
|
||||
|
||||
$store = $this->getStore();
|
||||
$store->insertBlock( $block );
|
||||
}
|
||||
|
||||
public function testUpdateBlock() {
|
||||
$existingBlock = DatabaseBlock::newFromTarget( $this->sysop );
|
||||
$existingBlock->isUsertalkEditAllowed( true );
|
||||
|
||||
$store = $this->getStore();
|
||||
$result = $store->updateBlock( $existingBlock );
|
||||
|
||||
$updatedBlock = DatabaseBlock::newFromId( $result['id'] );
|
||||
$autoblock = DatabaseBlock::newFromId( $result['autoIds'][0] );
|
||||
|
||||
$this->assertTrue( $updatedBlock->equals( $existingBlock ) );
|
||||
$this->assertAutoblockEqualsBlock( $existingBlock, $autoblock );
|
||||
}
|
||||
|
||||
public function testUpdateBlockAddOrRemoveAutoblock() {
|
||||
// Existing block is autoblocking to begin with
|
||||
$existingBlock = DatabaseBlock::newFromTarget( $this->sysop );
|
||||
$existingBlock->isAutoblocking( false );
|
||||
|
||||
$store = $this->getStore();
|
||||
$result = $store->updateBlock( $existingBlock );
|
||||
|
||||
$updatedBlock = DatabaseBlock::newFromId( $result['id'] );
|
||||
|
||||
$this->assertTrue( $updatedBlock->equals( $existingBlock ) );
|
||||
$this->assertCount( 0, $result['autoIds'] );
|
||||
|
||||
// Test adding an autoblock in the same test run, since we need the
|
||||
// target to be the sysop (see addDBDataOnce documentation), and the
|
||||
// sysop is blocked with an autoblock between test runs.
|
||||
$existingBlock->isAutoblocking( true );
|
||||
$result = $store->updateBlock( $existingBlock );
|
||||
|
||||
$updatedBlock = DatabaseBlock::newFromId( $result['id'] );
|
||||
$autoblock = DatabaseBlock::newFromId( $result['autoIds'][0] );
|
||||
|
||||
$this->assertTrue( $updatedBlock->equals( $existingBlock ) );
|
||||
$this->assertAutoblockEqualsBlock( $existingBlock, $autoblock );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideUpdateBlockRestrictions
|
||||
*/
|
||||
public function testUpdateBlockRestrictions( $expectedCount ) {
|
||||
$existingBlock = DatabaseBlock::newFromTarget( $this->sysop );
|
||||
$restrictions = [];
|
||||
for ( $ns = 0; $ns < $expectedCount; $ns++ ) {
|
||||
$restrictions[] = new NamespaceRestriction( $existingBlock->getId(), $ns );
|
||||
}
|
||||
$existingBlock->setRestrictions( $restrictions );
|
||||
|
||||
$store = $this->getStore();
|
||||
$result = $store->updateBlock( $existingBlock );
|
||||
|
||||
$retrievedBlock = DatabaseBlock::newFromId( $result['id'] );
|
||||
$this->assertCount(
|
||||
$expectedCount,
|
||||
$retrievedBlock->getRestrictions()
|
||||
);
|
||||
}
|
||||
|
||||
public function provideUpdateBlockRestrictions() {
|
||||
return [
|
||||
'Restrictions deleted if removed' => [ 0 ],
|
||||
'Restrictions changed if updated' => [ 2 ],
|
||||
];
|
||||
}
|
||||
|
||||
public function testDeleteBlockSuccess() {
|
||||
$target = $this->sysop;
|
||||
$block = DatabaseBlock::newFromTarget( $target );
|
||||
|
||||
$store = $this->getStore();
|
||||
|
||||
$this->assertTrue( $store->deleteBlock( $block ) );
|
||||
$this->assertNull( DatabaseBlock::newFromTarget( $target ) );
|
||||
}
|
||||
|
||||
public function testDeleteBlockFailureReadOnly() {
|
||||
$target = $this->sysop;
|
||||
$block = DatabaseBlock::newFromTarget( $target );
|
||||
|
||||
$readOnlyMode = $this->createMock( ReadOnlyMode::class );
|
||||
$readOnlyMode->method( 'isReadOnly' )
|
||||
->willReturn( true );
|
||||
$store = $this->getStore( [
|
||||
'constructorArgs' => [
|
||||
'readOnlyMode' => $readOnlyMode
|
||||
],
|
||||
] );
|
||||
|
||||
$this->assertFalse( $store->deleteBlock( $block ) );
|
||||
$this->assertTrue( (bool)DatabaseBlock::newFromTarget( $target ) );
|
||||
}
|
||||
|
||||
public function testDeleteBlockFailureNoBlockId() {
|
||||
$block = $this->createMock( DatabaseBlock::class );
|
||||
$block->method( 'getId' )
|
||||
->willReturn( null );
|
||||
|
||||
$this->expectException( MWException::class );
|
||||
$this->expectExceptionMessage( 'delete' );
|
||||
|
||||
$store = $this->getStore();
|
||||
$store->deleteBlock( $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether expired blocks and restrictions were removed from the database.
|
||||
*
|
||||
* @param int $blockId
|
||||
* @param bool $expected Whether to expect to find any rows
|
||||
*/
|
||||
private function assertPurgeWorked( int $blockId, bool $expected ) : void {
|
||||
$blockRows = (bool)$this->db->select(
|
||||
'ipblocks',
|
||||
'ipb_id',
|
||||
[ 'ipb_id' => $blockId ]
|
||||
)->numRows();
|
||||
$blockRestrictionsRows = (bool)$this->db->select(
|
||||
'ipblocks_restrictions',
|
||||
'ir_ipb_id',
|
||||
[ 'ir_ipb_id' => $blockId ]
|
||||
)->numRows();
|
||||
|
||||
$this->assertSame( $expected, $blockRows );
|
||||
$this->assertSame( $expected, $blockRestrictionsRows );
|
||||
}
|
||||
|
||||
public function testPurgeExpiredBlocksSuccess() {
|
||||
$store = $this->getStore();
|
||||
$store->purgeExpiredBlocks();
|
||||
|
||||
$this->assertPurgeWorked( $this->expiredBlockId, false );
|
||||
$this->assertPurgeWorked( $this->unexpiredBlockId, true );
|
||||
}
|
||||
|
||||
public function testPurgeExpiredBlocksFailureReadOnly() {
|
||||
$readOnlyMode = $this->createMock( ReadOnlyMode::class );
|
||||
$readOnlyMode->method( 'isReadOnly' )
|
||||
->willReturn( true );
|
||||
$store = $this->getStore( [
|
||||
'constructorArgs' => [
|
||||
'readOnlyMode' => $readOnlyMode,
|
||||
],
|
||||
] );
|
||||
$store->purgeExpiredBlocks();
|
||||
|
||||
$this->assertPurgeWorked( $this->expiredBlockId, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to autoblock a user, they must have a recent change.
|
||||
*
|
||||
* Make a recent change for the test sysop. This user persists between test runs,
|
||||
* so will always have this recent change.
|
||||
*
|
||||
* Regular test users don't persist between test runs, because the TestUserRegistry
|
||||
* is cleared between runs. If we tested autoblocking on a regular test user, we
|
||||
* would need to make a recent change for each test, which is slow.
|
||||
*
|
||||
* Instead we always test autoblocks on the test sysop.
|
||||
*/
|
||||
public function addDBDataOnce() {
|
||||
$this->editPage(
|
||||
'UTPage', // Added in addCoreDBData
|
||||
'an edit',
|
||||
'a summary',
|
||||
NS_MAIN,
|
||||
$this->getTestSysop()->getUser()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Three blocks are added:
|
||||
* - an expired block with restrictions, against an IP
|
||||
* - a current block with restrictions, against a user with recent changes
|
||||
* - a current autoblock from the current block above
|
||||
*/
|
||||
public function addDBData() {
|
||||
$this->sysop = $this->getTestSysop()->getUser();
|
||||
|
||||
// Get a comment ID. One was added in addCoreDBData.
|
||||
$commentId = $this->db->select(
|
||||
'comment',
|
||||
'comment_id'
|
||||
)->fetchObject()->comment_id;
|
||||
|
||||
$commonBlockData = [
|
||||
'ipb_user' => 0,
|
||||
'ipb_by_actor' => $this->sysop->getActorId(),
|
||||
'ipb_reason_id' => $commentId,
|
||||
'ipb_timestamp' => $this->db->timestamp( '20000101000000' ),
|
||||
'ipb_auto' => 0,
|
||||
'ipb_anon_only' => 0,
|
||||
'ipb_create_account' => 0,
|
||||
'ipb_enable_autoblock' => 0,
|
||||
'ipb_expiry' => $this->db->getInfinity(),
|
||||
'ipb_range_start' => '',
|
||||
'ipb_range_end' => '',
|
||||
'ipb_deleted' => 0,
|
||||
'ipb_block_email' => 0,
|
||||
'ipb_allow_usertalk' => 0,
|
||||
'ipb_parent_block_id' => 0,
|
||||
'ipb_sitewide' => 0,
|
||||
];
|
||||
|
||||
$blockData = [
|
||||
[
|
||||
'ipb_id' => $this->expiredBlockId,
|
||||
'ipb_address' => '1.1.1.1',
|
||||
'ipb_expiry' => $this->db->timestamp( '20010101000000' ),
|
||||
],
|
||||
[
|
||||
'ipb_id' => $this->unexpiredBlockId,
|
||||
'ipb_address' => $this->sysop,
|
||||
'ipb_user' => $this->sysop->getId(),
|
||||
'ipb_enable_autoblock' => 1,
|
||||
],
|
||||
[
|
||||
'ipb_id' => $this->autoblockId,
|
||||
'ipb_address' => '2.2.2.2',
|
||||
'ipb_parent_block_id' => $this->unexpiredBlockId,
|
||||
],
|
||||
];
|
||||
|
||||
$restrictionData = [
|
||||
[
|
||||
'ir_ipb_id' => $this->expiredBlockId,
|
||||
'ir_type' => 1,
|
||||
'ir_value' => 1,
|
||||
],
|
||||
[
|
||||
'ir_ipb_id' => $this->unexpiredBlockId,
|
||||
'ir_type' => 2,
|
||||
'ir_value' => 2,
|
||||
],
|
||||
[
|
||||
'ir_ipb_id' => $this->autoblockId,
|
||||
'ir_type' => 2,
|
||||
'ir_value' => 2,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ( $blockData as $row ) {
|
||||
$this->db->insert( 'ipblocks', $row + $commonBlockData );
|
||||
}
|
||||
|
||||
foreach ( $restrictionData as $row ) {
|
||||
$this->db->insert( 'ipblocks_restrictions', $row );
|
||||
}
|
||||
|
||||
$this->tablesUsed[] = 'ipblocks';
|
||||
$this->tablesUsed[] = 'ipblocks_restrictions';
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue