Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
21.43% covered (danger)
21.43%
3 / 14
CRAP
79.45% covered (warning)
79.45%
116 / 146
BlockRestrictionStore
0.00% covered (danger)
0.00%
0 / 1
26.67% covered (danger)
26.67%
4 / 15
71.69
79.45% covered (warning)
79.45%
116 / 146
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 loadByBlockId
0.00% covered (danger)
0.00%
0 / 1
4.01
90.91% covered (success)
90.91%
10 / 11
 insert
0.00% covered (danger)
0.00%
0 / 1
5.39
75.00% covered (warning)
75.00%
12 / 16
 update
0.00% covered (danger)
0.00%
0 / 1
6
96.30% covered (success)
96.30%
26 / 27
 updateByParentBlockId
0.00% covered (danger)
0.00%
0 / 1
4.09
82.35% covered (warning)
82.35%
14 / 17
 delete
0.00% covered (danger)
0.00%
0 / 1
4.01
90.91% covered (success)
90.91%
10 / 11
 deleteByBlockId
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 deleteByParentBlockId
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
8 / 8
 equals
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 15
 anonymous function
0.00% covered (danger)
0.00%
0 / 1
4.05
85.71% covered (warning)
85.71%
6 / 7
 setBlockId
0.00% covered (danger)
0.00%
0 / 1
3.02
87.50% covered (warning)
87.50%
7 / 8
 restrictionsToRemove
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
0 / 0
 restrictionsByBlockId
0.00% covered (danger)
0.00%
0 / 1
4.03
87.50% covered (warning)
87.50%
7 / 8
 resultToRestrictions
0.00% covered (danger)
0.00%
0 / 1
3.03
85.71% covered (warning)
85.71%
6 / 7
 rowToRestriction
0.00% covered (danger)
0.00%
0 / 1
2.06
75.00% covered (warning)
75.00%
3 / 4
<?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\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\Restriction\Restriction;
use MWException;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
class BlockRestrictionStore {
    /**
     * Map of all of the restriction types.
     */
    private $types = [
        PageRestriction::TYPE_ID => PageRestriction::class,
        NamespaceRestriction::TYPE_ID => NamespaceRestriction::class,
    ];
    /**
     * @var ILoadBalancer
     */
    private $loadBalancer;
    /*
     * @param LoadBalancer $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->getConnection( 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->getConnection( DB_MASTER );
        $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->getConnection( DB_MASTER );
        $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->getConnection( DB_MASTER );
        $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[]|null $restrictions
     * @throws MWException
     * @return bool
     */
    public function delete( array $restrictions ) {
        $dbw = $this->loadBalancer->getConnection( DB_MASTER );
        $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->getConnection( DB_MASTER );
        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->getConnection( DB_MASTER );
        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 = 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 = 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, 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, $this->types ) ) {
            $class = $this->types[ (int)$row->ir_type ];
            return call_user_func( [ $class, 'newFromRow' ], $row );
        }
        return null;
    }
}