MediaWiki REL1_39
DatabaseBlockStore.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Block;
24
26use CommentStore;
28use InvalidArgumentException;
35use MWException;
36use Psr\Log\LoggerInterface;
37use ReadOnlyMode;
38use Wikimedia\Assert\Assert;
41
48
50 private $options;
51
55 public const CONSTRUCTOR_OPTIONS = [
59 ];
60
62 private $logger;
63
65 private $actorStoreFactory;
66
68 private $blockRestrictionStore;
69
71 private $commentStore;
72
74 private $hookRunner;
75
77 private $loadBalancer;
78
80 private $readOnlyMode;
81
83 private $userFactory;
84
96 public function __construct(
97 ServiceOptions $options,
98 LoggerInterface $logger,
99 ActorStoreFactory $actorStoreFactory,
100 BlockRestrictionStore $blockRestrictionStore,
101 CommentStore $commentStore,
102 HookContainer $hookContainer,
103 ILoadBalancer $loadBalancer,
104 ReadOnlyMode $readOnlyMode,
105 UserFactory $userFactory
106 ) {
107 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
108
109 $this->options = $options;
110 $this->logger = $logger;
111 $this->actorStoreFactory = $actorStoreFactory;
112 $this->blockRestrictionStore = $blockRestrictionStore;
113 $this->commentStore = $commentStore;
114 $this->hookRunner = new HookRunner( $hookContainer );
115 $this->loadBalancer = $loadBalancer;
116 $this->readOnlyMode = $readOnlyMode;
117 $this->userFactory = $userFactory;
118 }
119
125 public function purgeExpiredBlocks() {
126 if ( $this->readOnlyMode->isReadOnly() ) {
127 return;
128 }
129
130 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
131 $store = $this->blockRestrictionStore;
132 $limit = $this->options->get( MainConfigNames::UpdateRowsPerQuery );
133
134 DeferredUpdates::addUpdate( new AutoCommitUpdate(
135 $dbw,
136 __METHOD__,
137 static function ( IDatabase $dbw, $fname ) use ( $store, $limit ) {
138 $ids = $dbw->selectFieldValues(
139 'ipblocks',
140 'ipb_id',
141 [ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
142 $fname,
143 // Set a limit to avoid causing read-only mode (T301742)
144 [ 'LIMIT' => $limit ]
145 );
146 if ( $ids ) {
147 $store->deleteByBlockId( $ids );
148 $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
149 }
150 }
151 ) );
152 }
153
161 private function checkDatabaseDomain( ?IDatabase $db, $expectedWiki ) {
162 if ( $db ) {
163 $dbDomain = $db->getDomainID();
164 $storeDomain = $this->loadBalancer->resolveDomainID( $expectedWiki );
165 if ( $dbDomain !== $storeDomain ) {
166 throw new InvalidArgumentException(
167 "DB connection domain '$dbDomain' does not match '$storeDomain'"
168 );
169 }
170 } else {
171 if ( $expectedWiki !== Block::LOCAL ) {
172 throw new InvalidArgumentException(
173 "Must provide a database connection for wiki '$expectedWiki'."
174 );
175 }
176 }
177 }
178
190 public function insertBlock(
191 DatabaseBlock $block,
192 IDatabase $database = null
193 ) {
194 $blocker = $block->getBlocker();
195 if ( !$blocker || $blocker->getName() === '' ) {
196 throw new MWException( 'Cannot insert a block without a blocker set' );
197 }
198
199 $this->checkDatabaseDomain( $database, $block->getWikiId() );
200
201 $this->logger->debug( 'Inserting block; timestamp ' . $block->getTimestamp() );
202
203 // TODO T258866 - consider passing the database
204 $this->purgeExpiredBlocks();
205
206 $dbw = $database ?: $this->loadBalancer->getConnectionRef( DB_PRIMARY );
207 $row = $this->getArrayForDatabaseBlock( $block, $dbw );
208
209 $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
210 $affected = $dbw->affectedRows();
211
212 if ( $affected ) {
213 $block->setId( $dbw->insertId() );
214 $restrictions = $block->getRawRestrictions();
215 if ( $restrictions ) {
216 $this->blockRestrictionStore->insert( $restrictions );
217 }
218 }
219
220 // Don't collide with expired blocks.
221 // Do this after trying to insert to avoid locking.
222 if ( !$affected ) {
223 // T96428: The ipb_address index uses a prefix on a field, so
224 // use a standard SELECT + DELETE to avoid annoying gap locks.
225 $ids = $dbw->selectFieldValues(
226 'ipblocks',
227 'ipb_id',
228 [
229 'ipb_address' => $row['ipb_address'],
230 'ipb_user' => $row['ipb_user'],
231 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
232 ],
233 __METHOD__
234 );
235 if ( $ids ) {
236 $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
237 $this->blockRestrictionStore->deleteByBlockId( $ids );
238 $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
239 $affected = $dbw->affectedRows();
240 $block->setId( $dbw->insertId() );
241 $restrictions = $block->getRawRestrictions();
242 if ( $restrictions ) {
243 $this->blockRestrictionStore->insert( $restrictions );
244 }
245 }
246 }
247
248 if ( $affected ) {
249 $autoBlockIds = $this->doRetroactiveAutoblock( $block );
250
251 if ( $this->options->get( MainConfigNames::BlockDisablesLogin ) ) {
252 $targetUserIdentity = $block->getTargetUserIdentity();
253 if ( $targetUserIdentity ) {
254 $targetUser = $this->userFactory->newFromUserIdentity( $targetUserIdentity );
255 // Change user login token to force them to be logged out.
256 $targetUser->setToken();
257 $targetUser->saveSettings();
258 }
259 }
260
261 return [ 'id' => $block->getId(), 'autoIds' => $autoBlockIds ];
262 }
263
264 return false;
265 }
266
275 public function updateBlock( DatabaseBlock $block ) {
276 $this->logger->debug( 'Updating block; timestamp ' . $block->getTimestamp() );
277
278 // We could allow cross-wiki updates here, just like we do in insertBlock().
279 Assert::parameter(
280 $block->getWikiId() === Block::LOCAL,
281 '$block->getWikiId()',
282 'must belong to the local wiki.'
283 );
284 $blockId = $block->getId();
285 if ( !$blockId ) {
286 throw new MWException(
287 __METHOD__ . " requires that a block id be set\n"
288 );
289 }
290
291 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
292 $row = $this->getArrayForDatabaseBlock( $block, $dbw );
293 $dbw->startAtomic( __METHOD__ );
294
295 $result = $dbw->update(
296 'ipblocks',
297 $row,
298 [ 'ipb_id' => $blockId ],
299 __METHOD__
300 );
301
302 // Only update the restrictions if they have been modified.
303 $restrictions = $block->getRawRestrictions();
304 if ( $restrictions !== null ) {
305 // An empty array should remove all of the restrictions.
306 if ( empty( $restrictions ) ) {
307 $success = $this->blockRestrictionStore->deleteByBlockId( $blockId );
308 } else {
309 $success = $this->blockRestrictionStore->update( $restrictions );
310 }
311 // Update the result. The first false is the result, otherwise, true.
312 $result = $result && $success;
313 }
314
315 if ( $block->isAutoblocking() ) {
316 // update corresponding autoblock(s) (T50813)
317 $dbw->update(
318 'ipblocks',
319 $this->getArrayForAutoblockUpdate( $block ),
320 [ 'ipb_parent_block_id' => $blockId ],
321 __METHOD__
322 );
323
324 // Only update the restrictions if they have been modified.
325 if ( $restrictions !== null ) {
326 $this->blockRestrictionStore->updateByParentBlockId(
327 $blockId,
328 $restrictions
329 );
330 }
331 } else {
332 // autoblock no longer required, delete corresponding autoblock(s)
333 $this->blockRestrictionStore->deleteByParentBlockId( $blockId );
334 $dbw->delete(
335 'ipblocks',
336 [ 'ipb_parent_block_id' => $blockId ],
337 __METHOD__
338 );
339 }
340
341 $dbw->endAtomic( __METHOD__ );
342
343 if ( $result ) {
344 $autoBlockIds = $this->doRetroactiveAutoblock( $block );
345 return [ 'id' => $blockId, 'autoIds' => $autoBlockIds ];
346 }
347
348 return false;
349 }
350
358 public function deleteBlock( DatabaseBlock $block ): bool {
359 if ( $this->readOnlyMode->isReadOnly() ) {
360 return false;
361 }
362
363 $blockId = $block->getId();
364
365 if ( !$blockId ) {
366 throw new MWException(
367 __METHOD__ . " requires that a block id be set\n"
368 );
369 }
370 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
371
372 $this->blockRestrictionStore->deleteByParentBlockId( $blockId );
373 $dbw->delete(
374 'ipblocks',
375 [ 'ipb_parent_block_id' => $blockId ],
376 __METHOD__
377 );
378
379 $this->blockRestrictionStore->deleteByBlockId( $blockId );
380 $dbw->delete(
381 'ipblocks',
382 [ 'ipb_id' => $blockId ],
383 __METHOD__
384 );
385
386 return $dbw->affectedRows() > 0;
387 }
388
397 private function getArrayForDatabaseBlock(
398 DatabaseBlock $block,
399 IDatabase $dbw
400 ): array {
401 $expiry = $dbw->encodeExpiry( $block->getExpiry() );
402
403 if ( $block->getTargetUserIdentity() ) {
404 $userId = $block->getTargetUserIdentity()->getId( $block->getWikiId() );
405 } else {
406 $userId = 0;
407 }
408 $blocker = $block->getBlocker();
409 if ( !$blocker ) {
410 throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
411 }
412 // DatabaseBlockStore supports inserting cross-wiki blocks by passing non-local IDatabase and blocker.
413 $blockerActor = $this->actorStoreFactory
414 ->getActorStore( $dbw->getDomainID() )
415 ->acquireActorId( $blocker, $dbw );
416
417 $blockArray = [
418 'ipb_address' => $block->getTargetName(),
419 'ipb_user' => $userId,
420 'ipb_by_actor' => $blockerActor,
421 'ipb_timestamp' => $dbw->timestamp( $block->getTimestamp() ),
422 'ipb_auto' => $block->getType() === AbstractBlock::TYPE_AUTO,
423 'ipb_anon_only' => !$block->isHardblock(),
424 'ipb_create_account' => $block->isCreateAccountBlocked(),
425 'ipb_enable_autoblock' => $block->isAutoblocking(),
426 'ipb_expiry' => $expiry,
427 'ipb_range_start' => $block->getRangeStart(),
428 'ipb_range_end' => $block->getRangeEnd(),
429 'ipb_deleted' => intval( $block->getHideName() ), // typecast required for SQLite
430 'ipb_block_email' => $block->isEmailBlocked(),
431 'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
432 'ipb_parent_block_id' => $block->getParentBlockId(),
433 'ipb_sitewide' => $block->isSitewide(),
434 ];
435 $commentArray = $this->commentStore->insert(
436 $dbw,
437 'ipb_reason',
438 $block->getReasonComment()
439 );
440
441 $combinedArray = $blockArray + $commentArray;
442 return $combinedArray;
443 }
444
451 private function getArrayForAutoblockUpdate( DatabaseBlock $block ): array {
452 $blocker = $block->getBlocker();
453 if ( !$blocker ) {
454 throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
455 }
456 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
457 $blockerActor = $this->actorStoreFactory
458 ->getActorNormalization()
459 ->acquireActorId( $blocker, $dbw );
460 $blockArray = [
461 'ipb_by_actor' => $blockerActor,
462 'ipb_create_account' => $block->isCreateAccountBlocked(),
463 'ipb_deleted' => (int)$block->getHideName(), // typecast required for SQLite
464 'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
465 'ipb_sitewide' => $block->isSitewide(),
466 ];
467
468 $commentArray = $this->commentStore->insert(
469 $dbw,
470 'ipb_reason',
471 $block->getReasonComment()
472 );
473
474 $combinedArray = $blockArray + $commentArray;
475 return $combinedArray;
476 }
477
485 private function doRetroactiveAutoblock( DatabaseBlock $block ): array {
486 $autoBlockIds = [];
487 // If autoblock is enabled, autoblock the LAST IP(s) used
488 if ( $block->isAutoblocking() && $block->getType() == AbstractBlock::TYPE_USER ) {
489 $this->logger->debug(
490 'Doing retroactive autoblocks for ' . $block->getTargetName()
491 );
492
493 $hookAutoBlocked = [];
494 $continue = $this->hookRunner->onPerformRetroactiveAutoblock(
495 $block,
496 $hookAutoBlocked
497 );
498
499 if ( $continue ) {
500 $coreAutoBlocked = $this->performRetroactiveAutoblock( $block );
501 $autoBlockIds = array_merge( $hookAutoBlocked, $coreAutoBlocked );
502 } else {
503 $autoBlockIds = $hookAutoBlocked;
504 }
505 }
506 return $autoBlockIds;
507 }
508
516 private function performRetroactiveAutoblock( DatabaseBlock $block ): array {
517 if ( !$this->options->get( MainConfigNames::PutIPinRC ) ) {
518 // No IPs in the recent changes table to autoblock
519 return [];
520 }
521
522 $type = $block->getType();
523 if ( $type !== AbstractBlock::TYPE_USER ) {
524 // Autoblocks only apply to users
525 return [];
526 }
527
528 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
529
530 $targetUser = $block->getTargetUserIdentity();
531 $actor = $targetUser ? $this->actorStoreFactory
532 ->getActorNormalization( $block->getWikiId() )
533 ->findActorId( $targetUser, $dbr ) : null;
534
535 if ( !$actor ) {
536 $this->logger->debug( 'No actor found to retroactively autoblock' );
537 return [];
538 }
539
540 $rcIp = $dbr->selectField(
541 [ 'recentchanges' ],
542 'rc_ip',
543 [ 'rc_actor' => $actor ],
544 __METHOD__,
545 [ 'ORDER BY' => 'rc_timestamp DESC' ]
546 );
547
548 if ( !$rcIp ) {
549 $this->logger->debug( 'No IP found to retroactively autoblock' );
550 return [];
551 }
552
553 $id = $block->doAutoblock( $rcIp );
554 if ( !$id ) {
555 return [];
556 }
557 return [ $id ];
558 }
559
560}
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Deferrable Update for closure/callback updates that should use auto-commit mode.
Handle database storage of comments such as edit summaries and log reasons.
Class for managing the deferral of updates within the scope of a PHP script invocation.
MediaWiki exception.
getTimestamp()
Get the timestamp indicating when the block was created.
insertBlock(DatabaseBlock $block, IDatabase $database=null)
Insert a block into the block table.
updateBlock(DatabaseBlock $block)
Update a block in the DB with new parameters.
deleteBlock(DatabaseBlock $block)
Delete a DatabaseBlock from the database.
purgeExpiredBlocks()
Delete expired blocks from the ipblocks table.
__construct(ServiceOptions $options, LoggerInterface $logger, ActorStoreFactory $actorStoreFactory, BlockRestrictionStore $blockRestrictionStore, CommentStore $commentStore, HookContainer $hookContainer, ILoadBalancer $loadBalancer, ReadOnlyMode $readOnlyMode, UserFactory $userFactory)
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
setId( $blockId)
Set the block ID.
getBlocker()
Get the user who implemented this block.
getId( $wikiId=self::LOCAL)
Get the block ID.?int
getRawRestrictions()
Get restrictions without loading from database if not yet loaded.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
const UpdateRowsPerQuery
Name constant for the UpdateRowsPerQuery setting, for use with Config::get()
const BlockDisablesLogin
Name constant for the BlockDisablesLogin setting, for use with Config::get()
const PutIPinRC
Name constant for the PutIPinRC setting, for use with Config::get()
Creates User objects.
A service class for fetching the wiki's current read-only mode.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
getDomainID()
Return the currently selected domain ID.
delete( $table, $conds, $fname=__METHOD__)
Delete all rows in a table that match a condition.
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
Create and track the database connections and transactions for a given database cluster.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28