3 * Block restriction interface.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 namespace MediaWiki\Block
;
25 use MediaWiki\Block\Restriction\NamespaceRestriction
;
26 use MediaWiki\Block\Restriction\PageRestriction
;
27 use MediaWiki\Block\Restriction\Restriction
;
30 use Wikimedia\Rdbms\IResultWrapper
;
31 use Wikimedia\Rdbms\IDatabase
;
32 use Wikimedia\Rdbms\ILoadBalancer
;
34 class BlockRestrictionStore
{
37 * Map of all of the restriction types.
40 PageRestriction
::TYPE_ID
=> PageRestriction
::class,
41 NamespaceRestriction
::TYPE_ID
=> NamespaceRestriction
::class,
47 private $loadBalancer;
50 * @param ILoadBalancer $loadBalancer load balancer for acquiring database connections
52 public function __construct( ILoadBalancer
$loadBalancer ) {
53 $this->loadBalancer
= $loadBalancer;
57 * Retrieves the restrictions from the database by block id.
60 * @param int|array $blockId
61 * @param IDatabase|null $db
62 * @return Restriction[]
64 public function loadByBlockId( $blockId, IDatabase
$db = null ) {
65 if ( $blockId === null ||
$blockId === [] ) {
69 $db = $db ?
: $this->loadBalancer
->getConnectionRef( DB_REPLICA
);
71 $result = $db->select(
72 [ 'ipblocks_restrictions', 'page' ],
73 [ 'ir_ipb_id', 'ir_type', 'ir_value', 'page_namespace', 'page_title' ],
74 [ 'ir_ipb_id' => $blockId ],
77 [ 'page' => [ 'LEFT JOIN', [ 'ir_type' => PageRestriction
::TYPE_ID
, 'ir_value=page_id' ] ] ]
80 return $this->resultToRestrictions( $result );
84 * Inserts the restrictions into the database.
87 * @param Restriction[] $restrictions
90 public function insert( array $restrictions ) {
91 if ( !$restrictions ) {
96 foreach ( $restrictions as $restriction ) {
97 if ( !$restriction instanceof Restriction
) {
100 $rows[] = $restriction->toRow();
107 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
110 'ipblocks_restrictions',
120 * Updates the list of restrictions. This method does not allow removing all
121 * of the restrictions. To do that, use ::deleteByBlockId().
124 * @param Restriction[] $restrictions
127 public function update( array $restrictions ) {
128 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
130 $dbw->startAtomic( __METHOD__
);
132 // Organize the restrictions by blockid.
133 $restrictionList = $this->restrictionsByBlockId( $restrictions );
135 // Load the existing restrictions and organize by block id. Any block ids
136 // that were passed into this function will be used to load all of the
137 // existing restrictions. This list might be the same, or may be completely
140 $blockIds = array_keys( $restrictionList );
141 if ( !empty( $blockIds ) ) {
142 $result = $dbw->select(
143 [ 'ipblocks_restrictions' ],
144 [ 'ir_ipb_id', 'ir_type', 'ir_value' ],
145 [ 'ir_ipb_id' => $blockIds ],
150 $existingList = $this->restrictionsByBlockId(
151 $this->resultToRestrictions( $result )
156 // Perform the actions on a per block-id basis.
157 foreach ( $restrictionList as $blockId => $blockRestrictions ) {
158 // Insert all of the restrictions first, ignoring ones that already exist.
159 $success = $this->insert( $blockRestrictions );
161 // Update the result. The first false is the result, otherwise, true.
162 $result = $success && $result;
164 $restrictionsToRemove = $this->restrictionsToRemove(
165 $existingList[$blockId] ??
[],
169 if ( empty( $restrictionsToRemove ) ) {
173 $success = $this->delete( $restrictionsToRemove );
175 // Update the result. The first false is the result, otherwise, true.
176 $result = $success && $result;
179 $dbw->endAtomic( __METHOD__
);
185 * Updates the list of restrictions by parent id.
188 * @param int $parentBlockId
189 * @param Restriction[] $restrictions
192 public function updateByParentBlockId( $parentBlockId, array $restrictions ) {
193 // If removing all of the restrictions, then just delete them all.
194 if ( empty( $restrictions ) ) {
195 return $this->deleteByParentBlockId( $parentBlockId );
198 $parentBlockId = (int)$parentBlockId;
200 $db = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
202 $db->startAtomic( __METHOD__
);
204 $blockIds = $db->selectFieldValues(
207 [ 'ipb_parent_block_id' => $parentBlockId ],
213 foreach ( $blockIds as $id ) {
214 $success = $this->update( $this->setBlockId( $id, $restrictions ) );
215 // Update the result. The first false is the result, otherwise, true.
216 $result = $success && $result;
219 $db->endAtomic( __METHOD__
);
225 * Delete the restrictions.
228 * @param Restriction[] $restrictions
229 * @throws MWException
232 public function delete( array $restrictions ) {
233 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
235 foreach ( $restrictions as $restriction ) {
236 if ( !$restriction instanceof Restriction
) {
240 $success = $dbw->delete(
241 'ipblocks_restrictions',
242 // The restriction row is made up of a compound primary key. Therefore,
243 // the row and the delete conditions are the same.
244 $restriction->toRow(),
247 // Update the result. The first false is the result, otherwise, true.
248 $result = $success && $result;
255 * Delete the restrictions by block ID.
258 * @param int|array $blockId
259 * @throws MWException
262 public function deleteByBlockId( $blockId ) {
263 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
265 'ipblocks_restrictions',
266 [ 'ir_ipb_id' => $blockId ],
272 * Delete the restrictions by parent block ID.
275 * @param int|array $parentBlockId
276 * @throws MWException
279 public function deleteByParentBlockId( $parentBlockId ) {
280 $dbw = $this->loadBalancer
->getConnectionRef( DB_MASTER
);
281 return $dbw->deleteJoin(
282 'ipblocks_restrictions',
286 [ 'ipb_parent_block_id' => $parentBlockId ],
292 * Checks if two arrays of Restrictions are effectively equal. This is a loose
293 * equality check as the restrictions do not have to contain the same block
297 * @param Restriction[] $a
298 * @param Restriction[] $b
301 public function equals( array $a, array $b ) {
302 $filter = function ( $restriction ) {
303 return $restriction instanceof Restriction
;
306 // Ensure that every item in the array is a Restriction. This prevents a
307 // fatal error from calling Restriction::getHash if something in the array
308 // is not a restriction.
309 $a = array_filter( $a, $filter );
310 $b = array_filter( $b, $filter );
312 $aCount = count( $a );
313 $bCount = count( $b );
315 // If the count is different, then they are obviously a different set.
316 if ( $aCount !== $bCount ) {
320 // If both sets contain no items, then they are the same set.
321 if ( $aCount === 0 && $bCount === 0 ) {
325 $hasher = function ( $r ) {
326 return $r->getHash();
329 $aHashes = array_map( $hasher, $a );
330 $bHashes = array_map( $hasher, $b );
335 return $aHashes === $bHashes;
339 * Set the blockId on a set of restrictions and return a new set.
342 * @param int $blockId
343 * @param Restriction[] $restrictions
344 * @return Restriction[]
346 public function setBlockId( $blockId, array $restrictions ) {
347 $blockRestrictions = [];
349 foreach ( $restrictions as $restriction ) {
350 if ( !$restriction instanceof Restriction
) {
354 // Clone the restriction so any references to the current restriction are
355 // not suddenly changed to a different blockId.
356 $restriction = clone $restriction;
357 $restriction->setBlockId( $blockId );
359 $blockRestrictions[] = $restriction;
362 return $blockRestrictions;
366 * Get the restrictions that should be removed, which are existing
367 * restrictions that are not in the new list of restrictions.
369 * @param Restriction[] $existing
370 * @param Restriction[] $new
373 private function restrictionsToRemove( array $existing, array $new ) {
374 return array_filter( $existing, function ( $e ) use ( $new ) {
375 foreach ( $new as $restriction ) {
376 if ( !$restriction instanceof Restriction
) {
380 if ( $restriction->equals( $e ) ) {
390 * Converts an array of restrictions to an associative array of restrictions
391 * where the keys are the block ids.
393 * @param Restriction[] $restrictions
396 private function restrictionsByBlockId( array $restrictions ) {
397 $blockRestrictions = [];
399 foreach ( $restrictions as $restriction ) {
400 // Ensure that all of the items in the array are restrictions.
401 if ( !$restriction instanceof Restriction
) {
405 if ( !isset( $blockRestrictions[$restriction->getBlockId()] ) ) {
406 $blockRestrictions[$restriction->getBlockId()] = [];
409 $blockRestrictions[$restriction->getBlockId()][] = $restriction;
412 return $blockRestrictions;
416 * Convert an Result Wrapper to an array of restrictions.
418 * @param IResultWrapper $result
419 * @return Restriction[]
421 private function resultToRestrictions( IResultWrapper
$result ) {
423 foreach ( $result as $row ) {
424 $restriction = $this->rowToRestriction( $row );
426 if ( !$restriction ) {
430 $restrictions[] = $restriction;
433 return $restrictions;
437 * Convert a result row from the database into a restriction object.
439 * @param stdClass $row
440 * @return Restriction|null
442 private function rowToRestriction( stdClass
$row ) {
443 if ( array_key_exists( (int)$row->ir_type
, $this->types
) ) {
444 $class = $this->types
[ (int)$row->ir_type
];
445 return call_user_func( [ $class, 'newFromRow' ], $row );