Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.45% covered (warning)
74.45%
577 / 775
35.90% covered (danger)
35.90%
14 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
DatabaseBlockStore
74.45% covered (warning)
74.45%
577 / 775
35.90% covered (danger)
35.90%
14 / 39
658.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getReadStage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getWriteStage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 newFromID
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getQueryInfo
51.52% covered (warning)
51.52%
34 / 66
0.00% covered (danger)
0.00%
0 / 1
4.03
 newLoad
72.31% covered (warning)
72.31%
47 / 65
0.00% covered (danger)
0.00%
0 / 1
40.36
 chooseMostSpecificBlock
37.50% covered (danger)
37.50%
6 / 16
0.00% covered (danger)
0.00%
0 / 1
14.79
 getRangeCond
62.50% covered (warning)
62.50%
20 / 32
0.00% covered (danger)
0.00%
0 / 1
6.32
 getIpFragment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 newFromRow
51.06% covered (warning)
51.06%
24 / 47
0.00% covered (danger)
0.00%
0 / 1
5.88
 newFromTarget
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newListFromTarget
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
7.29
 newListFromIPs
88.46% covered (warning)
88.46%
23 / 26
0.00% covered (danger)
0.00%
0 / 1
8.10
 newListFromConds
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 purgeExpiredBlocks
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 deleteBlocksMatchingConds
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
3.02
 mapActorAlias
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 hasActorAlias
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 mapConds
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 deleteBlockRows
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 releaseTargets
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getReplicaDB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryDB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 insertBlock
82.14% covered (warning)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
9.46
 attemptInsert
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
5.03
 purgeExpiredConflicts
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getTargetConds
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 acquireTarget
91.67% covered (success)
91.67%
55 / 60
0.00% covered (danger)
0.00%
0 / 1
18.19
 updateBlock
89.47% covered (warning)
89.47%
34 / 38
0.00% covered (danger)
0.00%
0 / 1
7.06
 updateTarget
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 deleteBlock
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
3
 getArrayForBlockUpdate
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 getArrayForAutoblockUpdate
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
3
 doRetroactiveAutoblock
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 performRetroactiveAutoblock
84.62% covered (warning)
84.62%
22 / 26
0.00% covered (danger)
0.00%
0 / 1
7.18
 doAutoblock
