Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.71% |
636 / 742 |
|
48.78% |
20 / 41 |
CRAP | |
0.00% |
0 / 1 |
DatabaseBlockStore | |
85.71% |
636 / 742 |
|
48.78% |
20 / 41 |
254.26 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
newFromID | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getQueryInfo | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
1 | |||
newLoad | |
84.38% |
54 / 64 |
|
0.00% |
0 / 1 |
26.20 | |||
chooseMostSpecificBlock | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
getConditionForRanges | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
4 | |||
getRangeCond | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getIpFragment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
newFromRow | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
2 | |||
newFromTarget | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
newListFromTarget | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
9.05 | |||
newListFromIPs | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
7.02 | |||
newListFromConds | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
newUnsaved | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
4.01 | |||
purgeExpiredBlocks | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
2 | |||
deleteBlocksMatchingConds | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
3.02 | |||
mapActorAlias | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
hasActorAlias | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
mapConds | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
deleteBlockRows | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
6 | |||
releaseTargets | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
2.00 | |||
getReplicaDB | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPrimaryDB | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
insertBlock | |
81.58% |
31 / 38 |
|
0.00% |
0 / 1 |
12.90 | |||
insertBlockWithParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
attemptInsert | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
5.03 | |||
purgeExpiredConflicts | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getExpiredConflictingBlockRows | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getTargetConds | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
acquireTarget | |
87.18% |
68 / 78 |
|
0.00% |
0 / 1 |
12.30 | |||
updateBlock | |
89.74% |
35 / 39 |
|
0.00% |
0 / 1 |
7.05 | |||
updateTarget | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
deleteBlock | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
3 | |||
getArrayForBlockUpdate | |
96.30% |
26 / 27 |
|
0.00% |
0 / 1 |
2 | |||
getArrayForAutoblockUpdate | |
96.30% |
26 / 27 |
|
0.00% |
0 / 1 |
3 | |||
doRetroactiveAutoblock | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
performRetroactiveAutoblock | |
84.00% |
21 / 25 |
|
0.00% |
0 / 1 |
6.15 | |||
doAutoblock | |
81.82% |
36 / 44 |
|
0.00% |
0 / 1 |
11.73 | |||
getAutoblockReason | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
updateTimestamp | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
getAutoblockExpiry | |
80.00% |
4 / 5 |
|
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 | |
23 | namespace MediaWiki\Block; |
24 | |
25 | use InvalidArgumentException; |
26 | use MediaWiki\CommentStore\CommentStore; |
27 | use MediaWiki\Config\ServiceOptions; |
28 | use MediaWiki\Deferred\AutoCommitUpdate; |
29 | use MediaWiki\Deferred\DeferredUpdates; |
30 | use MediaWiki\HookContainer\HookContainer; |
31 | use MediaWiki\HookContainer\HookRunner; |
32 | use MediaWiki\MainConfigNames; |
33 | use MediaWiki\Session\SessionManager; |
34 | use MediaWiki\User\ActorStoreFactory; |
35 | use MediaWiki\User\TempUser\TempUserConfig; |
36 | use MediaWiki\User\UserFactory; |
37 | use MediaWiki\User\UserIdentity; |
38 | use Psr\Log\LoggerInterface; |
39 | use RuntimeException; |
40 | use stdClass; |
41 | use Wikimedia\IPUtils; |
42 | use Wikimedia\Rdbms\IConnectionProvider; |
43 | use Wikimedia\Rdbms\IDatabase; |
44 | use Wikimedia\Rdbms\IExpression; |
45 | use Wikimedia\Rdbms\IReadableDatabase; |
46 | use Wikimedia\Rdbms\IResultWrapper; |
47 | use Wikimedia\Rdbms\LikeValue; |
48 | use Wikimedia\Rdbms\RawSQLExpression; |
49 | use Wikimedia\Rdbms\RawSQLValue; |
50 | use Wikimedia\Rdbms\ReadOnlyMode; |
51 | use Wikimedia\Rdbms\SelectQueryBuilder; |
52 | use function array_key_exists; |
53 | |
54 | /** |
55 | * @since 1.36 |
56 | * |
57 | * @author DannyS712 |
58 | */ |
59 | class DatabaseBlockStore { |
60 | /** Load all autoblocks */ |
61 | public const AUTO_ALL = 'all'; |
62 | /** Load only autoblocks specified by ID */ |
63 | public const AUTO_SPECIFIED = 'specified'; |
64 | /** Do not load autoblocks */ |
65 | public const AUTO_NONE = 'none'; |
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 | private ServiceOptions $options; |
82 | private LoggerInterface $logger; |
83 | private ActorStoreFactory $actorStoreFactory; |
84 | private BlockRestrictionStore $blockRestrictionStore; |
85 | private CommentStore $commentStore; |
86 | private HookRunner $hookRunner; |
87 | private IConnectionProvider $dbProvider; |
88 | private ReadOnlyMode $readOnlyMode; |
89 | private UserFactory $userFactory; |
90 | private TempUserConfig $tempUserConfig; |
91 | private BlockTargetFactory $blockTargetFactory; |
92 | private AutoblockExemptionList $autoblockExemptionList; |
93 | |
94 | public function __construct( |
95 | ServiceOptions $options, |
96 | LoggerInterface $logger, |
97 | ActorStoreFactory $actorStoreFactory, |
98 | BlockRestrictionStore $blockRestrictionStore, |
99 | CommentStore $commentStore, |
100 | HookContainer $hookContainer, |
101 | IConnectionProvider $dbProvider, |
102 | ReadOnlyMode $readOnlyMode, |
103 | UserFactory $userFactory, |
104 | TempUserConfig $tempUserConfig, |
105 | BlockTargetFactory $blockTargetFactory, |
106 | AutoblockExemptionList $autoblockExemptionList, |
107 | /* string|false */ $wikiId = DatabaseBlock::LOCAL |
108 | ) { |
109 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
110 | |
111 | $this->wikiId = $wikiId; |
112 | |
113 | $this->options = $options; |
114 | $this->logger = $logger; |
115 | $this->actorStoreFactory = $actorStoreFactory; |
116 | $this->blockRestrictionStore = $blockRestrictionStore; |
117 | $this->commentStore = $commentStore; |
118 | $this->hookRunner = new HookRunner( $hookContainer ); |
119 | $this->dbProvider = $dbProvider; |
120 | $this->readOnlyMode = $readOnlyMode; |
121 | $this->userFactory = $userFactory; |
122 | $this->tempUserConfig = $tempUserConfig; |
123 | $this->blockTargetFactory = $blockTargetFactory; |
124 | $this->autoblockExemptionList = $autoblockExemptionList; |
125 | } |
126 | |
127 | /***************************************************************************/ |
128 | // region Database read methods |
129 | /** @name Database read methods */ |
130 | |
131 | /** |
132 | * Load a block from the block ID. |
133 | * |
134 | * @since 1.42 |
135 | * @param int $id ID to search for |
136 | * @param bool $fromPrimary Whether to use the DB_PRIMARY database (since 1.44) |
137 | * @param bool $includeExpired Whether to include expired blocks (since 1.44) |
138 | * @return DatabaseBlock|null |
139 | */ |
140 | public function newFromID( $id, $fromPrimary = false, $includeExpired = false ) { |
141 | $blocks = $this->newListFromConds( [ 'bl_id' => $id ], $fromPrimary, $includeExpired ); |
142 | return $blocks ? $blocks[0] : null; |
143 | } |
144 | |
145 | /** |
146 | * Return the tables, fields, and join conditions to be selected to create |
147 | * a new block object. |
148 | * |
149 | * @since 1.42 |
150 | * @internal Prefer newListFromConds() and deleteBlocksMatchingConds(). |
151 | * |
152 | * @return array[] With three keys: |
153 | * - tables: (string[]) to include in the `$table` to `IDatabase->select()` |
154 | * or `SelectQueryBuilder::tables` |
155 | * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` |
156 | * or `SelectQueryBuilder::fields` |
157 | * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` |
158 | * or `SelectQueryBuilder::joinConds` |
159 | * @phan-return array{tables:string[],fields:string[],joins:array} |
160 | */ |
161 | public function getQueryInfo() { |
162 | $commentQuery = $this->commentStore->getJoin( 'bl_reason' ); |
163 | return [ |
164 | 'tables' => [ |
165 | 'block', |
166 | 'block_target', |
167 | 'block_by_actor' => 'actor', |
168 | ] + $commentQuery['tables'], |
169 | 'fields' => [ |
170 | 'bl_id', |
171 | 'bt_address', |
172 | 'bt_user', |
173 | 'bt_user_text', |
174 | 'bl_timestamp', |
175 | 'bt_auto', |
176 | 'bl_anon_only', |
177 | 'bl_create_account', |
178 | 'bl_enable_autoblock', |
179 | 'bl_expiry', |
180 | 'bl_deleted', |
181 | 'bl_block_email', |
182 | 'bl_allow_usertalk', |
183 | 'bl_parent_block_id', |
184 | 'bl_sitewide', |
185 | 'bl_by_actor', |
186 | 'bl_by' => 'block_by_actor.actor_user', |
187 | 'bl_by_text' => 'block_by_actor.actor_name', |
188 | ] + $commentQuery['fields'], |
189 | 'joins' => [ |
190 | 'block_target' => [ 'JOIN', 'bt_id=bl_target' ], |
191 | 'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ], |
192 | ] + $commentQuery['joins'], |
193 | ]; |
194 | } |
195 | |
196 | /** |
197 | * Load blocks from the database which target the specific target exactly, or which cover the |
198 | * vague target. |
199 | * |
200 | * @param BlockTarget|null $specificTarget |
201 | * @param bool $fromPrimary |
202 | * @param BlockTarget|null $vagueTarget Also search for blocks affecting |
203 | * this target. Doesn't make any sense to use TYPE_AUTO here. Leave blank to |
204 | * skip IP lookups. |
205 | * @param string $auto One of the self::AUTO_* constants |
206 | * @return DatabaseBlock[] Any relevant blocks |
207 | */ |
208 | private function newLoad( |
209 | $specificTarget, |
210 | $fromPrimary, |
211 | $vagueTarget = null, |
212 | $auto = self::AUTO_ALL |
213 | ) { |
214 | if ( $fromPrimary ) { |
215 | $db = $this->getPrimaryDB(); |
216 | } else { |
217 | $db = $this->getReplicaDB(); |
218 | } |
219 | |
220 | $userIds = []; |
221 | $userNames = []; |
222 | $addresses = []; |
223 | $ranges = []; |
224 | if ( $specificTarget instanceof UserBlockTarget ) { |
225 | $userId = $specificTarget->getUserIdentity()->getId( $this->wikiId ); |
226 | if ( $userId ) { |
227 | $userIds[] = $userId; |
228 | } else { |
229 | // A nonexistent user can have no blocks. |
230 | // This case is hit in testing, possibly production too. |
231 | // Ignoring the user is optimal for production performance. |
232 | } |
233 | } elseif ( $specificTarget instanceof AnonIpBlockTarget |
234 | || $specificTarget instanceof RangeBlockTarget |
235 | ) { |
236 | $addresses[] = (string)$specificTarget; |
237 | } |
238 | |
239 | // Be aware that the != '' check is explicit, since empty values will be |
240 | // passed by some callers (T31116) |
241 | if ( $vagueTarget !== null ) { |
242 | if ( $vagueTarget instanceof UserBlockTarget ) { |
243 | // Slightly weird, but who are we to argue? |
244 | $vagueUser = $vagueTarget->getUserIdentity(); |
245 | $userId = $vagueUser->getId( $this->wikiId ); |
246 | if ( $userId ) { |
247 | $userIds[] = $userId; |
248 | } else { |
249 | $userNames[] = $vagueUser->getName(); |
250 | } |
251 | } elseif ( $vagueTarget instanceof BlockTargetWithIp ) { |
252 | $ranges[] = $vagueTarget->toHexRange(); |
253 | } else { |
254 | $this->logger->debug( "Ignoring invalid vague target" ); |
255 | } |
256 | } |
257 | |
258 | $orConds = []; |
259 | if ( $userIds ) { |
260 | // @phan-suppress-next-line PhanTypeMismatchArgument -- array_unique() result is non-empty |
261 | $orConds[] = $db->expr( 'bt_user', '=', array_unique( $userIds ) ); |
262 | } |
263 | if ( $userNames ) { |
264 | // Add bt_ip_hex to the condition since it is in the index |
265 | $orConds[] = $db->expr( 'bt_ip_hex', '=', null ) |
266 | // @phan-suppress-next-line PhanTypeMismatchArgument -- array_unique() result is non-empty |
267 | ->and( 'bt_user_text', '=', array_unique( $userNames ) ); |
268 | } |
269 | if ( $addresses ) { |
270 | // @phan-suppress-next-line PhanTypeMismatchArgument |
271 | $orConds[] = $db->expr( 'bt_address', '=', array_unique( $addresses ) ); |
272 | } |
273 | foreach ( $this->getConditionForRanges( $ranges ) as $cond ) { |
274 | $orConds[] = new RawSQLExpression( $cond ); |
275 | } |
276 | if ( !$orConds ) { |
277 | return []; |
278 | } |
279 | |
280 | // Exclude autoblocks unless AUTO_ALL was requested. |
281 | $autoConds = $auto === self::AUTO_ALL ? [] : [ 'bt_auto' => 0 ]; |
282 | |
283 | $blockQuery = $this->getQueryInfo(); |
284 | $res = $db->newSelectQueryBuilder() |
285 | ->queryInfo( $blockQuery ) |
286 | ->where( $db->orExpr( $orConds ) ) |
287 | ->andWhere( $autoConds ) |
288 | ->caller( __METHOD__ ) |
289 | ->fetchResultSet(); |
290 | |
291 | $blocks = []; |
292 | $blockIds = []; |
293 | $autoBlocks = []; |
294 | foreach ( $res as $row ) { |
295 | $block = $this->newFromRow( $db, $row ); |
296 | |
297 | // Don't use expired blocks |
298 | if ( $block->isExpired() ) { |
299 | continue; |
300 | } |
301 | |
302 | // Don't use anon only blocks on users |
303 | if ( |
304 | $specificTarget instanceof UserBlockTarget && |
305 | !$block->isHardblock() && |
306 | !$this->tempUserConfig->isTempName( $specificTarget->toString() ) |
307 | ) { |
308 | continue; |
309 | } |
310 | |
311 | // Check for duplicate autoblocks |
312 | if ( $block->getType() === Block::TYPE_AUTO ) { |
313 | $autoBlocks[] = $block; |
314 | } else { |
315 | $blocks[] = $block; |
316 | $blockIds[] = $block->getId( $this->wikiId ); |
317 | } |
318 | } |
319 | |
320 | // Only add autoblocks that aren't duplicates |
321 | foreach ( $autoBlocks as $block ) { |
322 | if ( !in_array( $block->getParentBlockId(), $blockIds ) ) { |
323 | $blocks[] = $block; |
324 | } |
325 | } |
326 | |
327 | return $blocks; |
328 | } |
329 | |
330 | /** |
331 | * Choose the most specific block from some combination of user, IP and IP range |
332 | * blocks. Decreasing order of specificity: user > IP > narrower IP range > wider IP |
333 | * range. A range that encompasses one IP address is ranked equally to a singe IP. |
334 | * |
335 | * @param DatabaseBlock[] $blocks These should not include autoblocks or ID blocks |
336 | * @return DatabaseBlock|null The block with the most specific target |
337 | */ |
338 | private function chooseMostSpecificBlock( array $blocks ) { |
339 | if ( count( $blocks ) === 1 ) { |
340 | return $blocks[0]; |
341 | } |
342 | |
343 | // This result could contain a block on the user, a block on the IP, and a russian-doll |
344 | // set of range blocks. We want to choose the most specific one, so keep a leader board. |
345 | $bestBlock = null; |
346 | |
347 | // Lower will be better |
348 | $bestBlockScore = 100; |
349 | foreach ( $blocks as $block ) { |
350 | $score = $block->getTarget()->getSpecificity(); |
351 | if ( $score < $bestBlockScore ) { |
352 | $bestBlockScore = $score; |
353 | $bestBlock = $block; |
354 | } |
355 | } |
356 | |
357 | return $bestBlock; |
358 | } |
359 | |
360 | /** |
361 | * Get a set of SQL conditions which select range blocks encompassing the |
362 | * given ranges. For each range that is really a single IP (start=end), it will |
363 | * also select single IP blocks with that IP. |
364 | * |
365 | * @since 1.44 |
366 | * @param string[][] $ranges List of elements with `[ start, end ]`, where `start` and `end` are hexadecimal |
367 | * IP representation, and `end` can be null to use `end = start`. |
368 | * @phan-param list<array{0:string,1:?string}> $ranges |
369 | * @return string[] List of conditions to be ORed. |
370 | */ |
371 | public function getConditionForRanges( array $ranges ): array { |
372 | $dbr = $this->getReplicaDB(); |
373 | |
374 | $conds = []; |
375 | $individualIPs = []; |
376 | foreach ( $ranges as [ $start, $end ] ) { |
377 | // Per T16634, we want to include relevant active range blocks; for |
378 | // range blocks, we want to include larger ranges which enclose the given |
379 | // range. We know that all blocks must be smaller than $wgBlockCIDRLimit, |
380 | // so we can improve performance by filtering on a LIKE clause |
381 | $chunk = $this->getIpFragment( $start ); |
382 | $end ??= $start; |
383 | |
384 | $expr = $dbr->expr( |
385 | 'bt_range_start', |
386 | IExpression::LIKE, |
387 | new LikeValue( $chunk, $dbr->anyString() ) |
388 | ) |
389 | ->and( 'bt_range_start', '<=', $start ) |
390 | ->and( 'bt_range_end', '>=', $end ); |
391 | if ( $start === $end ) { |
392 | $individualIPs[] = $start; |
393 | } |
394 | $conds[] = $expr->toSql( $dbr ); |
395 | } |
396 | if ( $individualIPs ) { |
397 | // Also select single IP blocks for these targets |
398 | $conds[] = $dbr->expr( 'bt_ip_hex', '=', $individualIPs ) |
399 | ->and( 'bt_range_start', '=', null ) |
400 | ->toSql( $dbr ); |
401 | } |
402 | return $conds; |
403 | } |
404 | |
405 | /** |
406 | * Get a set of SQL conditions which select range blocks encompassing a |
407 | * given range. If the given range is a single IP with start=end, it will |
408 | * also select single IP blocks with that IP. |
409 | * |
410 | * @since 1.42 |
411 | * @param string $start Hexadecimal IP representation |
412 | * @param string|null $end Hexadecimal IP representation, or null to use $start = $end |
413 | * @return string |
414 | */ |
415 | public function getRangeCond( $start, $end ) { |
416 | $dbr = $this->getReplicaDB(); |
417 | $conds = $this->getConditionForRanges( [ [ $start, $end ] ] ); |
418 | return $dbr->makeList( $conds, IDatabase::LIST_OR ); |
419 | } |
420 | |
421 | /** |
422 | * Get the component of an IP address which is certain to be the same between an IP |
423 | * address and a range block containing that IP address. |
424 | * |
425 | * @param string $hex Hexadecimal IP representation |
426 | * @return string |
427 | */ |
428 | private function getIpFragment( $hex ) { |
429 | $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit ); |
430 | if ( str_starts_with( $hex, 'v6-' ) ) { |
431 | return 'v6-' . substr( substr( $hex, 3 ), 0, (int)floor( $blockCIDRLimit['IPv6'] / 4 ) ); |
432 | } else { |
433 | return substr( $hex, 0, (int)floor( $blockCIDRLimit['IPv4'] / 4 ) ); |
434 | } |
435 | } |
436 | |
437 | /** |
438 | * Create a new DatabaseBlock object from a database row |
439 | * |
440 | * @since 1.42 |
441 | * @param IReadableDatabase $db The database you got the row from |
442 | * @param stdClass $row Row from the block table |
443 | * @return DatabaseBlock |
444 | */ |
445 | public function newFromRow( IReadableDatabase $db, $row ) { |
446 | return new DatabaseBlock( [ |
447 | 'target' => $this->blockTargetFactory->newFromRowRaw( $row ), |
448 | 'wiki' => $this->wikiId, |
449 | 'timestamp' => $row->bl_timestamp, |
450 | 'auto' => (bool)$row->bt_auto, |
451 | 'hideName' => (bool)$row->bl_deleted, |
452 | 'id' => (int)$row->bl_id, |
453 | // Blocks with no parent ID should have bl_parent_block_id as null, |
454 | // don't save that as 0 though, see T282890 |
455 | 'parentBlockId' => $row->bl_parent_block_id |
456 | ? (int)$row->bl_parent_block_id : null, |
457 | 'by' => $this->actorStoreFactory |
458 | ->getActorStore( $this->wikiId ) |
459 | ->newActorFromRowFields( $row->bl_by, $row->bl_by_text, $row->bl_by_actor ), |
460 | 'decodedExpiry' => $db->decodeExpiry( $row->bl_expiry ), |
461 | 'reason' => $this->commentStore->getComment( 'bl_reason', $row ), |
462 | 'anonOnly' => $row->bl_anon_only, |
463 | 'enableAutoblock' => (bool)$row->bl_enable_autoblock, |
464 | 'sitewide' => (bool)$row->bl_sitewide, |
465 | 'createAccount' => (bool)$row->bl_create_account, |
466 | 'blockEmail' => (bool)$row->bl_block_email, |
467 | 'allowUsertalk' => (bool)$row->bl_allow_usertalk |
468 | ] ); |
469 | } |
470 | |
471 | /** |
472 | * Given a target and the target's type, get an existing block object if possible. |
473 | * |
474 | * @since 1.42 |
475 | * @param BlockTarget|string|UserIdentity|int|null $specificTarget A block target, which may be one of |
476 | * several types: |
477 | * * A user to block, in which case $target will be a User |
478 | * * An IP to block, in which case $target will be a User generated by using |
479 | * User::newFromName( $ip, false ) to turn off name validation |
480 | * * An IP range, in which case $target will be a String "123.123.123.123/18" etc |
481 | * * The ID of an existing block, in the format "#12345" (since pure numbers are valid |
482 | * usernames |
483 | * Calling this with a user, IP address or range will not select autoblocks, and will |
484 | * only select a block where the targets match exactly (so looking for blocks on |
485 | * 1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32) |
486 | * @param BlockTarget|string|UserIdentity|int|null $vagueTarget As above, but we will search for *any* |
487 | * block which affects that target (so for an IP address, get ranges containing that IP; |
488 | * and also get any relevant autoblocks). Leave empty or blank to skip IP-based lookups. |
489 | * @param bool $fromPrimary Whether to use the DB_PRIMARY database |
490 | * @param string $auto Since 1.44. One of the self::AUTO_* constants: |
491 | * - AUTO_ALL: always load autoblocks |
492 | * - AUTO_SPECIFIED: load only autoblocks specified in the input by ID |
493 | * - AUTO_NONE: do not load autoblocks |
494 | * @return DatabaseBlock|null (null if no relevant block could be found). The target and type |
495 | * of the returned block will refer to the actual block which was found, which might |
496 | * not be the same as the target you gave if you used $vagueTarget! |
497 | */ |
498 | public function newFromTarget( |
499 | $specificTarget, |
500 | $vagueTarget = null, |
501 | $fromPrimary = false, |
502 | $auto = self::AUTO_ALL |
503 | ) { |
504 | $blocks = $this->newListFromTarget( $specificTarget, $vagueTarget, $fromPrimary, $auto ); |
505 | return $this->chooseMostSpecificBlock( $blocks ); |
506 | } |
507 | |
508 | /** |
509 | * This is similar to DatabaseBlockStore::newFromTarget, but it returns all the relevant blocks. |
510 | * |
511 | * @since 1.42 |
512 | * @param BlockTarget|string|UserIdentity|int|null $specificTarget |
513 | * @param BlockTarget|string|UserIdentity|int|null $vagueTarget |
514 | * @param bool $fromPrimary |
515 | * @param string $auto Since 1.44. One of the self::AUTO_* constants: |
516 | * - AUTO_ALL: always load autoblocks |
517 | * - AUTO_SPECIFIED: load only autoblocks specified in the input by ID |
518 | * - AUTO_NONE: do not load autoblocks |
519 | * @return DatabaseBlock[] Any relevant blocks |
520 | */ |
521 | public function newListFromTarget( |
522 | $specificTarget, |
523 | $vagueTarget = null, |
524 | $fromPrimary = false, |
525 | $auto = self::AUTO_ALL |
526 | ) { |
527 | if ( !( $specificTarget instanceof BlockTarget ) ) { |
528 | $specificTarget = $this->blockTargetFactory->newFromLegacyUnion( $specificTarget ); |
529 | } |
530 | if ( $vagueTarget !== null && !( $vagueTarget instanceof BlockTarget ) ) { |
531 | $vagueTarget = $this->blockTargetFactory->newFromLegacyUnion( $vagueTarget ); |
532 | } |
533 | if ( $specificTarget instanceof AutoBlockTarget ) { |
534 | if ( $auto === self::AUTO_NONE ) { |
535 | return []; |
536 | } |
537 | $block = $this->newFromID( $specificTarget->getId() ); |
538 | return $block ? [ $block ] : []; |
539 | } elseif ( $specificTarget === null && $vagueTarget === null ) { |
540 | // We're not going to find anything useful here |
541 | return []; |
542 | } else { |
543 | return $this->newLoad( $specificTarget, $fromPrimary, $vagueTarget, $auto ); |
544 | } |
545 | } |
546 | |
547 | /** |
548 | * Get all blocks that match any IP from an array of IP addresses |
549 | * |
550 | * @since 1.42 |
551 | * @param string[] $addresses Validated list of IP addresses |
552 | * @param bool $applySoftBlocks Include soft blocks (anonymous-only blocks). These |
553 | * should only block anonymous and temporary users. |
554 | * @param bool $fromPrimary Whether to query the primary or replica DB |
555 | * @return DatabaseBlock[] |
556 | */ |
557 | public function newListFromIPs( array $addresses, $applySoftBlocks, $fromPrimary = false ) { |
558 | $addresses = array_unique( $addresses ); |
559 | if ( $addresses === [] ) { |
560 | return []; |
561 | } |
562 | |
563 | $ranges = []; |
564 | foreach ( $addresses as $ipaddr ) { |
565 | $ranges[] = [ IPUtils::toHex( $ipaddr ), null ]; |
566 | } |
567 | $rangeConds = $this->getConditionForRanges( $ranges ); |
568 | |
569 | if ( $fromPrimary ) { |
570 | $db = $this->getPrimaryDB(); |
571 | } else { |
572 | $db = $this->getReplicaDB(); |
573 | } |
574 | $conds = $db->makeList( $rangeConds, LIST_OR ); |
575 | if ( !$applySoftBlocks ) { |
576 | $conds = [ $conds, 'bl_anon_only' => 0 ]; |
577 | } |
578 | $blockQuery = $this->getQueryInfo(); |
579 | $rows = $db->newSelectQueryBuilder() |
580 | ->queryInfo( $blockQuery ) |
581 | ->fields( [ 'bt_range_start', 'bt_range_end' ] ) |
582 | ->where( $conds ) |
583 | ->caller( __METHOD__ ) |
584 | ->fetchResultSet(); |
585 | |
586 | $blocks = []; |
587 | foreach ( $rows as $row ) { |
588 | $block = $this->newFromRow( $db, $row ); |
589 | if ( !$block->isExpired() ) { |
590 | $blocks[] = $block; |
591 | } |
592 | } |
593 | |
594 | return $blocks; |
595 | } |
596 | |
597 | /** |
598 | * Construct an array of blocks from database conditions. |
599 | * |
600 | * @since 1.42 |
601 | * @param array $conds Query conditions, given as an associative array |
602 | * mapping field names to values. |
603 | * @param bool $fromPrimary |
604 | * @param bool $includeExpired |
605 | * @return DatabaseBlock[] |
606 | */ |
607 | public function newListFromConds( $conds, $fromPrimary = false, $includeExpired = false ) { |
608 | $db = $fromPrimary ? $this->getPrimaryDB() : $this->getReplicaDB(); |
609 | $conds = self::mapActorAlias( $conds ); |
610 | if ( !$includeExpired ) { |
611 | $conds[] = $db->expr( 'bl_expiry', '>=', $db->timestamp() ); |
612 | } |
613 | $res = $db->newSelectQueryBuilder() |
614 | ->queryInfo( $this->getQueryInfo() ) |
615 | ->conds( $conds ) |
616 | ->caller( __METHOD__ ) |
617 | ->fetchResultSet(); |
618 | $blocks = []; |
619 | foreach ( $res as $row ) { |
620 | $blocks[] = $this->newFromRow( $db, $row ); |
621 | } |
622 | return $blocks; |
623 | } |
624 | |
625 | // endregion -- end of database read methods |
626 | |
627 | /***************************************************************************/ |
628 | // region Database write methods |
629 | /** @name Database write methods */ |
630 | |
631 | /** |
632 | * Create a DatabaseBlock representing an unsaved block. Pass the returned |
633 | * object to insertBlock(). |
634 | * |
635 | * @since 1.44 |
636 | * |
637 | * @param array $options Options as documented in DatabaseBlock and |
638 | * AbstractBlock, and additionally: |
639 | * - address: (string) A string specifying the block target. This is not |
640 | * the same as the legacy address parameter which allows UserIdentity. |
641 | * - targetUser: (UserIdentity) The UserIdentity to block |
642 | * @return DatabaseBlock |
643 | */ |
644 | public function newUnsaved( array $options ): DatabaseBlock { |
645 | if ( isset( $options['targetUser'] ) ) { |
646 | $options['target'] = $this->blockTargetFactory |
647 | ->newFromUser( $options['targetUser'] ); |
648 | unset( $options['targetUser'] ); |
649 | } |
650 | if ( isset( $options['address'] ) ) { |
651 | $target = $this->blockTargetFactory |
652 | ->newFromString( $options['address'] ); |
653 | if ( !$target ) { |
654 | throw new InvalidArgumentException( 'Invalid target address' ); |
655 | } |
656 | $options['target'] = $target; |
657 | unset( $options['address'] ); |
658 | } |
659 | return new DatabaseBlock( $options ); |
660 | } |
661 | |
662 | /** |
663 | * Delete expired blocks from the block table |
664 | * |
665 | * @internal only public for use in DatabaseBlock |
666 | */ |
667 | public function purgeExpiredBlocks() { |
668 | if ( $this->readOnlyMode->isReadOnly( $this->wikiId ) ) { |
669 | return; |
670 | } |
671 | |
672 | $dbw = $this->getPrimaryDB(); |
673 | |
674 | DeferredUpdates::addUpdate( new AutoCommitUpdate( |
675 | $dbw, |
676 | __METHOD__, |
677 | function ( IDatabase $dbw, $fname ) { |
678 | $limit = $this->options->get( MainConfigNames::UpdateRowsPerQuery ); |
679 | $res = $dbw->newSelectQueryBuilder() |
680 | ->select( [ 'bl_id', 'bl_target' ] ) |
681 | ->from( 'block' ) |
682 | ->where( $dbw->expr( 'bl_expiry', '<', $dbw->timestamp() ) ) |
683 | // Set a limit to avoid causing replication lag (T301742) |
684 | ->limit( $limit ) |
685 | ->caller( $fname )->fetchResultSet(); |
686 | $this->deleteBlockRows( $res ); |
687 | } |
688 | ) ); |
689 | } |
690 | |
691 | /** |
692 | * Delete all blocks matching the given conditions. |
693 | * |
694 | * @since 1.42 |
695 | * @param array $conds An associative array mapping the field name to the |
696 | * matched value. |
697 | * @param int|null $limit The maximum number of blocks to delete |
698 | * @return int The number of blocks deleted |
699 | */ |
700 | public function deleteBlocksMatchingConds( array $conds, $limit = null ) { |
701 | $dbw = $this->getPrimaryDB(); |
702 | $conds = self::mapActorAlias( $conds ); |
703 | $qb = $dbw->newSelectQueryBuilder() |
704 | ->select( [ 'bl_id', 'bl_target' ] ) |
705 | ->from( 'block' ) |
706 | // Typical input conds need block_target |
707 | ->join( 'block_target', null, 'bt_id=bl_target' ) |
708 | ->where( $conds ) |
709 | ->caller( __METHOD__ ); |
710 | if ( self::hasActorAlias( $conds ) ) { |
711 | $qb->join( 'actor', 'ipblocks_actor', 'actor_id=bl_by_actor' ); |
712 | } |
713 | if ( $limit !== null ) { |
714 | $qb->limit( $limit ); |
715 | } |
716 | $res = $qb->fetchResultSet(); |
717 | return $this->deleteBlockRows( $res ); |
718 | } |
719 | |
720 | /** |
721 | * Helper for deleteBlocksMatchingConds() |
722 | * |
723 | * @param array $conds |
724 | * @return array |
725 | */ |
726 | private static function mapActorAlias( $conds ) { |
727 | return self::mapConds( |
728 | [ |
729 | 'bl_by' => 'ipblocks_actor.actor_user', |
730 | ], |
731 | $conds |
732 | ); |
733 | } |
734 | |
735 | /** |
736 | * @param array $conds |
737 | * @return bool |
738 | */ |
739 | private static function hasActorAlias( $conds ) { |
740 | return array_key_exists( 'ipblocks_actor.actor_user', $conds ) |
741 | || array_key_exists( 'ipblocks_actor.actor_name', $conds ); |
742 | } |
743 | |
744 | /** |
745 | * Remap the keys in an array |
746 | * |
747 | * @param array $map |
748 | * @param array $conds |
749 | * @return array |
750 | */ |
751 | private static function mapConds( $map, $conds ) { |
752 | $newConds = []; |
753 | foreach ( $conds as $field => $value ) { |
754 | if ( isset( $map[$field] ) ) { |
755 | $newConds[$map[$field]] = $value; |
756 | } else { |
757 | $newConds[$field] = $value; |
758 | } |
759 | } |
760 | return $newConds; |
761 | } |
762 | |
763 | /** |
764 | * Delete rows from the block table and update the block_target |
765 | * and ipblocks_restrictions tables accordingly. |
766 | * |
767 | * @param IResultWrapper $rows Rows containing bl_id and bl_target |
768 | * @return int Number of deleted block rows |
769 | */ |
770 | private function deleteBlockRows( $rows ) { |
771 | $ids = []; |
772 | $deltasByTarget = []; |
773 | foreach ( $rows as $row ) { |
774 | $ids[] = (int)$row->bl_id; |
775 | $target = (int)$row->bl_target; |
776 | if ( !isset( $deltasByTarget[$target] ) ) { |
777 | $deltasByTarget[$target] = 0; |
778 | } |
779 | $deltasByTarget[$target]++; |
780 | } |
781 | if ( !$ids ) { |
782 | return 0; |
783 | } |
784 | $dbw = $this->getPrimaryDB(); |
785 | $dbw->startAtomic( __METHOD__ ); |
786 | |
787 | $maxTargetCount = max( $deltasByTarget ); |
788 | for ( $delta = 1; $delta <= $maxTargetCount; $delta++ ) { |
789 | $targetsWithThisDelta = array_keys( $deltasByTarget, $delta, true ); |
790 | if ( $targetsWithThisDelta ) { |
791 | $this->releaseTargets( $dbw, $targetsWithThisDelta, $delta ); |
792 | } |
793 | } |
794 | |
795 | $dbw->newDeleteQueryBuilder() |
796 | ->deleteFrom( 'block' ) |
797 | ->where( [ 'bl_id' => $ids ] ) |
798 | ->caller( __METHOD__ )->execute(); |
799 | $numDeleted = $dbw->affectedRows(); |
800 | $dbw->endAtomic( __METHOD__ ); |
801 | $this->blockRestrictionStore->deleteByBlockId( $ids ); |
802 | return $numDeleted; |
803 | } |
804 | |
805 | /** |
806 | * Decrement the bt_count field of a set of block_target rows and delete |
807 | * the rows if the count falls to zero. |
808 | * |
809 | * @param IDatabase $dbw |
810 | * @param int[] $targetIds |
811 | * @param int $delta The amount to decrement by |
812 | */ |
813 | private function releaseTargets( IDatabase $dbw, $targetIds, int $delta = 1 ) { |
814 | if ( !$targetIds ) { |
815 | return; |
816 | } |
817 | $dbw->newUpdateQueryBuilder() |
818 | ->update( 'block_target' ) |
819 | ->set( [ 'bt_count' => new RawSQLValue( "bt_count-$delta" ) ] ) |
820 | ->where( [ 'bt_id' => $targetIds ] ) |
821 | ->caller( __METHOD__ ) |
822 | ->execute(); |
823 | $dbw->newDeleteQueryBuilder() |
824 | ->deleteFrom( 'block_target' ) |
825 | ->where( [ |
826 | 'bt_count<1', |
827 | 'bt_id' => $targetIds |
828 | ] ) |
829 | ->caller( __METHOD__ ) |
830 | ->execute(); |
831 | } |
832 | |
833 | private function getReplicaDB(): IReadableDatabase { |
834 | return $this->dbProvider->getReplicaDatabase( $this->wikiId ); |
835 | } |
836 | |
837 | private function getPrimaryDB(): IDatabase { |
838 | return $this->dbProvider->getPrimaryDatabase( $this->wikiId ); |
839 | } |
840 | |
841 | /** |
842 | * Insert a block into the block table. Will fail if there is a conflicting |
843 | * block (same name and options) already in the database. |
844 | * |
845 | * @param DatabaseBlock $block |
846 | * @param int|null $expectedTargetCount The expected number of existing blocks |
847 | * on the specified target. If this is zero but there is an existing |
848 | * block, the insertion will fail. |
849 | * @return bool|array False on failure, assoc array on success: |
850 | * - id: block ID |
851 | * - autoIds: array of autoblock IDs |
852 | * - finalTargetCount: The updated number of blocks for the specified target. |
853 | */ |
854 | public function insertBlock( |
855 | DatabaseBlock $block, |
856 | $expectedTargetCount = 0 |
857 | ) { |
858 | $block->assertWiki( $this->wikiId ); |
859 | |
860 | $blocker = $block->getBlocker(); |
861 | if ( !$blocker || $blocker->getName() === '' ) { |
862 | throw new InvalidArgumentException( 'Cannot insert a block without a blocker set' ); |
863 | } |
864 | |
865 | if ( $expectedTargetCount instanceof IDatabase ) { |
866 | throw new InvalidArgumentException( |
867 | 'Old method signature: Passing a custom database connection to ' |
868 | . 'DatabaseBlockStore::insertBlock is no longer supported' |
869 | ); |
870 | } |
871 | |
872 | $this->logger->debug( 'Inserting block; timestamp ' . $block->getTimestamp() ); |
873 | |
874 | // Purge expired blocks. This now just queues a deferred update, so it |
875 | // is possible for expired blocks to conflict with inserted blocks below. |
876 | $this->purgeExpiredBlocks(); |
877 | |
878 | $dbw = $this->getPrimaryDB(); |
879 | $dbw->startAtomic( __METHOD__ ); |
880 | $finalTargetCount = $this->attemptInsert( $block, $dbw, $expectedTargetCount ); |
881 | $purgeDone = false; |
882 | |
883 | // Don't collide with expired blocks. |
884 | // Do this after trying to insert to avoid locking. |
885 | if ( !$finalTargetCount ) { |
886 | if ( $this->purgeExpiredConflicts( $block, $dbw ) ) { |
887 | $finalTargetCount = $this->attemptInsert( $block, $dbw, $expectedTargetCount ); |
888 | $purgeDone = true; |
889 | } |
890 | } |
891 | $dbw->endAtomic( __METHOD__ ); |
892 | |
893 | if ( $finalTargetCount > 1 && !$purgeDone ) { |
894 | // Subtract expired blocks from the target count |
895 | $expiredBlockCount = $this->getExpiredConflictingBlockRows( $block, $dbw )->count(); |
896 | if ( $expiredBlockCount >= $finalTargetCount ) { |
897 | $finalTargetCount = 1; |
898 | } else { |
899 | $finalTargetCount -= $expiredBlockCount; |
900 | } |
901 | } |
902 | |
903 | if ( $finalTargetCount ) { |
904 | $autoBlockIds = $this->doRetroactiveAutoblock( $block ); |
905 | |
906 | if ( $this->options->get( MainConfigNames::BlockDisablesLogin ) ) { |
907 | $targetUserIdentity = $block->getTargetUserIdentity(); |
908 | if ( $targetUserIdentity ) { |
909 | $targetUser = $this->userFactory->newFromUserIdentity( $targetUserIdentity ); |
910 | SessionManager::singleton()->invalidateSessionsForUser( $targetUser ); |
911 | } |
912 | } |
913 | |
914 | return [ |
915 | 'id' => $block->getId( $this->wikiId ), |
916 | 'autoIds' => $autoBlockIds, |
917 | 'finalTargetCount' => $finalTargetCount |
918 | ]; |
919 | } |
920 | |
921 | return false; |
922 | } |
923 | |
924 | /** |
925 | * Create a block with an array of parameters and immediately insert it. |
926 | * Throw an exception on failure. This is a convenience method for testing. |
927 | * |
928 | * Duplicate blocks for a given target are allowed by default. |
929 | * |
930 | * @since 1.44 |
931 | * @param array $params Parameters for newUnsaved(), and also: |
932 | * - expectedTargetCount: Use this to override conflict checking |
933 | * @return DatabaseBlock The inserted Block |
934 | */ |
935 | public function insertBlockWithParams( array $params ): DatabaseBlock { |
936 | $block = $this->newUnsaved( $params ); |
937 | $status = $this->insertBlock( $block, $params['expectedTargetCount'] ?? null ); |
938 | if ( !$status ) { |
939 | throw new RuntimeException( 'Failed to insert block' ); |
940 | } |
941 | return $block; |
942 | } |
943 | |
944 | /** |
945 | * Attempt to insert rows into block, block_target and ipblocks_restrictions. |
946 | * If there is a conflict, return false. |
947 | * |
948 | * @param DatabaseBlock $block |
949 | * @param IDatabase $dbw |
950 | * @param int|null $expectedTargetCount |
951 | * @return int|false The updated number of blocks for the target, or false on failure |
952 | */ |
953 | private function attemptInsert( |
954 | DatabaseBlock $block, |
955 | IDatabase $dbw, |
956 | $expectedTargetCount |
957 | ) { |
958 | [ $targetId, $finalCount ] = $this->acquireTarget( $block, $dbw, $expectedTargetCount ); |
959 | if ( !$targetId ) { |
960 | return false; |
961 | } |
962 | $row = $this->getArrayForBlockUpdate( $block, $dbw ); |
963 | $row['bl_target'] = $targetId; |
964 | $dbw->newInsertQueryBuilder() |
965 | ->insertInto( 'block' ) |
966 | ->row( $row ) |
967 | ->caller( __METHOD__ )->execute(); |
968 | if ( !$dbw->affectedRows() ) { |
969 | return false; |
970 | } |
971 | $id = $dbw->insertId(); |
972 | |
973 | if ( !$id ) { |
974 | throw new RuntimeException( 'block insert ID is falsey' ); |
975 | } |
976 | $block->setId( $id ); |
977 | $restrictions = $block->getRawRestrictions(); |
978 | if ( $restrictions ) { |
979 | $this->blockRestrictionStore->insert( $restrictions ); |
980 | } |
981 | |
982 | return $finalCount; |
983 | } |
984 | |
985 | /** |
986 | * Purge expired blocks that have the same target as the specified block |
987 | * |
988 | * @param DatabaseBlock $block |
989 | * @param IDatabase $dbw |
990 | * @return bool True if a conflicting block was deleted |
991 | */ |
992 | private function purgeExpiredConflicts( |
993 | DatabaseBlock $block, |
994 | IDatabase $dbw |
995 | ) { |
996 | return (bool)$this->deleteBlockRows( |
997 | $this->getExpiredConflictingBlockRows( $block, $dbw ) |
998 | ); |
999 | } |
1000 | |
1001 | /** |
1002 | * Get rows with bl_id/bl_target for expired blocks that have the same |
1003 | * target as the specified block. |
1004 | * |
1005 | * @param DatabaseBlock $block |
1006 | * @param IDatabase $dbw |
1007 | * @return IResultWrapper |
1008 | */ |
1009 | private function getExpiredConflictingBlockRows( |
1010 | DatabaseBlock $block, |
1011 | IDatabase $dbw |
1012 | ) { |
1013 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
1014 | $targetConds = $this->getTargetConds( $block->getTarget() ); |
1015 | return $dbw->newSelectQueryBuilder() |
1016 | ->select( [ 'bl_id', 'bl_target' ] ) |
1017 | ->from( 'block' ) |
1018 | ->join( 'block_target', null, [ 'bt_id=bl_target' ] ) |
1019 | ->where( $targetConds ) |
1020 | ->andWhere( $dbw->expr( 'bl_expiry', '<', $dbw->timestamp() ) ) |
1021 | ->caller( __METHOD__ )->fetchResultSet(); |
1022 | } |
1023 | |
1024 | /** |
1025 | * Get conditions matching an existing block's block_target row |
1026 | * |
1027 | * @param BlockTarget $target |
1028 | * @return array |
1029 | */ |
1030 | private function getTargetConds( BlockTarget $target ) { |
1031 | if ( $target instanceof UserBlockTarget ) { |
1032 | return [ |
1033 | 'bt_user' => $target->getUserIdentity()->getId( $this->wikiId ) |
1034 | ]; |
1035 | } elseif ( $target instanceof AnonIpBlockTarget || $target instanceof RangeBlockTarget ) { |
1036 | return [ 'bt_address' => $target->toString() ]; |
1037 | } else { |
1038 | throw new \InvalidArgumentException( 'Invalid target type' ); |
1039 | } |
1040 | } |
1041 | |
1042 | /** |
1043 | * Insert a new block_target row, or update bt_count in the existing target |
1044 | * row for a given block, and return the target ID and new bt_count. |
1045 | * |
1046 | * An atomic section should be active while calling this function. |
1047 | * |
1048 | * @param DatabaseBlock $block |
1049 | * @param IDatabase $dbw |
1050 | * @param int|null $expectedTargetCount If this is zero and a row already |
1051 | * exists, abort the insert and return null. If this is greater than zero |
1052 | * and the pre-increment bt_count value does not match, abort the update |
1053 | * and return null. If this is null, do not perform any conflict checks. |
1054 | * @return array{?int,int} |
1055 | */ |
1056 | private function acquireTarget( |
1057 | DatabaseBlock $block, |
1058 | IDatabase $dbw, |
1059 | $expectedTargetCount |
1060 | ) { |
1061 | $target = $block->getTarget(); |
1062 | // Note: for new autoblocks, the target is an IpBlockTarget |
1063 | $isAuto = $block->getType() === Block::TYPE_AUTO; |
1064 | if ( $target instanceof UserBlockTarget ) { |
1065 | $targetAddress = null; |
1066 | $targetUserName = (string)$target; |
1067 | $targetUserId = $target->getUserIdentity()->getId( $this->wikiId ); |
1068 | $targetConds = [ 'bt_user' => $targetUserId ]; |
1069 | $targetLockKey = $dbw->getDomainID() . ':block:u:' . $targetUserId; |
1070 | } else { |
1071 | $targetAddress = (string)$target; |
1072 | $targetUserName = null; |
1073 | $targetUserId = null; |
1074 | $targetConds = [ |
1075 | 'bt_address' => $targetAddress, |
1076 | 'bt_auto' => $isAuto, |
1077 | ]; |
1078 | $targetLockKey = $dbw->getDomainID() . ':block:' . |
1079 | ( $isAuto ? 'a' : 'i' ) . ':' . $targetAddress; |
1080 | } |
1081 | |
1082 | $condsWithCount = $targetConds; |
1083 | if ( $expectedTargetCount !== null ) { |
1084 | $condsWithCount['bt_count'] = $expectedTargetCount; |
1085 | } |
1086 | |
1087 | $dbw->lock( $targetLockKey, __METHOD__ ); |
1088 | $func = __METHOD__; |
1089 | $dbw->onTransactionCommitOrIdle( |
1090 | static function () use ( $dbw, $targetLockKey, $func ) { |
1091 | $dbw->unlock( $targetLockKey, "$func.closure" ); |
1092 | }, |
1093 | __METHOD__ |
1094 | ); |
1095 | |
1096 | // This query locks the index gap when the target doesn't exist yet, |
1097 | // so there is a risk of throttling adjacent block insertions, |
1098 | // especially on small wikis which have larger gaps. If this proves to |
1099 | // be a problem, we could have getPrimaryDB() return an autocommit |
1100 | // connection. |
1101 | $dbw->newUpdateQueryBuilder() |
1102 | ->update( 'block_target' ) |
1103 | ->set( [ 'bt_count' => new RawSQLValue( 'bt_count+1' ) ] ) |
1104 | ->where( $condsWithCount ) |
1105 | ->caller( __METHOD__ )->execute(); |
1106 | $numUpdatedRows = $dbw->affectedRows(); |
1107 | |
1108 | // Now that the row is locked, find the target ID |
1109 | $res = $dbw->newSelectQueryBuilder() |
1110 | ->select( [ 'bt_id', 'bt_count' ] ) |
1111 | ->from( 'block_target' ) |
1112 | ->where( $targetConds ) |
1113 | ->forUpdate() |
1114 | ->caller( __METHOD__ ) |
1115 | ->fetchResultSet(); |
1116 | if ( $res->numRows() > 1 ) { |
1117 | $ids = []; |
1118 | foreach ( $res as $row ) { |
1119 | $ids[] = $row->bt_id; |
1120 | } |
1121 | throw new RuntimeException( "Duplicate block_target rows detected: " . |
1122 | implode( ',', $ids ) ); |
1123 | } |
1124 | $row = $res->fetchObject(); |
1125 | |
1126 | if ( $row ) { |
1127 | $count = (int)$row->bt_count; |
1128 | if ( !$numUpdatedRows ) { |
1129 | // ID found but count update failed -- must be a conflict due to bt_count mismatch |
1130 | return [ null, $count ]; |
1131 | } |
1132 | $id = (int)$row->bt_id; |
1133 | } else { |
1134 | if ( $numUpdatedRows ) { |
1135 | throw new RuntimeException( |
1136 | 'block_target row unexpectedly missing after we locked it' ); |
1137 | } |
1138 | if ( $expectedTargetCount !== 0 && $expectedTargetCount !== null ) { |
1139 | // Conflict (expectation failure) |
1140 | return [ null, 0 ]; |
1141 | } |
1142 | |
1143 | // Insert new row |
1144 | $targetRow = [ |
1145 | 'bt_address' => $targetAddress, |
1146 | 'bt_user' => $targetUserId, |
1147 | 'bt_user_text' => $targetUserName, |
1148 | 'bt_auto' => $isAuto, |
1149 | 'bt_range_start' => $block->getRangeStart(), |
1150 | 'bt_range_end' => $block->getRangeEnd(), |
1151 | 'bt_ip_hex' => $block->getIpHex(), |
1152 | 'bt_count' => 1 |
1153 | ]; |
1154 | $dbw->newInsertQueryBuilder() |
1155 | ->insertInto( 'block_target' ) |
1156 | ->row( $targetRow ) |
1157 | ->caller( __METHOD__ )->execute(); |
1158 | $id = $dbw->insertId(); |
1159 | if ( !$id ) { |
1160 | throw new RuntimeException( |
1161 | 'block_target insert ID is falsey despite unconditional insert' ); |
1162 | } |
1163 | $count = 1; |
1164 | } |
1165 | |
1166 | return [ $id, $count ]; |
1167 | } |
1168 | |
1169 | /** |
1170 | * Update a block in the DB with new parameters. |
1171 | * The ID field needs to be loaded first. The target must stay the same. |
1172 | * |
1173 | * TODO: remove the possibility of false return. The cases where this |
1174 | * happens are exotic enough that they should just be exceptions. |
1175 | * |
1176 | * @param DatabaseBlock $block |
1177 | * @return bool|array False on failure, array on success: |
1178 | * ('id' => block ID, 'autoIds' => array of autoblock IDs) |
1179 | */ |
1180 | public function updateBlock( DatabaseBlock $block ) { |
1181 | $this->logger->debug( 'Updating block; timestamp ' . $block->getTimestamp() ); |
1182 | |
1183 | $block->assertWiki( $this->wikiId ); |
1184 | |
1185 | $blockId = $block->getId( $this->wikiId ); |
1186 | if ( !$blockId ) { |
1187 | throw new InvalidArgumentException( |
1188 | __METHOD__ . ' requires that a block id be set' |
1189 | ); |
1190 | } |
1191 | |
1192 | // Update bl_timestamp to current when making any updates to a block (T389275) |
1193 | $block->setTimestamp( wfTimestamp() ); |
1194 | |
1195 | $dbw = $this->getPrimaryDB(); |
1196 | |
1197 | $dbw->startAtomic( __METHOD__ ); |
1198 | |
1199 | $row = $this->getArrayForBlockUpdate( $block, $dbw ); |
1200 | $dbw->newUpdateQueryBuilder() |
1201 | ->update( 'block' ) |
1202 | ->set( $row ) |
1203 | ->where( [ 'bl_id' => $blockId ] ) |
1204 | ->caller( __METHOD__ )->execute(); |
1205 | |
1206 | // Only update the restrictions if they have been modified. |
1207 | $result = true; |
1208 | $restrictions = $block->getRawRestrictions(); |
1209 | if ( $restrictions !== null ) { |
1210 | // An empty array should remove all of the restrictions. |
1211 | if ( $restrictions === [] ) { |
1212 | $result = $this->blockRestrictionStore->deleteByBlockId( $blockId ); |
1213 | } else { |
1214 | $result = $this->blockRestrictionStore->update( $restrictions ); |
1215 | } |
1216 | } |
1217 | |
1218 | if ( $block->isAutoblocking() ) { |
1219 | // Update corresponding autoblock(s) (T50813) |
1220 | $dbw->newUpdateQueryBuilder() |
1221 | ->update( 'block' ) |
1222 | ->set( $this->getArrayForAutoblockUpdate( $block ) ) |
1223 | ->where( [ 'bl_parent_block_id' => $blockId ] ) |
1224 | ->caller( __METHOD__ )->execute(); |
1225 | |
1226 | // Only update the restrictions if they have been modified. |
1227 | if ( $restrictions !== null ) { |
1228 | $this->blockRestrictionStore->updateByParentBlockId( |
1229 | $blockId, |
1230 | $restrictions |
1231 | ); |
1232 | } |
1233 | } else { |
1234 | // Autoblock no longer required, delete corresponding autoblock(s) |
1235 | $this->deleteBlocksMatchingConds( [ 'bl_parent_block_id' => $blockId ] ); |
1236 | } |
1237 | |
1238 | $dbw->endAtomic( __METHOD__ ); |
1239 | |
1240 | if ( $result ) { |
1241 | $autoBlockIds = $this->doRetroactiveAutoblock( $block ); |
1242 | return [ 'id' => $blockId, 'autoIds' => $autoBlockIds ]; |
1243 | } |
1244 | |
1245 | return false; |
1246 | } |
1247 | |
1248 | /** |
1249 | * Update the target in the specified object and in the database. The block |
1250 | * ID must be set. |
1251 | * |
1252 | * This is an unusual operation, currently used only by the UserMerge |
1253 | * extension. |
1254 | * |
1255 | * @since 1.42 |
1256 | * @param DatabaseBlock $block |
1257 | * @param BlockTarget|UserIdentity|string $newTarget |
1258 | * @return bool True if the update was successful, false if there was no |
1259 | * match for the block ID. |
1260 | */ |
1261 | public function updateTarget( DatabaseBlock $block, $newTarget ) { |
1262 | $dbw = $this->getPrimaryDB(); |
1263 | $blockId = $block->getId( $this->wikiId ); |
1264 | if ( !$blockId ) { |
1265 | throw new InvalidArgumentException( |
1266 | __METHOD__ . " requires that a block id be set\n" |
1267 | ); |
1268 | } |
1269 | if ( !( $newTarget instanceof BlockTarget ) ) { |
1270 | $newTarget = $this->blockTargetFactory->newFromLegacyUnion( $newTarget ); |
1271 | } |
1272 | |
1273 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
1274 | $oldTargetConds = $this->getTargetConds( $block->getTarget() ); |
1275 | $block->setTarget( $newTarget ); |
1276 | |
1277 | $dbw->startAtomic( __METHOD__ ); |
1278 | [ $targetId, $count ] = $this->acquireTarget( $block, $dbw, null ); |
1279 | if ( !$targetId ) { |
1280 | // This is an exotic and unlikely error -- perhaps an exception should be thrown |
1281 | $dbw->endAtomic( __METHOD__ ); |
1282 | return false; |
1283 | } |
1284 | $oldTargetId = $dbw->newSelectQueryBuilder() |
1285 | ->select( 'bt_id' ) |
1286 | ->from( 'block_target' ) |
1287 | ->where( $oldTargetConds ) |
1288 | ->caller( __METHOD__ )->fetchField(); |
1289 | $this->releaseTargets( $dbw, [ $oldTargetId ] ); |
1290 | |
1291 | $dbw->newUpdateQueryBuilder() |
1292 | ->update( 'block' ) |
1293 | ->set( [ 'bl_target' => $targetId ] ) |
1294 | ->where( [ 'bl_id' => $blockId ] ) |
1295 | ->caller( __METHOD__ ) |
1296 | ->execute(); |
1297 | $affected = $dbw->affectedRows(); |
1298 | $dbw->endAtomic( __METHOD__ ); |
1299 | return (bool)$affected; |
1300 | } |
1301 | |
1302 | /** |
1303 | * Delete a DatabaseBlock from the database |
1304 | * |
1305 | * @param DatabaseBlock $block |
1306 | * @return bool whether it was deleted |
1307 | */ |
1308 | public function deleteBlock( DatabaseBlock $block ): bool { |
1309 | if ( $this->readOnlyMode->isReadOnly( $this->wikiId ) ) { |
1310 | return false; |
1311 | } |
1312 | |
1313 | $block->assertWiki( $this->wikiId ); |
1314 | |
1315 | $blockId = $block->getId( $this->wikiId ); |
1316 | |
1317 | if ( !$blockId ) { |
1318 | throw new InvalidArgumentException( |
1319 | __METHOD__ . ' requires that a block id be set' |
1320 | ); |
1321 | } |
1322 | $dbw = $this->getPrimaryDB(); |
1323 | $dbw->startAtomic( __METHOD__ ); |
1324 | $res = $dbw->newSelectQueryBuilder() |
1325 | ->select( [ 'bl_id', 'bl_target' ] ) |
1326 | ->from( 'block' ) |
1327 | ->where( |
1328 | $dbw->orExpr( [ |
1329 | 'bl_parent_block_id' => $blockId, |
1330 | 'bl_id' => $blockId, |
1331 | ] ) |
1332 | ) |
1333 | ->caller( __METHOD__ )->fetchResultSet(); |
1334 | $this->deleteBlockRows( $res ); |
1335 | $affected = $res->numRows(); |
1336 | $dbw->endAtomic( __METHOD__ ); |
1337 | |
1338 | return $affected > 0; |
1339 | } |
1340 | |
1341 | /** |
1342 | * Get an array suitable for passing to $dbw->insert() or $dbw->update() |
1343 | * |
1344 | * @param DatabaseBlock $block |
1345 | * @param IDatabase $dbw Database to use if not the same as the one in the load balancer. |
1346 | * Must connect to the wiki identified by $block->getBlocker->getWikiId(). |
1347 | * @return array |
1348 | */ |
1349 | private function getArrayForBlockUpdate( |
1350 | DatabaseBlock $block, |
1351 | IDatabase $dbw |
1352 | ): array { |
1353 | $expiry = $dbw->encodeExpiry( $block->getExpiry() ); |
1354 | |
1355 | $blocker = $block->getBlocker(); |
1356 | if ( !$blocker ) { |
1357 | throw new RuntimeException( __METHOD__ . ': this block does not have a blocker' ); |
1358 | } |
1359 | // DatabaseBlockStore supports inserting cross-wiki blocks by passing |
1360 | // non-local IDatabase and blocker. |
1361 | $blockerActor = $this->actorStoreFactory |
1362 | ->getActorStore( $dbw->getDomainID() ) |
1363 | ->acquireActorId( $blocker, $dbw ); |
1364 | |
1365 | $blockArray = [ |
1366 | 'bl_by_actor' => $blockerActor, |
1367 | 'bl_timestamp' => $dbw->timestamp( $block->getTimestamp() ), |
1368 | 'bl_anon_only' => !$block->isHardblock(), |
1369 | 'bl_create_account' => $block->isCreateAccountBlocked(), |
1370 | 'bl_enable_autoblock' => $block->isAutoblocking(), |
1371 | 'bl_expiry' => $expiry, |
1372 | 'bl_deleted' => intval( $block->getHideName() ), // typecast required for SQLite |
1373 | 'bl_block_email' => $block->isEmailBlocked(), |
1374 | 'bl_allow_usertalk' => $block->isUsertalkEditAllowed(), |
1375 | 'bl_parent_block_id' => $block->getParentBlockId(), |
1376 | 'bl_sitewide' => $block->isSitewide(), |
1377 | ]; |
1378 | $commentArray = $this->commentStore->insert( |
1379 | $dbw, |
1380 | 'bl_reason', |
1381 | $block->getReasonComment() |
1382 | ); |
1383 | |
1384 | $combinedArray = $blockArray + $commentArray; |
1385 | return $combinedArray; |
1386 | } |
1387 | |
1388 | /** |
1389 | * Get an array suitable for autoblock updates |
1390 | * |
1391 | * @param DatabaseBlock $block |
1392 | * @return array |
1393 | */ |
1394 | private function getArrayForAutoblockUpdate( DatabaseBlock $block ): array { |
1395 | $blocker = $block->getBlocker(); |
1396 | if ( !$blocker ) { |
1397 | throw new RuntimeException( __METHOD__ . ': this block does not have a blocker' ); |
1398 | } |
1399 | $dbw = $this->getPrimaryDB(); |
1400 | $blockerActor = $this->actorStoreFactory |
1401 | ->getActorNormalization( $this->wikiId ) |
1402 | ->acquireActorId( $blocker, $dbw ); |
1403 | |
1404 | $blockArray = [ |
1405 | 'bl_by_actor' => $blockerActor, |
1406 | 'bl_create_account' => $block->isCreateAccountBlocked(), |
1407 | 'bl_deleted' => (int)$block->getHideName(), // typecast required for SQLite |
1408 | 'bl_allow_usertalk' => $block->isUsertalkEditAllowed(), |
1409 | 'bl_sitewide' => $block->isSitewide(), |
1410 | ]; |
1411 | |
1412 | // Shorten the autoblock expiry if the parent block expiry is sooner. |
1413 | // Don't lengthen -- that is only done when the IP address is actually |
1414 | // used by the blocked user. |
1415 | if ( $block->getExpiry() !== 'infinity' ) { |
1416 | $blockArray['bl_expiry'] = new RawSQLValue( $dbw->conditional( |
1417 | $dbw->expr( 'bl_expiry', '>', $dbw->timestamp( $block->getExpiry() ) ), |
1418 | $dbw->addQuotes( $dbw->timestamp( $block->getExpiry() ) ), |
1419 | 'bl_expiry' |
1420 | ) ); |
1421 | } |
1422 | |
1423 | $commentArray = $this->commentStore->insert( |
1424 | $dbw, |
1425 | 'bl_reason', |
1426 | $this->getAutoblockReason( $block ) |
1427 | ); |
1428 | |
1429 | $combinedArray = $blockArray + $commentArray; |
1430 | return $combinedArray; |
1431 | } |
1432 | |
1433 | /** |
1434 | * Handle retroactively autoblocking the last IP used by the user (if it is a user) |
1435 | * blocked by an auto block. |
1436 | * |
1437 | * @param DatabaseBlock $block |
1438 | * @return array IDs of retroactive autoblocks made |
1439 | */ |
1440 | private function doRetroactiveAutoblock( DatabaseBlock $block ): array { |
1441 | $autoBlockIds = []; |
1442 | // If autoblock is enabled, autoblock the LAST IP(s) used |
1443 | if ( $block->isAutoblocking() && $block->getType() == AbstractBlock::TYPE_USER ) { |
1444 | $this->logger->debug( |
1445 | 'Doing retroactive autoblocks for ' . $block->getTargetName() |
1446 | ); |
1447 | |
1448 | $hookAutoBlocked = []; |
1449 | $continue = $this->hookRunner->onPerformRetroactiveAutoblock( |
1450 | $block, |
1451 | $hookAutoBlocked |
1452 | ); |
1453 | |
1454 | if ( $continue ) { |
1455 | $coreAutoBlocked = $this->performRetroactiveAutoblock( $block ); |
1456 | $autoBlockIds = array_merge( $hookAutoBlocked, $coreAutoBlocked ); |
1457 | } else { |
1458 | $autoBlockIds = $hookAutoBlocked; |
1459 | } |
1460 | } |
1461 | return $autoBlockIds; |
1462 | } |
1463 | |
1464 | /** |
1465 | * Actually retroactively autoblocks the last IP used by the user (if it is a user) |
1466 | * blocked by this block. This will use the recentchanges table. |
1467 | * |
1468 | * @param DatabaseBlock $block |
1469 | * @return array |
1470 | */ |
1471 | private function performRetroactiveAutoblock( DatabaseBlock $block ): array { |
1472 | if ( !$this->options->get( MainConfigNames::PutIPinRC ) ) { |
1473 | // No IPs in the recent changes table to autoblock |
1474 | return []; |
1475 | } |
1476 | |
1477 | $target = $block->getTarget(); |
1478 | if ( !( $target instanceof UserBlockTarget ) ) { |
1479 | // Autoblocks only apply to users |
1480 | return []; |
1481 | } |
1482 | |
1483 | $dbr = $this->getReplicaDB(); |
1484 | |
1485 | $actor = $this->actorStoreFactory |
1486 | ->getActorNormalization( $this->wikiId ) |
1487 | ->findActorId( $target->getUserIdentity(), $dbr ); |
1488 | |
1489 | if ( !$actor ) { |
1490 | $this->logger->debug( 'No actor found to retroactively autoblock' ); |
1491 | return []; |
1492 | } |
1493 | |
1494 | $rcIp = $dbr->newSelectQueryBuilder() |
1495 | ->select( 'rc_ip' ) |
1496 | ->from( 'recentchanges' ) |
1497 | ->where( [ 'rc_actor' => $actor ] ) |
1498 | ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC ) |
1499 | ->caller( __METHOD__ )->fetchField(); |
1500 | |
1501 | if ( !$rcIp ) { |
1502 | $this->logger->debug( 'No IP found to retroactively autoblock' ); |
1503 | return []; |
1504 | } |
1505 | |
1506 | $id = $this->doAutoblock( $block, $rcIp ); |
1507 | if ( !$id ) { |
1508 | return []; |
1509 | } |
1510 | return [ $id ]; |
1511 | } |
1512 | |
1513 | /** |
1514 | * Autoblocks the given IP, referring to the specified block. |
1515 | * |
1516 | * @since 1.42 |
1517 | * @param DatabaseBlock $parentBlock |
1518 | * @param string $autoblockIP The IP to autoblock. |
1519 | * @return int|false ID if an autoblock was inserted, false if not. |
1520 | */ |
1521 | public function doAutoblock( DatabaseBlock $parentBlock, $autoblockIP ) { |
1522 | // If autoblocks are disabled, go away. |
1523 | if ( !$parentBlock->isAutoblocking() ) { |
1524 | return false; |
1525 | } |
1526 | $parentBlock->assertWiki( $this->wikiId ); |
1527 | |
1528 | $target = $this->blockTargetFactory->newFromIp( $autoblockIP ); |
1529 | if ( !$target ) { |
1530 | $this->logger->debug( "Invalid autoblock IP" ); |
1531 | return false; |
1532 | } |
1533 | |
1534 | // Check if autoblock exempt. |
1535 | if ( $this->autoblockExemptionList->isExempt( $autoblockIP ) ) { |
1536 | return false; |
1537 | } |
1538 | |
1539 | // Allow hooks to cancel the autoblock. |
1540 | if ( !$this->hookRunner->onAbortAutoblock( $autoblockIP, $parentBlock ) ) { |
1541 | $this->logger->debug( "Autoblock aborted by hook." ); |
1542 | return false; |
1543 | } |
1544 | |
1545 | // It's okay to autoblock. Go ahead and insert/update the block... |
1546 | |
1547 | // Do not add a *new* block if the IP is already blocked. |
1548 | $blocks = $this->newLoad( $target, false ); |
1549 | if ( $blocks ) { |
1550 | foreach ( $blocks as $ipblock ) { |
1551 | // Check if the block is an autoblock and would exceed the user block |
1552 | // if renewed. If so, do nothing, otherwise prolong the block time... |
1553 | if ( $ipblock->getType() === Block::TYPE_AUTO |
1554 | && $parentBlock->getExpiry() > $ipblock->getExpiry() |
1555 | ) { |
1556 | // Reset block timestamp to now and its expiry to |
1557 | // $wgAutoblockExpiry in the future |
1558 | $this->updateTimestamp( $ipblock ); |
1559 | } |
1560 | } |
1561 | return false; |
1562 | } |
1563 | $blocker = $parentBlock->getBlocker(); |
1564 | if ( !$blocker ) { |
1565 | throw new RuntimeException( __METHOD__ . ': this block does not have a blocker' ); |
1566 | } |
1567 | |
1568 | $timestamp = wfTimestampNow(); |
1569 | $expiry = $this->getAutoblockExpiry( $timestamp, $parentBlock->getExpiry() ); |
1570 | $autoblock = new DatabaseBlock( [ |
1571 | 'wiki' => $this->wikiId, |
1572 | 'target' => $target, |
1573 | 'by' => $blocker, |
1574 | 'reason' => $this->getAutoblockReason( $parentBlock ), |
1575 | 'decodedTimestamp' => $timestamp, |
1576 | 'auto' => true, |
1577 | 'createAccount' => $parentBlock->isCreateAccountBlocked(), |
1578 | // Continue suppressing the name if needed |
1579 | 'hideName' => $parentBlock->getHideName(), |
1580 | 'allowUsertalk' => $parentBlock->isUsertalkEditAllowed(), |
1581 | 'parentBlockId' => $parentBlock->getId( $this->wikiId ), |
1582 | 'sitewide' => $parentBlock->isSitewide(), |
1583 | 'restrictions' => $parentBlock->getRestrictions(), |
1584 | 'decodedExpiry' => $expiry, |
1585 | ] ); |
1586 | |
1587 | $this->logger->debug( "Autoblocking {$parentBlock->getTargetName()}@" . $target ); |
1588 | |
1589 | $status = $this->insertBlock( $autoblock ); |
1590 | return $status |
1591 | ? $status['id'] |
1592 | : false; |
1593 | } |
1594 | |
1595 | private function getAutoblockReason( DatabaseBlock $parentBlock ): string { |
1596 | return wfMessage( |
1597 | 'autoblocker', |
1598 | $parentBlock->getTargetName(), |
1599 | $parentBlock->getReasonComment()->text |
1600 | )->inContentLanguage()->plain(); |
1601 | } |
1602 | |
1603 | /** |
1604 | * Update the timestamp on autoblocks. |
1605 | * |
1606 | * @internal Public to support deprecated DatabaseBlock::updateTimestamp() |
1607 | * @param DatabaseBlock $block |
1608 | */ |
1609 | public function updateTimestamp( DatabaseBlock $block ) { |
1610 | $block->assertWiki( $this->wikiId ); |
1611 | if ( $block->getType() !== Block::TYPE_AUTO ) { |
1612 | return; |
1613 | } |
1614 | $now = wfTimestamp(); |
1615 | $block->setTimestamp( $now ); |
1616 | // No need to reduce the autoblock expiry to the expiry of the parent |
1617 | // block, since the caller already checked for that. |
1618 | $block->setExpiry( $this->getAutoblockExpiry( $now ) ); |
1619 | |
1620 | $dbw = $this->getPrimaryDB(); |
1621 | $dbw->newUpdateQueryBuilder() |
1622 | ->update( 'block' ) |
1623 | ->set( |
1624 | [ |
1625 | 'bl_timestamp' => $dbw->timestamp( $block->getTimestamp() ), |
1626 | 'bl_expiry' => $dbw->timestamp( $block->getExpiry() ), |
1627 | ] |
1628 | ) |
1629 | ->where( [ 'bl_id' => $block->getId( $this->wikiId ) ] ) |
1630 | ->caller( __METHOD__ )->execute(); |
1631 | } |
1632 | |
1633 | /** |
1634 | * Get the expiry timestamp for an autoblock created at the given time. |
1635 | * |
1636 | * If the parent block expiry is specified, the return value will be earlier |
1637 | * than or equal to the parent block expiry. |
1638 | * |
1639 | * @internal Public to support deprecated DatabaseBlock method |
1640 | * @param string|int $timestamp |
1641 | * @param string|null $parentExpiry |
1642 | * @return string |
1643 | */ |
1644 | public function getAutoblockExpiry( $timestamp, ?string $parentExpiry = null ) { |
1645 | $maxDuration = $this->options->get( MainConfigNames::AutoblockExpiry ); |
1646 | $expiry = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $timestamp ) + $maxDuration ); |
1647 | if ( $parentExpiry !== null && $parentExpiry !== 'infinity' ) { |
1648 | $expiry = min( $parentExpiry, $expiry ); |
1649 | } |
1650 | return $expiry; |
1651 | } |
1652 | |
1653 | // endregion -- end of database write methods |
1654 | |
1655 | } |