Support migration stages when reading and writing blocks. I tried to set it up for an easy next stage, in which support for the old schema is removed. I tried to avoid factoring out of shared code between the two schemas, so that the old schema cases can simply be deleted without the need to revert unnecessary abstractions. However, I added HideUserUtils to factor out ipb_deleted queries. Code review showed that this was already quite complex, with multiple approaches to the problem, so it benefits from refactoring even without the schema abstraction. HideUserUtils is a service rather than a standalone class to support unit tests, since unit tests do not allow global config access. When the migration stage config is removed, it will be a service with no constructor parameters -- an unnecessary abstraction which should ideally be resolved at that time. When interpreting result rows, it is possible to share code by using field aliases. But when constructing WHERE conditions, the actual field names need to be used, so the migration is more intrusive in ApiQueryBlocks and SpecialBlockList, where complex conditions are used. Bug: T346293 Bug: T51504 Bug: T349883 Change-Id: I408acf7a57b0100fe18c455fc13141277a598925
403 lines
10 KiB
PHP
403 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Block restriction interface.
|
|
*
|
|
* 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 MediaWiki\Block\Restriction\ActionRestriction;
|
|
use MediaWiki\Block\Restriction\NamespaceRestriction;
|
|
use MediaWiki\Block\Restriction\PageRestriction;
|
|
use MediaWiki\Block\Restriction\Restriction;
|
|
use MediaWiki\DAO\WikiAwareEntity;
|
|
use stdClass;
|
|
use Wikimedia\Rdbms\IConnectionProvider;
|
|
use Wikimedia\Rdbms\IResultWrapper;
|
|
|
|
class BlockRestrictionStore {
|
|
|
|
/**
|
|
* @var IConnectionProvider
|
|
*/
|
|
private $dbProvider;
|
|
|
|
/**
|
|
* @var string|false
|
|
*/
|
|
private $wikiId;
|
|
|
|
/**
|
|
* @var int The block_target read stage.
|
|
* Temporary -- will be deleted once migration is complete.
|
|
*/
|
|
private $readStage;
|
|
|
|
/**
|
|
* @param IConnectionProvider $dbProvider
|
|
* @param int $blockTargetMigrationStage
|
|
* @param string|false $wikiId
|
|
*/
|
|
public function __construct(
|
|
IConnectionProvider $dbProvider,
|
|
$blockTargetMigrationStage,
|
|
$wikiId = WikiAwareEntity::LOCAL
|
|
) {
|
|
$this->dbProvider = $dbProvider;
|
|
$this->wikiId = $wikiId;
|
|
$this->readStage = $blockTargetMigrationStage & SCHEMA_COMPAT_READ_MASK;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the restrictions from the database by block ID.
|
|
*
|
|
* @since 1.33
|
|
* @param int|int[] $blockId
|
|
* @return Restriction[]
|
|
*/
|
|
public function loadByBlockId( $blockId ) {
|
|
if ( $blockId === null || $blockId === [] ) {
|
|
return [];
|
|
}
|
|
|
|
$result = $this->dbProvider->getReplicaDatabase( $this->wikiId )
|
|
->newSelectQueryBuilder()
|
|
->select( [ 'ir_ipb_id', 'ir_type', 'ir_value', 'page_namespace', 'page_title' ] )
|
|
->from( 'ipblocks_restrictions' )
|
|
->leftJoin( 'page', null, [ 'ir_type' => PageRestriction::TYPE_ID, 'ir_value=page_id' ] )
|
|
->where( [ 'ir_ipb_id' => $blockId ] )
|
|
->caller( __METHOD__ )->fetchResultSet();
|
|
|
|
return $this->resultToRestrictions( $result );
|
|
}
|
|
|
|
/**
|
|
* Insert the restrictions into the database.
|
|
*
|
|
* @since 1.33
|
|
* @param Restriction[] $restrictions
|
|
* @return bool
|
|
*/
|
|
public function insert( array $restrictions ) {
|
|
if ( !$restrictions ) {
|
|
return false;
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ( $restrictions as $restriction ) {
|
|
$rows[] = $restriction->toRow();
|
|
}
|
|
|
|
$dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId );
|
|
|
|
$dbw->newInsertQueryBuilder()
|
|
->insertInto( 'ipblocks_restrictions' )
|
|
->ignore()
|
|
->rows( $rows )
|
|
->caller( __METHOD__ )->execute();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update the list of restrictions. This method does not allow removing all
|
|
* of the restrictions. To do that, use ::deleteByBlockId().
|
|
*
|
|
* @since 1.33
|
|
* @param Restriction[] $restrictions
|
|
* @return bool Whether all operations were successful
|
|
*/
|
|
public function update( array $restrictions ) {
|
|
$dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId );
|
|
|
|
$dbw->startAtomic( __METHOD__ );
|
|
|
|
// Organize the restrictions by block ID.
|
|
$restrictionList = $this->restrictionsByBlockId( $restrictions );
|
|
|
|
// Load the existing restrictions and organize by block ID. Any block IDs
|
|
// that were passed into this function will be used to load all of the
|
|
// existing restrictions. This list might be the same, or may be completely
|
|
// different.
|
|
$existingList = [];
|
|
$blockIds = array_keys( $restrictionList );
|
|
if ( $blockIds ) {
|
|
$result = $dbw->newSelectQueryBuilder()
|
|
->select( [ 'ir_ipb_id', 'ir_type', 'ir_value' ] )
|
|
->forUpdate()
|
|
->from( 'ipblocks_restrictions' )
|
|
->where( [ 'ir_ipb_id' => $blockIds ] )
|
|
->caller( __METHOD__ )->fetchResultSet();
|
|
|
|
$existingList = $this->restrictionsByBlockId(
|
|
$this->resultToRestrictions( $result )
|
|
);
|
|
}
|
|
|
|
$result = true;
|
|
// Perform the actions on a per block-ID basis.
|
|
foreach ( $restrictionList as $blockId => $blockRestrictions ) {
|
|
// Insert all of the restrictions first, ignoring ones that already exist.
|
|
$success = $this->insert( $blockRestrictions );
|
|
|
|
$result = $success && $result;
|
|
|
|
$restrictionsToRemove = $this->restrictionsToRemove(
|
|
$existingList[$blockId] ?? [],
|
|
$restrictions
|
|
);
|
|
|
|
if ( !$restrictionsToRemove ) {
|
|
continue;
|
|
}
|
|
|
|
$success = $this->delete( $restrictionsToRemove );
|
|
|
|
$result = $success && $result;
|
|
}
|
|
|
|
$dbw->endAtomic( __METHOD__ );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Updates the list of restrictions by parent ID.
|
|
*
|
|
* @since 1.33
|
|
* @param int $parentBlockId
|
|
* @param Restriction[] $restrictions
|
|
* @return bool Whether all updates were successful
|
|
*/
|
|
public function updateByParentBlockId( $parentBlockId, array $restrictions ) {
|
|
$parentBlockId = (int)$parentBlockId;
|
|
|
|
$db = $this->dbProvider->getPrimaryDatabase( $this->wikiId );
|
|
|
|
if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
|
|
$blockIds = $db->newSelectQueryBuilder()
|
|
->select( [ 'ipb_id' ] )
|
|
->forUpdate()
|
|
->from( 'ipblocks' )
|
|
->where( [ 'ipb_parent_block_id' => $parentBlockId ] )
|
|
->caller( __METHOD__ )->fetchFieldValues();
|
|
} else {
|
|
$blockIds = $db->newSelectQueryBuilder()
|
|
->select( 'bl_id' )
|
|
->forUpdate()
|
|
->from( 'block' )
|
|
->where( [ 'bl_parent_block_id' => $parentBlockId ] )
|
|
->caller( __METHOD__ )->fetchFieldValues();
|
|
}
|
|
if ( !$blockIds ) {
|
|
return true;
|
|
}
|
|
|
|
// If removing all of the restrictions, then just delete them all.
|
|
if ( !$restrictions ) {
|
|
$blockIds = array_map( 'intval', $blockIds );
|
|
return $this->deleteByBlockId( $blockIds );
|
|
}
|
|
|
|
$db->startAtomic( __METHOD__ );
|
|
|
|
$result = true;
|
|
foreach ( $blockIds as $id ) {
|
|
$success = $this->update( $this->setBlockId( $id, $restrictions ) );
|
|
$result = $success && $result;
|
|
}
|
|
|
|
$db->endAtomic( __METHOD__ );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Delete the restrictions.
|
|
*
|
|
* @since 1.33
|
|
* @param Restriction[] $restrictions
|
|
* @return bool
|
|
*/
|
|
public function delete( array $restrictions ) {
|
|
$dbw = $this->dbProvider->getPrimaryDatabase( $this->wikiId );
|
|
foreach ( $restrictions as $restriction ) {
|
|
$dbw->newDeleteQueryBuilder()
|
|
->deleteFrom( 'ipblocks_restrictions' )
|
|
// The restriction row is made up of a compound primary key. Therefore,
|
|
// the row and the delete conditions are the same.
|
|
->where( $restriction->toRow() )
|
|
->caller( __METHOD__ )->execute();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Delete the restrictions by block ID.
|
|
*
|
|
* @since 1.33
|
|
* @param int|int[] $blockId
|
|
* @return bool
|
|
*/
|
|
public function deleteByBlockId( $blockId ) {
|
|
$this->dbProvider->getPrimaryDatabase( $this->wikiId )
|
|
->newDeleteQueryBuilder()
|
|
->deleteFrom( 'ipblocks_restrictions' )
|
|
->where( [ 'ir_ipb_id' => $blockId ] )
|
|
->caller( __METHOD__ )->execute();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if two arrays of Restrictions are effectively equal. This is a loose
|
|
* equality check as the restrictions do not have to contain the same block
|
|
* IDs.
|
|
*
|
|
* @since 1.33
|
|
* @param Restriction[] $a
|
|
* @param Restriction[] $b
|
|
* @return bool
|
|
*/
|
|
public function equals( array $a, array $b ) {
|
|
$aCount = count( $a );
|
|
$bCount = count( $b );
|
|
|
|
// If the count is different, then they are obviously a different set.
|
|
if ( $aCount !== $bCount ) {
|
|
return false;
|
|
}
|
|
|
|
// If both sets contain no items, then they are the same set.
|
|
if ( $aCount === 0 && $bCount === 0 ) {
|
|
return true;
|
|
}
|
|
|
|
$hasher = static function ( Restriction $r ) {
|
|
return $r->getHash();
|
|
};
|
|
|
|
$aHashes = array_map( $hasher, $a );
|
|
$bHashes = array_map( $hasher, $b );
|
|
|
|
sort( $aHashes );
|
|
sort( $bHashes );
|
|
|
|
return $aHashes === $bHashes;
|
|
}
|
|
|
|
/**
|
|
* Set the blockId on a set of restrictions and return a new set.
|
|
*
|
|
* @since 1.33
|
|
* @param int $blockId
|
|
* @param Restriction[] $restrictions
|
|
* @return Restriction[]
|
|
*/
|
|
public function setBlockId( $blockId, array $restrictions ) {
|
|
$blockRestrictions = [];
|
|
|
|
foreach ( $restrictions as $restriction ) {
|
|
// Clone the restriction so any references to the current restriction are
|
|
// not suddenly changed to a different blockId.
|
|
$restriction = clone $restriction;
|
|
$restriction->setBlockId( $blockId );
|
|
|
|
$blockRestrictions[] = $restriction;
|
|
}
|
|
|
|
return $blockRestrictions;
|
|
}
|
|
|
|
/**
|
|
* Get the restrictions that should be removed, which are existing
|
|
* restrictions that are not in the new list of restrictions.
|
|
*
|
|
* @param Restriction[] $existing
|
|
* @param Restriction[] $new
|
|
* @return array
|
|
*/
|
|
private function restrictionsToRemove( array $existing, array $new ) {
|
|
$restrictionsByHash = [];
|
|
foreach ( $existing as $restriction ) {
|
|
$restrictionsByHash[$restriction->getHash()] = $restriction;
|
|
}
|
|
foreach ( $new as $restriction ) {
|
|
unset( $restrictionsByHash[$restriction->getHash()] );
|
|
}
|
|
return array_values( $restrictionsByHash );
|
|
}
|
|
|
|
/**
|
|
* Converts an array of restrictions to an associative array of restrictions
|
|
* where the keys are the block IDs.
|
|
*
|
|
* @param Restriction[] $restrictions
|
|
* @return array
|
|
*/
|
|
private function restrictionsByBlockId( array $restrictions ) {
|
|
$blockRestrictions = [];
|
|
|
|
foreach ( $restrictions as $restriction ) {
|
|
$blockRestrictions[$restriction->getBlockId()][] = $restriction;
|
|
}
|
|
|
|
return $blockRestrictions;
|
|
}
|
|
|
|
/**
|
|
* Convert a result wrapper to an array of restrictions.
|
|
*
|
|
* @param IResultWrapper $result
|
|
* @return Restriction[]
|
|
*/
|
|
private function resultToRestrictions( IResultWrapper $result ) {
|
|
$restrictions = [];
|
|
foreach ( $result as $row ) {
|
|
$restriction = $this->rowToRestriction( $row );
|
|
|
|
if ( !$restriction ) {
|
|
continue;
|
|
}
|
|
|
|
$restrictions[] = $restriction;
|
|
}
|
|
|
|
return $restrictions;
|
|
}
|
|
|
|
/**
|
|
* Convert a result row from the database into a restriction object.
|
|
*
|
|
* @param stdClass $row
|
|
* @return Restriction|null
|
|
*/
|
|
private function rowToRestriction( stdClass $row ) {
|
|
switch ( (int)$row->ir_type ) {
|
|
case PageRestriction::TYPE_ID:
|
|
return PageRestriction::newFromRow( $row );
|
|
case NamespaceRestriction::TYPE_ID:
|
|
return NamespaceRestriction::newFromRow( $row );
|
|
case ActionRestriction::TYPE_ID:
|
|
return ActionRestriction::newFromRow( $row );
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|