82.22% covered (warning)
82.22%
37 / 45
0.00% covered (danger)
0.00%
0 / 1
11.68
 getAutoblockReason
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 updateTimestamp
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 getAutoblockExpiry
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
1<?php
2/**
3 * Class for DatabaseBlock objects to interact with the database
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 */
22
23namespace MediaWiki\Block;
24
25use InvalidArgumentException;
26use MediaWiki\CommentStore\CommentStore;
27use MediaWiki\Config\ServiceOptions;
28use MediaWiki\Deferred\AutoCommitUpdate;
29use MediaWiki\Deferred\DeferredUpdates;
30use MediaWiki\HookContainer\HookContainer;
31use MediaWiki\HookContainer\HookRunner;
32use MediaWiki\MainConfigNames;
33use MediaWiki\User\ActorStoreFactory;
34use MediaWiki\User\TempUser\TempUserConfig;
35use MediaWiki\User\UserFactory;
36use MediaWiki\User\UserIdentity;
37use MediaWiki\User\UserIdentityValue;
38use Psr\Log\LoggerInterface;
39use RuntimeException;
40use stdClass;
41use Wikimedia\IPUtils;
42use Wikimedia\Rdbms\IConnectionProvider;
43use Wikimedia\Rdbms\IDatabase;
44use Wikimedia\Rdbms\IExpression;
45use Wikimedia\Rdbms\IReadableDatabase;
46use Wikimedia\Rdbms\IResultWrapper;
47use Wikimedia\Rdbms\LikeValue;
48use Wikimedia\Rdbms\RawSQLExpression;
49use Wikimedia\Rdbms\RawSQLValue;
50use Wikimedia\Rdbms\ReadOnlyMode;
51use Wikimedia\Rdbms\SelectQueryBuilder;
52use function array_key_exists;
53
54/**
55 * @since 1.36
56 *
57 * @author DannyS712
58 */
59class DatabaseBlockStore {
60    /** The old schema */
61    public const SCHEMA_IPBLOCKS = 'ipblocks';
62    /** The new schema */
63    public const SCHEMA_BLOCK = 'block';
64    /** The schema currently selected by the read stage */
65    public const SCHEMA_CURRENT = 'current';
66
67    /**
68     * @internal For use by ServiceWiring
69     */
70    public const CONSTRUCTOR_OPTIONS = [
71        MainConfigNames::AutoblockExpiry,
72        MainConfigNames::BlockCIDRLimit,
73        MainConfigNames::BlockDisablesLogin,
74        MainConfigNames::PutIPinRC,
75        MainConfigNames::UpdateRowsPerQuery,
76    ];
77
78    /** @var string|false */
79    private $wikiId;
80
81    /** @var ServiceOptions */
82    private $options;
83
84    /** @var LoggerInterface */
85    private $logger;
86
87    /** @var ActorStoreFactory */
88    private $actorStoreFactory;
89
90    /** @var BlockRestrictionStore */
91    private $blockRestrictionStore;
92
93    /** @var CommentStore */
94    private $commentStore;
95
96    /** @var HookRunner */
97    private $hookRunner;
98
99    /** @var IConnectionProvider */
100    private $dbProvider;
101
102    /** @var ReadOnlyMode */
103    private $readOnlyMode;
104
105    /** @var UserFactory */
106    private $userFactory;
107
108    /** @var TempUserConfig */
109    private $tempUserConfig;
110
111    /** @var BlockUtils */
112    private $blockUtils;
113
114    /** @var AutoblockExemptionList */
115    private $autoblockExemptionList;
116
117    /**
118     * @param ServiceOptions $options
119     * @param LoggerInterface $logger
120     * @param ActorStoreFactory $actorStoreFactory
121     * @param BlockRestrictionStore $blockRestrictionStore
122     * @param CommentStore $commentStore
123     * @param HookContainer $hookContainer
124     * @param IConnectionProvider $dbProvider
125     * @param ReadOnlyMode $readOnlyMode
126     * @param UserFactory $userFactory
127     * @param TempUserConfig $tempUserConfig
128     * @param BlockUtils $blockUtils
129     * @param AutoblockExemptionList $autoblockExemptionList
130     * @param string|false $wikiId
131     */
132    public function __construct(
133        ServiceOptions $options,
134        LoggerInterface $logger,
135        ActorStoreFactory $actorStoreFactory,
136        BlockRestrictionStore $blockRestrictionStore,
137        CommentStore $commentStore,
138        HookContainer $hookContainer,
139        IConnectionProvider $dbProvider,
140        ReadOnlyMode $readOnlyMode,
141        UserFactory $userFactory,
142        TempUserConfig $tempUserConfig,
143        BlockUtils $blockUtils,
144        AutoblockExemptionList $autoblockExemptionList,
145        $wikiId = DatabaseBlock::LOCAL
146    ) {
147        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
148
149        $this->wikiId = $wikiId;
150
151        $this->options = $options;
152        $this->logger = $logger;
153        $this->actorStoreFactory = $actorStoreFactory;
154        $this->blockRestrictionStore = $blockRestrictionStore;
155        $this->commentStore = $commentStore;
156        $this->hookRunner = new HookRunner( $hookContainer );
157        $this->dbProvider = $dbProvider;
158        $this->readOnlyMode = $readOnlyMode;
159        $this->userFactory = $userFactory;
160        $this->tempUserConfig = $tempUserConfig;
161        $this->blockUtils = $blockUtils;
162        $this->autoblockExemptionList = $autoblockExemptionList;
163    }
164
165    /**
166     * Get the read stage of the block_target migration
167     *
168     * @since 1.42
169     * @deprecated since 1.43
170     * @return int
171     */
172    public function getReadStage() {
173        wfDeprecated( __METHOD__, '1.43' );
174        return SCHEMA_COMPAT_NEW;
175    }
176
177    /**
178     * Get the write stage of the block_target migration
179     *
180     * @since 1.42
181     * @deprecated since 1.43
182     * @return int
183     */
184    public function getWriteStage() {
185        wfDeprecated( __METHOD__, '1.43' );
186        return SCHEMA_COMPAT_NEW;
187    }
188
189    /***************************************************************************/
190    // region   Database read methods
191    /** @name   Database read methods */
192
193    /**
194     * Load a block from the block ID.
195     *
196     * @since 1.42
197     * @param int $id ID to search for
198     * @return DatabaseBlock|null
199     */
200    public function newFromID( $id ) {
201        $dbr = $this->getReplicaDB();
202        $blockQuery = $this->getQueryInfo();
203        $res = $dbr->newSelectQueryBuilder()
204            ->queryInfo( $blockQuery )
205            ->where( [ 'bl_id' => $id ] )
206            ->caller( __METHOD__ )
207            ->fetchRow();
208        if ( $res ) {
209            return $this->newFromRow( $dbr, $res );
210        } else {
211            return null;
212        }
213    }
214
215    /**
216     * Return the tables, fields, and join conditions to be selected to create
217     * a new block object.
218     *
219     * Since 1.34, ipb_by and ipb_by_text have not been present in the
220     * database, but they continue to be available in query results as
221     * aliases.
222     *
223     * @since 1.42
224     * @internal Avoid this method and DatabaseBlock::getQueryInfo() in new
225     *   external code, since they are not schema-independent. Use
226     *   newListFromConds() and deleteBlocksMatchingConds().
227     *
228     * @param string $schema What schema to use for field aliases. May be either
229     *   self::SCHEMA_IPBLOCKS or self::SCHEMA_BLOCK. This parameter will soon be
230     *   removed.
231     * @return array[] With three keys:
232     *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
233     *     or `SelectQueryBuilder::tables`
234     *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
235     *     or `SelectQueryBuilder::fields`
236     *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
237     *     or `SelectQueryBuilder::joinConds`
238     * @phan-return array{tables:string[],fields:string[],joins:array}
239     */
240    public function getQueryInfo( $schema = self::SCHEMA_BLOCK ) {
241        $commentQuery = $this->commentStore->getJoin( 'bl_reason' );
242        if ( $schema === self::SCHEMA_IPBLOCKS ) {
243            return [
244                'tables' => [
245                    'block',
246                    'block_by_actor' => 'actor',
247                ] + $commentQuery['tables'],
248                'fields' => [
249                    'ipb_id' => 'bl_id',
250                    'ipb_address' => 'COALESCE(bt_address, bt_user_text)',
251                    'ipb_timestamp' => 'bl_timestamp',
252                    'ipb_auto' => 'bt_auto',
253                    'ipb_anon_only' => 'bl_anon_only',
254                    'ipb_create_account' => 'bl_create_account',
255                    'ipb_enable_autoblock' => 'bl_enable_autoblock',
256                    'ipb_expiry' => 'bl_expiry',
257                    'ipb_deleted' => 'bl_deleted',
258                    'ipb_block_email' => 'bl_block_email',
259                    'ipb_allow_usertalk' => 'bl_allow_usertalk',
260                    'ipb_parent_block_id' => 'bl_parent_block_id',
261                    'ipb_sitewide' => 'bl_sitewide',
262                    'ipb_by_actor' => 'bl_by_actor',
263                    'ipb_by' => 'block_by_actor.actor_user',
264                    'ipb_by_text' => 'block_by_actor.actor_name',
265                    'ipb_reason_text' => $commentQuery['fields']['bl_reason_text'],
266                    'ipb_reason_data' => $commentQuery['fields']['bl_reason_data'],
267                    'ipb_reason_cid' => $commentQuery['fields']['bl_reason_cid'],
268                ],
269                'joins' => [
270                    'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ],
271                ] + $commentQuery['joins'],
272            ];
273        } elseif ( $schema === self::SCHEMA_BLOCK ) {
274            return [
275                'tables' => [
276                    'block',
277                    'block_target',
278                    'block_by_actor' => 'actor',
279                ] + $commentQuery['tables'],
280                'fields' => [
281                    'bl_id',
282                    'bt_address',
283                    'bt_user',
284                    'bt_user_text',
285                    'bl_timestamp',
286                    'bt_auto',
287                    'bl_anon_only',
288                    'bl_create_account',
289                    'bl_enable_autoblock',
290                    'bl_expiry',
291                    'bl_deleted',
292                    'bl_block_email',
293                    'bl_allow_usertalk',
294                    'bl_parent_block_id',
295                    'bl_sitewide',
296                    'bl_by_actor',
297                    'bl_by' => 'block_by_actor.actor_user',
298                    'bl_by_text' => 'block_by_actor.actor_name',
299                ] + $commentQuery['fields'],
300                'joins' => [
301                    'block_target' => [ 'JOIN', 'bt_id=bl_target' ],
302                    'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ],
303                ] + $commentQuery['joins'],
304            ];
305        }
306        throw new InvalidArgumentException(
307            '$schema must be SCHEMA_IPBLOCKS or SCHEMA_BLOCK' );
308    }
309
310    /**
311     * Load blocks from the database which target the specific target exactly, or which cover the
312     * vague target.
313     *
314     * @param UserIdentity|string|null $specificTarget
315     * @param int|null $specificType
316     * @param bool $fromPrimary
317     * @param UserIdentity|string|null $vagueTarget Also search for blocks affecting this target.
318     *     Doesn't make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups.
319     * @return DatabaseBlock[] Any relevant blocks
320     */
321    private function newLoad(
322        $specificTarget,
323        $specificType,
324        $fromPrimary,
325        $vagueTarget = null
326    ) {
327        if ( $fromPrimary ) {
328            $db = $this->getPrimaryDB();
329        } else {
330            $db = $this->getReplicaDB();
331        }
332
333        $userIds = [];
334        $userNames = [];
335        $addresses = [];
336        $ranges = [];
337        if ( $specificType === Block::TYPE_USER ) {
338            if ( $specificTarget instanceof UserIdentity ) {
339                $userId = $specificTarget->getId( $this->wikiId );
340                if ( $userId ) {
341                    $userIds[] = $specificTarget->getId( $this->wikiId );
342                } else {
343                    // A nonexistent user can have no blocks.
344                    // This case is hit in testing, possibly production too.
345                    // Ignoring the user is optimal for production performance.
346                }
347            } else {
348                $userNames[] = (string)$specificTarget;
349            }
350        } elseif ( in_array( $specificType, [ Block::TYPE_IP, Block::TYPE_RANGE ], true ) ) {
351            $addresses[] = (string)$specificTarget;
352        }
353
354        // Be aware that the != '' check is explicit, since empty values will be
355        // passed by some callers (T31116)
356        if ( $vagueTarget != '' ) {
357            [ $target, $type ] = $this->blockUtils->parseBlockTarget( $vagueTarget );
358            switch ( $type ) {
359                case Block::TYPE_USER:
360                    // Slightly weird, but who are we to argue?
361                    /** @var UserIdentity $vagueUser */
362                    $vagueUser = $target;
363                    if ( $vagueUser->getId( $this->wikiId ) ) {
364                        $userIds[] = $vagueUser->getId( $this->wikiId );
365                    } else {
366                        $userNames[] = $vagueUser->getName();
367                    }
368                    break;
369
370                case Block::TYPE_IP:
371                    $ranges[] = [ IPUtils::toHex( $target ), null ];
372                    break;
373
374                case Block::TYPE_RANGE:
375                    $ranges[] = IPUtils::parseRange( $target );
376                    break;
377
378                default:
379                    $this->logger->debug( "Ignoring invalid vague target" );
380            }
381        }
382
383        $orConds = [];
384        if ( $userIds ) {
385            // @phan-suppress-next-line PhanTypeMismatchArgument -- array_unique() result is non-empty
386            $orConds[] = $db->expr( 'bt_user', '=', array_unique( $userIds ) );
387        }
388        if ( $userNames ) {
389            // Add bt_ip_hex to the condition since it is in the index
390            $orConds[] = $db->expr( 'bt_ip_hex', '=', null )
391                // @phan-suppress-next-line PhanTypeMismatchArgument -- array_unique() result is non-empty
392                ->and( 'bt_user_text', '=', array_unique( $userNames ) );
393        }
394        if ( $addresses ) {
395            // @phan-suppress-next-line PhanTypeMismatchArgument
396            $orConds[] = $db->expr( 'bt_address', '=', array_unique( $addresses ) );
397        }
398        foreach ( $ranges as $range ) {
399            $orConds[] = new RawSQLExpression( $this->getRangeCond( $range[0], $range[1] ) );
400        }
401        if ( !$orConds ) {
402            return [];
403        }
404
405        $blockQuery = $this->getQueryInfo();
406        $res = $db->newSelectQueryBuilder()
407            ->queryInfo( $blockQuery )
408            ->where( $db->orExpr( $orConds ) )
409            ->caller( __METHOD__ )
410            ->fetchResultSet();
411
412        $blocks = [];
413        $blockIds = [];
414        $autoBlocks = [];
415        foreach ( $res as $row ) {
416            $block = $this->newFromRow( $db, $row );
417
418            // Don't use expired blocks
419            if ( $block->isExpired() ) {
420                continue;
421            }
422
423            // Don't use anon only blocks on users
424            if (
425                $specificType == Block::TYPE_USER && $specificTarget &&
426                !$block->isHardblock() &&
427                !$this->tempUserConfig->isTempName( $specificTarget )
428            ) {
429                continue;
430            }
431
432            // Check for duplicate autoblocks
433            if ( $block->getType() === Block::TYPE_AUTO ) {
434                $autoBlocks[] = $block;
435            } else {
436                $blocks[] = $block;
437                $blockIds[] = $block->getId( $this->wikiId );
438            }
439        }
440
441        // Only add autoblocks that aren't duplicates
442        foreach ( $autoBlocks as $block ) {
443            if ( !in_array( $block->getParentBlockId(), $blockIds ) ) {
444                $blocks[] = $block;
445            }
446        }
447
448        return $blocks;
449    }
450
451    /**
452     * Choose the most specific block from some combination of user, IP and IP range
453     * blocks. Decreasing order of specificity: user > IP > narrower IP range > wider IP
454     * range. A range that encompasses one IP address is ranked equally to a singe IP.
455     *
456     * @param DatabaseBlock[] $blocks These should not include autoblocks or ID blocks
457     * @return DatabaseBlock|null The block with the most specific target
458     */
459    private function chooseMostSpecificBlock( array $blocks ) {
460        if ( count( $blocks ) === 1 ) {
461            return $blocks[0];
462        }
463
464        // This result could contain a block on the user, a block on the IP, and a russian-doll
465        // set of range blocks.  We want to choose the most specific one, so keep a leader board.
466        $bestBlock = null;
467
468        // Lower will be better
469        $bestBlockScore = 100;
470        foreach ( $blocks as $block ) {
471            if ( $block->getType() == Block::TYPE_RANGE ) {
472                // This is the number of bits that are allowed to vary in the block, give
473                // or take some floating point errors
474                $target = $block->getTargetName();
475                $max = IPUtils::isIPv6( $target ) ? 128 : 32;
476                [ , $bits ] = IPUtils::parseCIDR( $target );
477                $size = $max - $bits;
478
479                // Rank a range block covering a single IP equally with a single-IP block
480                $score = Block::TYPE_RANGE - 1 + ( $size / $max );
481
482            } else {
483                $score = $block->getType();
484            }
485
486            if ( $score < $bestBlockScore ) {
487                $bestBlockScore = $score;
488                $bestBlock = $block;
489            }
490        }
491
492        return $bestBlock;
493    }
494
495    /**
496     * Get a set of SQL conditions which select range blocks encompassing a
497     * given range. If the given range is a single IP with start=end, it will
498     * also select single IP blocks with that IP.
499     *
500     * @since 1.42
501     * @param string $start Hexadecimal IP representation
502     * @param string|null $end Hexadecimal IP representation, or null to use $start = $end
503     * @param string $schema What schema to use for field aliases. Can be one of:
504     *    - self::SCHEMA_IPBLOCKS for the old schema
505     *    - self::SCHEMA_BLOCK for the new schema
506     *    - self::SCHEMA_CURRENT formerly used the configured schema, but now
507     *      acts the same as SCHEMA_BLOCK
508     *   In future this parameter will be removed.
509     * @return string
510     */
511    public function getRangeCond( $start, $end, $schema = self::SCHEMA_BLOCK ) {
512        // Per T16634, we want to include relevant active range blocks; for
513        // range blocks, we want to include larger ranges which enclose the given
514        // range. We know that all blocks must be smaller than $wgBlockCIDRLimit,
515        // so we can improve performance by filtering on a LIKE clause
516        $chunk = $this->getIpFragment( $start );
517        $dbr = $this->getReplicaDB();
518        $end ??= $start;
519
520        if ( $schema === self::SCHEMA_CURRENT ) {
521            $schema = self::SCHEMA_BLOCK;
522        }
523
524        if ( $schema === self::SCHEMA_IPBLOCKS ) {
525            return $dbr->makeList(
526                [
527                    $dbr->expr( 'ipb_range_start', IExpression::LIKE,
528                        new LikeValue( $chunk, $dbr->anyString() ) ),
529                    $dbr->expr( 'ipb_range_start', '<=', $start ),
530                    $dbr->expr( 'ipb_range_end', '>=', $end ),
531                ],
532                LIST_AND
533            );
534        } elseif ( $schema === self::SCHEMA_BLOCK ) {
535            $expr = $dbr->expr(
536                    'bt_range_start',
537                    IExpression::LIKE,
538                    new LikeValue( $chunk, $dbr->anyString() )
539                )
540                ->and( 'bt_range_start', '<=', $start )
541                ->and( 'bt_range_end', '>=', $end );
542            if ( $start === $end ) {
543                // Also select single IP blocks for this target
544                $expr = $dbr->orExpr( [
545                    $dbr->expr( 'bt_ip_hex', '=', $start )
546                        ->and( 'bt_range_start', '=', null ),
547                    $expr
548                ] );
549            }
550            return $expr->toSql( $dbr );
551        } else {
552            throw new InvalidArgumentException(
553                '$schema must be SCHEMA_IPBLOCKS or SCHEMA_BLOCK' );
554        }
555    }
556
557    /**
558     * Get the component of an IP address which is certain to be the same between an IP
559     * address and a range block containing that IP address.
560     *
561     * @param string $hex Hexadecimal IP representation
562     * @return string
563     */
564    private function getIpFragment( $hex ) {
565        $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit );
566        if ( str_starts_with( $hex, 'v6-' ) ) {
567            return 'v6-' . substr( substr( $hex, 3 ), 0, (int)floor( $blockCIDRLimit['IPv6'] / 4 ) );
568        } else {
569            return substr( $hex, 0, (int)floor( $blockCIDRLimit['IPv4'] / 4 ) );
570        }
571    }
572
573    /**
574     * Create a new DatabaseBlock object from a database row
575     *
576     * @since 1.42
577     * @param IReadableDatabase $db The database you got the row from
578     * @param stdClass $row Row from the ipblocks table
579     * @return DatabaseBlock
580     */
581    public function newFromRow( IReadableDatabase $db, $row ) {
582        if ( isset( $row->ipb_id ) ) {
583            return new DatabaseBlock( [
584                'address' => $row->ipb_address,
585                'wiki' => $this->wikiId,
586                'timestamp' => $row->ipb_timestamp,
587                'auto' => (bool)$row->ipb_auto,
588                'hideName' => (bool)$row->ipb_deleted,
589                'id' => (int)$row->ipb_id,
590                // Blocks with no parent ID should have ipb_parent_block_id as null,
591                // don't save that as 0 though, see T282890
592                'parentBlockId' => $row->ipb_parent_block_id
593                    ? (int)$row->ipb_parent_block_id : null,
594                'by' => $this->actorStoreFactory
595                    ->getActorStore( $this->wikiId )
596                    ->newActorFromRowFields( $row->ipb_by, $row->ipb_by_text, $row->ipb_by_actor ),
597                'decodedExpiry' => $db->decodeExpiry( $row->ipb_expiry ),
598                'reason' => $this->commentStore
599                    // Legacy because $row may have come from self::selectFields()
600                    ->getCommentLegacy( $db, 'ipb_reason', $row ),
601                'anonOnly' => $row->ipb_anon_only,
602                'enableAutoblock' => (bool)$row->ipb_enable_autoblock,
603                'sitewide' => (bool)$row->ipb_sitewide,
604                'createAccount' => (bool)$row->ipb_create_account,
605                'blockEmail' => (bool)$row->ipb_block_email,
606                'allowUsertalk' => (bool)$row->ipb_allow_usertalk
607            ] );
608        } else {
609            $address = $row->bt_address
610                ?? new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId );
611            return new DatabaseBlock( [
612                'address' => $address,
613                'wiki' => $this->wikiId,
614                'timestamp' => $row->bl_timestamp,
615                'auto' => (bool)$row->bt_auto,
616                'hideName' => (bool)$row->bl_deleted,
617                'id' => (int)$row->bl_id,
618                // Blocks with no parent ID should have ipb_parent_block_id as null,
619                // don't save that as 0 though, see T282890
620                'parentBlockId' => $row->bl_parent_block_id
621                    ? (int)$row->bl_parent_block_id : null,
622                'by' => $this->actorStoreFactory
623                    ->getActorStore( $this->wikiId )
624                    ->newActorFromRowFields( $row->bl_by, $row->bl_by_text, $row->bl_by_actor ),
625                'decodedExpiry' => $db->decodeExpiry( $row->bl_expiry ),
626                'reason' => $this->commentStore->getComment( 'bl_reason', $row ),
627                'anonOnly' => $row->bl_anon_only,
628                'enableAutoblock' => (bool)$row->bl_enable_autoblock,
629                'sitewide' => (bool)$row->bl_sitewide,
630                'createAccount' => (bool)$row->bl_create_account,
631                'blockEmail' => (bool)$row->bl_block_email,
632                'allowUsertalk' => (bool)$row->bl_allow_usertalk
633            ] );
634        }
635    }
636
637    /**
638     * Given a target and the target's type, get an existing block object if possible.
639     *
640     * @since 1.42
641     * @param string|UserIdentity|int|null $specificTarget A block target, which may be one of
642     *   several types:
643     *     * A user to block, in which case $target will be a User
644     *     * An IP to block, in which case $target will be a User generated by using
645     *       User::newFromName( $ip, false ) to turn off name validation
646     *     * An IP range, in which case $target will be a String "123.123.123.123/18" etc
647     *     * The ID of an existing block, in the format "#12345" (since pure numbers are valid
648     *       usernames
649     *     Calling this with a user, IP address or range will not select autoblocks, and will
650     *     only select a block where the targets match exactly (so looking for blocks on
651     *     1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32)
652     * @param string|UserIdentity|int|null $vagueTarget As above, but we will search for *any*
653     *     block which affects that target (so for an IP address, get ranges containing that IP;
654     *     and also get any relevant autoblocks). Leave empty or blank to skip IP-based lookups.
655     * @param bool $fromPrimary Whether to use the DB_PRIMARY database
656     * @return DatabaseBlock|null (null if no relevant block could be found). The target and type
657     *     of the returned block will refer to the actual block which was found, which might
658     *     not be the same as the target you gave if you used $vagueTarget!
659     */
660    public function newFromTarget(
661        $specificTarget,
662        $vagueTarget = null,
663        $fromPrimary = false
664    ) {
665        $blocks = $this->newListFromTarget( $specificTarget, $vagueTarget, $fromPrimary );
666        return $this->chooseMostSpecificBlock( $blocks );
667    }
668
669    /**
670     * This is similar to DatabaseBlockStore::newFromTarget, but it returns all the relevant blocks.
671     *
672     * @since 1.42
673     * @param string|UserIdentity|int|null $specificTarget
674     * @param string|UserIdentity|int|null $vagueTarget
675     * @param bool $fromPrimary
676     * @return DatabaseBlock[] Any relevant blocks
677     */
678    public function newListFromTarget(
679        $specificTarget,
680        $vagueTarget = null,
681        $fromPrimary = false
682    ) {
683        [ $target, $type ] = $this->blockUtils->parseBlockTarget( $specificTarget );
684        if ( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ) {
685            $block = $this->newFromID( $target );
686            return $block ? [ $block ] : [];
687        } elseif ( $target === null && $vagueTarget == '' ) {
688            // We're not going to find anything useful here
689            // Be aware that the == '' check is explicit, since empty values will be
690            // passed by some callers (T31116)
691            return [];
692        } elseif ( in_array(
693            $type,
694            [ Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ] )
695        ) {
696            return $this->newLoad( $target, $type, $fromPrimary, $vagueTarget );
697        }
698        return [];
699    }
700
701    /**
702     * Get all blocks that match any IP from an array of IP addresses
703     *
704     * @since 1.42
705     * @param string[] $addresses Validated list of IP addresses
706     * @param bool $applySoftBlocks Include soft blocks (anonymous-only blocks). These
707     *     should only block anonymous and temporary users.
708     * @param bool $fromPrimary Whether to query the primary or replica DB
709     * @return DatabaseBlock[]
710     */
711    public function newListFromIPs( array $addresses, $applySoftBlocks, $fromPrimary = false ) {
712        if ( $addresses === [] ) {
713            return [];
714        }
715
716        $conds = [];
717        foreach ( array_unique( $addresses ) as $ipaddr ) {
718            $conds[] = $this->getRangeCond( IPUtils::toHex( $ipaddr ), null );
719        }
720
721        if ( $conds === [] ) {
722            return [];
723        }
724
725        if ( $fromPrimary ) {
726            $db = $this->getPrimaryDB();
727        } else {
728            $db = $this->getReplicaDB();
729        }
730        $conds = $db->makeList( $conds, LIST_OR );
731        if ( !$applySoftBlocks ) {
732            $conds = [ $conds, 'bl_anon_only' => 0 ];
733        }
734        $blockQuery = $this->getQueryInfo();
735        $rows = $db->newSelectQueryBuilder()
736            ->queryInfo( $blockQuery )
737            ->fields( [ 'bt_range_start', 'bt_range_end' ] )
738            ->where( $conds )
739            ->caller( __METHOD__ )
740            ->fetchResultSet();
741
742        $blocks = [];
743        foreach ( $rows as $row ) {
744            $block = $this->newFromRow( $db, $row );
745            if ( !$block->isExpired() ) {
746                $blocks[] = $block;
747            }
748        }
749
750        return $blocks;
751    }
752
753    /**
754     * Construct an array of blocks from database conditions.
755     *
756     * @since 1.42
757     * @param array $conds For schema-independence this should be an associative
758     *   array mapping field names to values. Field names from the new schema
759     *   should be used.
760     * @param bool $fromPrimary
761     * @param bool $includeExpired
762     * @return DatabaseBlock[]
763     */
764    public function newListFromConds( $conds, $fromPrimary = false, $includeExpired = false ) {
765        $db = $fromPrimary ? $this->getPrimaryDB() : $this->getReplicaDB();
766        $conds = self::mapActorAlias( $conds );
767        if ( !$includeExpired ) {
768            $conds[] = $db->expr( 'bl_expiry', '>=', $db->timestamp() );
769        }
770        $res = $db->newSelectQueryBuilder()
771            ->queryInfo( $this->getQueryInfo() )
772            ->conds( $conds )
773            ->caller( __METHOD__ )
774            ->fetchResultSet();
775        $blocks = [];
776        foreach ( $res as $row ) {
777            $blocks[] = $this->newFromRow( $db, $row );
778        }
779        return $blocks;
780    }
781
782    // endregion -- end of database read methods
783
784    /***************************************************************************/
785    // region   Database write methods
786    /** @name   Database write methods */
787
788    /**
789     * Delete expired blocks from the ipblocks table
790     *
791     * @internal only public for use in DatabaseBlock
792     */
793    public function purgeExpiredBlocks() {
794        if ( $this->readOnlyMode->isReadOnly( $this->wikiId ) ) {
795            return;
796        }
797
798        $dbw = $this->getPrimaryDB();
799
800        DeferredUpdates::addUpdate( new AutoCommitUpdate(
801            $dbw,
802            __METHOD__,
803            function ( IDatabase $dbw, $fname ) {
804                $limit = $this->options->get( MainConfigNames::UpdateRowsPerQuery );
805                $res = $dbw->newSelectQueryBuilder()
806                    ->select( [ 'bl_id', 'bl_target' ] )
807                    ->from( 'block' )
808                    ->where( $dbw->expr( 'bl_expiry', '<', $dbw->timestamp() ) )
809                    // Set a limit to avoid causing replication lag (T301742)
810                    ->limit( $limit )
811                    ->caller( $fname )->fetchResultSet();
812                $this->deleteBlockRows( $res );
813            }
814        ) );
815    }
816
817    /**
818     * Delete all blocks matching the given conditions.
819     *
820     * @since 1.42
821     * @param array $conds An associative array mapping the field name to the
822     *   matched value. Some limited schema abstractions are implemented, to
823     *   allow new field names to be used with the old schema.
824     * @param int|null $limit The maximum number of blocks to delete
825     * @return int The number of blocks deleted
826     */
827    public function deleteBlocksMatchingConds( array $conds, $limit = null ) {
828        $dbw = $this->getPrimaryDB();
829        $conds = self::mapActorAlias( $conds );
830        $qb = $dbw->newSelectQueryBuilder()
831            ->select( [ 'bl_id', 'bl_target' ] )
832            ->from( 'block' )
833            // Typical input conds need block_target
834            ->join( 'block_target', null, 'bt_id=bl_target' )
835            ->where( $conds )
836            ->caller( __METHOD__ );
837        if ( self::hasActorAlias( $conds ) ) {
838            $qb->join( 'actor', 'ipblocks_actor', 'actor_id=bl_by_actor' );
839        }
840        if ( $limit !== null ) {
841            $qb->limit( $limit );
842        }
843        $res = $qb->fetchResultSet();
844        return $this->deleteBlockRows( $res );
845    }
846
847    /**
848     * Helper for deleteBlocksMatchingConds()
849     *
850     * @param array $conds
851     * @return array
852     */
853    private static function mapActorAlias( $conds ) {
854        return self::mapConds(
855            [
856                'bl_by' => 'ipblocks_actor.actor_user',
857            ],
858            $conds
859        );
860    }
861
862    /**
863     * @param array $conds
864     * @return bool
865     */
866    private static function hasActorAlias( $conds ) {
867        return array_key_exists( 'ipblocks_actor.actor_user', $conds )
868            || array_key_exists( 'ipblocks_actor.actor_name', $conds );
869    }
870
871    /**
872     * Remap the keys in an array
873     *
874     * @param array $map
875     * @param array $conds
876     * @return array
877     */
878    private static function mapConds( $map, $conds ) {
879        $newConds = [];
880        foreach ( $conds as $field => $value ) {
881            if ( isset( $map[$field] ) ) {
882                $newConds[$map[$field]] = $value;
883            } else {
884                $newConds[$field] = $value;
885            }
886        }
887        return $newConds;
888    }
889
890    /**
891     * Delete rows from the block table and update the block_target
892     * and ipblocks_restrictions tables accordingly.
893     *
894     * @param IResultWrapper $rows Rows containing bl_id and bl_target
895     * @return int Number of deleted block rows
896     */
897    private function deleteBlockRows( $rows ) {
898        $ids = [];
899        $deltasByTarget = [];
900        foreach ( $rows as $row ) {
901            $ids[] = (int)$row->bl_id;
902            $target = (int)$row->bl_target;
903            if ( !isset( $deltasByTarget[$target] ) ) {
904                $deltasByTarget[$target] = 0;
905            }
906            $deltasByTarget[$target]++;
907        }
908        if ( !$ids ) {
909            return 0;
910        }
911        $dbw = $this->getPrimaryDB();
912        $dbw->startAtomic( __METHOD__ );
913
914        $maxTargetCount = max( $deltasByTarget );
915        for ( $delta = 1; $delta <= $maxTargetCount; $delta++ ) {
916            $targetsWithThisDelta = array_keys( $deltasByTarget, $delta, true );
917            $this->releaseTargets( $dbw, $targetsWithThisDelta, $delta );
918        }
919
920        $dbw->newDeleteQueryBuilder()
921            ->deleteFrom( 'block' )
922            ->where( [ 'bl_id' => $ids ] )
923            ->caller( __METHOD__ )->execute();
924        $numDeleted = $dbw->affectedRows();
925        $dbw->endAtomic( __METHOD__ );
926        $this->blockRestrictionStore->deleteByBlockId( $ids );
927        return $numDeleted;
928    }
929
930    /**
931     * Decrement the bt_count field of a set of block_target rows and delete
932     * the rows if the count falls to zero.
933     *
934     * @param IDatabase $dbw
935     * @param int[] $targetIds
936     * @param int $delta The amount to decrement by
937     */
938    private function releaseTargets( IDatabase $dbw, $targetIds, int $delta = 1 ) {
939        $dbw->newUpdateQueryBuilder()
940            ->update( 'block_target' )
941            ->set( [ 'bt_count' => new RawSQLValue( "bt_count-$delta" ) ] )
942            ->where( [ 'bt_id' => $targetIds ] )
943            ->caller( __METHOD__ )
944            ->execute();
945        $dbw->newDeleteQueryBuilder()
946            ->deleteFrom( 'block_target' )
947            ->where( [
948                'bt_count<1',
949                'bt_id' => $targetIds
950            ] )
951            ->caller( __METHOD__ )
952            ->execute();
953    }
954
955    private function getReplicaDB(): IReadableDatabase {
956        return $this->dbProvider->getReplicaDatabase( $this->wikiId );
957    }
958
959    private function getPrimaryDB(): IDatabase {
960        return $this->dbProvider->getPrimaryDatabase( $this->wikiId );
961    }
962
963    /**
964     * Insert a block into the block table. Will fail if there is a conflicting
965     * block (same name and options) already in the database.
966     *
967     * @param DatabaseBlock $block
968     * @param int|null $expectedTargetCount The expected number of existing blocks
969     *   on the specified target. If this is zero but there is an existing
970     *   block, the insertion will fail.
971     * @return bool|array False on failure, assoc array on success:
972     *      ('id' => block ID, 'autoIds' => array of autoblock IDs)
973     */
974    public function insertBlock(
975        DatabaseBlock $block,
976        $expectedTargetCount = 0
977    ) {