wiki.techinc.nl/includes/block/BlockRestrictionStore.php
James D. Forrester df5eb22f83 Replace uses of DB_MASTER with DB_PRIMARY
Just an auto-replace from codesniffer for now.

Change-Id: I5240dc9ac5929d291b0ef1c743ea2bfd3f428266
2021-04-29 09:24:31 -07:00

452 lines
11 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 MWException;
use stdClass;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IResultWrapper;
class BlockRestrictionStore {
/**
* Map of all of the restriction types.
*/
private const TYPES_MAP = [
PageRestriction::TYPE_ID => PageRestriction::class,
NamespaceRestriction::TYPE_ID => NamespaceRestriction::class,
ActionRestriction::TYPE_ID => ActionRestriction::class,
];
/**
* @var ILoadBalancer
*/
private $loadBalancer;
/**
* @param ILoadBalancer $loadBalancer load balancer for acquiring database connections
*/
public function __construct( ILoadBalancer $loadBalancer ) {
$this->loadBalancer = $loadBalancer;
}
/**
* Retrieves the restrictions from the database by block id.
*
* @since 1.33
* @param int|array $blockId
* @param IDatabase|null $db
* @return Restriction[]
*/
public function loadByBlockId( $blockId, IDatabase $db = null ) {
if ( $blockId === null || $blockId === [] ) {
return [];
}
$db = $db ?: $this->loadBalancer->getConnectionRef( DB_REPLICA );
$result = $db->select(
[ 'ipblocks_restrictions', 'page' ],
[ 'ir_ipb_id', 'ir_type', 'ir_value', 'page_namespace', 'page_title' ],
[ 'ir_ipb_id' => $blockId ],
__METHOD__,
[],
[ 'page' => [ 'LEFT JOIN', [ 'ir_type' => PageRestriction::TYPE_ID, 'ir_value=page_id' ] ] ]
);
return $this->resultToRestrictions( $result );
}
/**
* Inserts 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 ) {
if ( !$restriction instanceof Restriction ) {
continue;
}
$rows[] = $restriction->toRow();
}
if ( !$rows ) {
return false;
}
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$dbw->insert(
'ipblocks_restrictions',
$rows,
__METHOD__,
[ 'IGNORE' ]
);
return true;
}
/**
* Updates 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
*/
public function update( array $restrictions ) {
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$dbw->startAtomic( __METHOD__ );
// Organize the restrictions by blockid.
$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 ( !empty( $blockIds ) ) {
$result = $dbw->select(
[ 'ipblocks_restrictions' ],
[ 'ir_ipb_id', 'ir_type', 'ir_value' ],
[ 'ir_ipb_id' => $blockIds ],
__METHOD__,
[ 'FOR UPDATE' ]
);
$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 );
// Update the result. The first false is the result, otherwise, true.
$result = $success && $result;
$restrictionsToRemove = $this->restrictionsToRemove(
$existingList[$blockId] ?? [],
$restrictions
);
if ( empty( $restrictionsToRemove ) ) {
continue;
}
$success = $this->delete( $restrictionsToRemove );
// Update the result. The first false is the result, otherwise, true.
$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
*/
public function updateByParentBlockId( $parentBlockId, array $restrictions ) {
// If removing all of the restrictions, then just delete them all.
if ( empty( $restrictions ) ) {
return $this->deleteByParentBlockId( $parentBlockId );
}
$parentBlockId = (int)$parentBlockId;
$db = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$db->startAtomic( __METHOD__ );
$blockIds = $db->selectFieldValues(
'ipblocks',
'ipb_id',
[ 'ipb_parent_block_id' => $parentBlockId ],
__METHOD__,
[ 'FOR UPDATE' ]
);
$result = true;
foreach ( $blockIds as $id ) {
$success = $this->update( $this->setBlockId( $id, $restrictions ) );
// Update the result. The first false is the result, otherwise, true.
$result = $success && $result;
}
$db->endAtomic( __METHOD__ );
return $result;
}
/**
* Delete the restrictions.
*
* @since 1.33
* @param Restriction[] $restrictions
* @throws MWException
* @return bool
*/
public function delete( array $restrictions ) {
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$result = true;
foreach ( $restrictions as $restriction ) {
if ( !$restriction instanceof Restriction ) {
continue;
}
$success = $dbw->delete(
'ipblocks_restrictions',
// The restriction row is made up of a compound primary key. Therefore,
// the row and the delete conditions are the same.
$restriction->toRow(),
__METHOD__
);
// Update the result. The first false is the result, otherwise, true.
$result = $success && $result;
}
return $result;
}
/**
* Delete the restrictions by block ID.
*
* @since 1.33
* @param int|array $blockId
* @throws MWException
* @return bool
*/
public function deleteByBlockId( $blockId ) {
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
return $dbw->delete(
'ipblocks_restrictions',
[ 'ir_ipb_id' => $blockId ],
__METHOD__
);
}
/**
* Delete the restrictions by parent block ID.
*
* @since 1.33
* @param int|array $parentBlockId
* @throws MWException
* @return bool
*/
public function deleteByParentBlockId( $parentBlockId ) {
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
return $dbw->deleteJoin(
'ipblocks_restrictions',
'ipblocks',
'ir_ipb_id',
'ipb_id',
[ 'ipb_parent_block_id' => $parentBlockId ],
__METHOD__
);
}
/**
* Checks 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 ) {
$filter = static function ( $restriction ) {
return $restriction instanceof Restriction;
};
// Ensure that every item in the array is a Restriction. This prevents a
// fatal error from calling Restriction::getHash if something in the array
// is not a restriction.
$a = array_filter( $a, $filter );
$b = array_filter( $b, $filter );
$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 ( $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 ) {
if ( !$restriction instanceof Restriction ) {
continue;
}
// 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 ) {
return array_filter( $existing, static function ( $e ) use ( $new ) {
foreach ( $new as $restriction ) {
if ( !$restriction instanceof Restriction ) {
continue;
}
if ( $restriction->equals( $e ) ) {
return false;
}
}
return true;
} );
}
/**
* 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 ) {
// Ensure that all of the items in the array are restrictions.
if ( !$restriction instanceof Restriction ) {
continue;
}
if ( !isset( $blockRestrictions[$restriction->getBlockId()] ) ) {
$blockRestrictions[$restriction->getBlockId()] = [];
}
$blockRestrictions[$restriction->getBlockId()][] = $restriction;
}
return $blockRestrictions;
}
/**
* Convert an 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 ) {
if ( array_key_exists( (int)$row->ir_type, self::TYPES_MAP ) ) {
$class = self::TYPES_MAP[ (int)$row->ir_type ];
return call_user_func( [ $class, 'newFromRow' ], $row );
}
return null;
}
}