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