MediaWiki  master
DatabaseBlockStore.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Block;
24 
26 use DeferredUpdates;
27 use InvalidArgumentException;
35 use Psr\Log\LoggerInterface;
40 
48  private $wikiId;
49 
51  private $options;
52 
56  public const CONSTRUCTOR_OPTIONS = [
60  ];
61 
63  private $logger;
64 
66  private $actorStoreFactory;
67 
69  private $blockRestrictionStore;
70 
72  private $commentStore;
73 
75  private $hookRunner;
76 
78  private $loadBalancer;
79 
81  private $readOnlyMode;
82 
84  private $userFactory;
85 
98  public function __construct(
99  ServiceOptions $options,
100  LoggerInterface $logger,
101  ActorStoreFactory $actorStoreFactory,
102  BlockRestrictionStore $blockRestrictionStore,
103  CommentStore $commentStore,
104  HookContainer $hookContainer,
105  ILoadBalancer $loadBalancer,
106  ReadOnlyMode $readOnlyMode,
107  UserFactory $userFactory,
108  $wikiId = DatabaseBlock::LOCAL
109  ) {
110  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
111 
112  $this->wikiId = $wikiId;
113 
114  $this->options = $options;
115  $this->logger = $logger;
116  $this->actorStoreFactory = $actorStoreFactory;
117  $this->blockRestrictionStore = $blockRestrictionStore;
118  $this->commentStore = $commentStore;
119  $this->hookRunner = new HookRunner( $hookContainer );
120  $this->loadBalancer = $loadBalancer;
121  $this->readOnlyMode = $readOnlyMode;
122  $this->userFactory = $userFactory;
123  }
124 
130  public function purgeExpiredBlocks() {
131  if ( $this->readOnlyMode->isReadOnly( $this->wikiId ) ) {
132  return;
133  }
134 
135  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
136  $store = $this->blockRestrictionStore;
137  $limit = $this->options->get( MainConfigNames::UpdateRowsPerQuery );
138 
140  $dbw,
141  __METHOD__,
142  static function ( IDatabase $dbw, $fname ) use ( $store, $limit ) {
143  $ids = $dbw->newSelectQueryBuilder()
144  ->select( 'ipb_id' )
145  ->from( 'ipblocks' )
146  ->where( $dbw->buildComparison( '<', [ 'ipb_expiry' => $dbw->timestamp() ] ) )
147  // Set a limit to avoid causing replication lag (T301742)
148  ->limit( $limit )
149  ->caller( $fname )->fetchFieldValues();
150  if ( $ids ) {
151  $ids = array_map( 'intval', $ids );
152  $store->deleteByBlockId( $ids );
153  $dbw->newDeleteQueryBuilder()
154  ->deleteFrom( 'ipblocks' )
155  ->where( [ 'ipb_id' => $ids ] )
156  ->caller( $fname )->execute();
157  }
158  }
159  ) );
160  }
161 
169  private function checkDatabaseDomain( $expectedWiki, ?IDatabase $db = null ) {
170  if ( $db ) {
171  $dbDomain = $db->getDomainID();
172  $storeDomain = $this->loadBalancer->resolveDomainID( $expectedWiki );
173  if ( $dbDomain !== $storeDomain ) {
174  throw new InvalidArgumentException(
175  "DB connection domain '$dbDomain' does not match '$storeDomain'"
176  );
177  }
178  } else {
179  if ( $expectedWiki !== $this->wikiId ) {
180  throw new InvalidArgumentException(
181  "Must provide a database connection for wiki '$expectedWiki'."
182  );
183  }
184  }
185  }
186 
199  public function insertBlock(
200  DatabaseBlock $block,
201  IDatabase $database = null
202  ) {
203  $blocker = $block->getBlocker();
204  if ( !$blocker || $blocker->getName() === '' ) {
205  throw new InvalidArgumentException( 'Cannot insert a block without a blocker set' );
206  }
207 
208  if ( $database !== null ) {
210  'Old method signature: Passing a $database is no longer supported',
211  '1.41'
212  );
213  }
214 
215  $this->checkDatabaseDomain( $block->getWikiId(), $database );
216 
217  $this->logger->debug( 'Inserting block; timestamp ' . $block->getTimestamp() );
218 
219  $this->purgeExpiredBlocks();
220 
221  $dbw = $database ?: $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
222  $row = $this->getArrayForDatabaseBlock( $block, $dbw );
223 
224  $dbw->newInsertQueryBuilder()
225  ->insertInto( 'ipblocks' )
226  ->ignore()
227  ->row( $row )
228  ->caller( __METHOD__ )->execute();
229  $affected = $dbw->affectedRows();
230 
231  if ( $affected ) {
232  $block->setId( $dbw->insertId() );
233  $restrictions = $block->getRawRestrictions();
234  if ( $restrictions ) {
235  $this->blockRestrictionStore->insert( $restrictions );
236  }
237  }
238 
239  // Don't collide with expired blocks.
240  // Do this after trying to insert to avoid locking.
241  if ( !$affected ) {
242  // T96428: The ipb_address index uses a prefix on a field, so
243  // use a standard SELECT + DELETE to avoid annoying gap locks.
244  $ids = $dbw->newSelectQueryBuilder()
245  ->select( 'ipb_id' )
246  ->from( 'ipblocks' )
247  ->where( [ 'ipb_address' => $row['ipb_address'], 'ipb_user' => $row['ipb_user'] ] )
248  ->andWhere( $dbw->buildComparison( '<', [ 'ipb_expiry' => $dbw->timestamp() ] ) )
249  ->caller( __METHOD__ )->fetchFieldValues();
250  if ( $ids ) {
251  $ids = array_map( 'intval', $ids );
252  $dbw->newDeleteQueryBuilder()
253  ->deleteFrom( 'ipblocks' )
254  ->where( [ 'ipb_id' => $ids ] )
255  ->caller( __METHOD__ )->execute();
256  $this->blockRestrictionStore->deleteByBlockId( $ids );
257  $dbw->newInsertQueryBuilder()
258  ->insertInto( 'ipblocks' )
259  ->ignore()
260  ->row( $row )
261  ->caller( __METHOD__ )->execute();
262  $affected = $dbw->affectedRows();
263  if ( $affected ) {
264  $block->setId( $dbw->insertId() );
265  $restrictions = $block->getRawRestrictions();
266  if ( $restrictions ) {
267  $this->blockRestrictionStore->insert( $restrictions );
268  }
269  }
270  }
271  }
272 
273  if ( $affected ) {
274  $autoBlockIds = $this->doRetroactiveAutoblock( $block );
275 
276  if ( $this->options->get( MainConfigNames::BlockDisablesLogin ) ) {
277  $targetUserIdentity = $block->getTargetUserIdentity();
278  if ( $targetUserIdentity ) {
279  $targetUser = $this->userFactory->newFromUserIdentity( $targetUserIdentity );
280  // TODO: respect the wiki the block belongs to here
281  // Change user login token to force them to be logged out.
282  $targetUser->setToken();
283  $targetUser->saveSettings();
284  }
285  }
286 
287  return [ 'id' => $block->getId( $this->wikiId ), 'autoIds' => $autoBlockIds ];
288  }
289 
290  return false;
291  }
292 
301  public function updateBlock( DatabaseBlock $block ) {
302  $this->logger->debug( 'Updating block; timestamp ' . $block->getTimestamp() );
303 
304  $this->checkDatabaseDomain( $block->getWikiId() );
305 
306  $blockId = $block->getId( $this->wikiId );
307  if ( !$blockId ) {
308  throw new InvalidArgumentException(
309  __METHOD__ . " requires that a block id be set\n"
310  );
311  }
312 
313  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
314 
315  $row = $this->getArrayForDatabaseBlock( $block, $dbw );
316  $dbw->startAtomic( __METHOD__ );
317 
318  $result = true;
319 
320  $dbw->newUpdateQueryBuilder()
321  ->update( 'ipblocks' )
322  ->set( $row )
323  ->where( [ 'ipb_id' => $blockId ] )
324  ->caller( __METHOD__ )->execute();
325 
326  // Only update the restrictions if they have been modified.
327  $restrictions = $block->getRawRestrictions();
328  if ( $restrictions !== null ) {
329  // An empty array should remove all of the restrictions.
330  if ( $restrictions === [] ) {
331  $result = $this->blockRestrictionStore->deleteByBlockId( $blockId );
332  } else {
333  $result = $this->blockRestrictionStore->update( $restrictions );
334  }
335  }
336 
337  if ( $block->isAutoblocking() ) {
338  // Update corresponding autoblock(s) (T50813)
339  $dbw->newUpdateQueryBuilder()
340  ->update( 'ipblocks' )
341  ->set( $this->getArrayForAutoblockUpdate( $block ) )
342  ->where( [ 'ipb_parent_block_id' => $blockId ] )
343  ->caller( __METHOD__ )->execute();
344 
345  // Only update the restrictions if they have been modified.
346  if ( $restrictions !== null ) {
347  $this->blockRestrictionStore->updateByParentBlockId(
348  $blockId,
349  $restrictions
350  );
351  }
352  } else {
353  // Autoblock no longer required, delete corresponding autoblock(s)
354  $ids = $dbw->newSelectQueryBuilder()
355  ->select( 'ipb_id' )
356  ->from( 'ipblocks' )
357  ->where( [ 'ipb_parent_block_id' => $blockId ] )
358  ->caller( __METHOD__ )->fetchFieldValues();
359  if ( $ids ) {
360  $ids = array_map( 'intval', $ids );
361  $this->blockRestrictionStore->deleteByBlockId( $ids );
362  $dbw->newDeleteQueryBuilder()
363  ->deleteFrom( 'ipblocks' )
364  ->where( [ 'ipb_id' => $ids ] )
365  ->caller( __METHOD__ )->execute();
366  }
367  }
368 
369  $dbw->endAtomic( __METHOD__ );
370 
371  if ( $result ) {
372  $autoBlockIds = $this->doRetroactiveAutoblock( $block );
373  return [ 'id' => $blockId, 'autoIds' => $autoBlockIds ];
374  }
375 
376  return false;
377  }
378 
385  public function deleteBlock( DatabaseBlock $block ): bool {
386  if ( $this->readOnlyMode->isReadOnly( $this->wikiId ) ) {
387  return false;
388  }
389 
390  $this->checkDatabaseDomain( $block->getWikiId() );
391 
392  $blockId = $block->getId( $this->wikiId );
393 
394  if ( !$blockId ) {
395  throw new InvalidArgumentException(
396  __METHOD__ . " requires that a block id be set\n"
397  );
398  }
399  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
400  $ids = $dbw->newSelectQueryBuilder()
401  ->select( 'ipb_id' )
402  ->from( 'ipblocks' )
403  ->where( [ 'ipb_parent_block_id' => $blockId ] )
404  ->caller( __METHOD__ )->fetchFieldValues();
405  $ids = array_map( 'intval', $ids );
406  $ids[] = $blockId;
407 
408  $this->blockRestrictionStore->deleteByBlockId( $ids );
409  $dbw->newDeleteQueryBuilder()
410  ->deleteFrom( 'ipblocks' )
411  ->where( [ 'ipb_id' => $ids ] )
412  ->caller( __METHOD__ )->execute();
413 
414  return $dbw->affectedRows() > 0;
415  }
416 
425  private function getArrayForDatabaseBlock(
426  DatabaseBlock $block,
427  IDatabase $dbw
428  ): array {
429  $expiry = $dbw->encodeExpiry( $block->getExpiry() );
430 
431  if ( $block->getTargetUserIdentity() ) {
432  $userId = $block->getTargetUserIdentity()->getId( $this->wikiId );
433  } else {
434  $userId = 0;
435  }
436  $blocker = $block->getBlocker();
437  if ( !$blocker ) {
438  throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
439  }
440  // DatabaseBlockStore supports inserting cross-wiki blocks by passing
441  // non-local IDatabase and blocker.
442  $blockerActor = $this->actorStoreFactory
443  ->getActorStore( $dbw->getDomainID() )
444  ->acquireActorId( $blocker, $dbw );
445 
446  $blockArray = [
447  'ipb_address' => $block->getTargetName(),
448  'ipb_user' => $userId,
449  'ipb_by_actor' => $blockerActor,
450  'ipb_timestamp' => $dbw->timestamp( $block->getTimestamp() ),
451  'ipb_auto' => $block->getType() === AbstractBlock::TYPE_AUTO,
452  'ipb_anon_only' => !$block->isHardblock(),
453  'ipb_create_account' => $block->isCreateAccountBlocked(),
454  'ipb_enable_autoblock' => $block->isAutoblocking(),
455  'ipb_expiry' => $expiry,
456  'ipb_range_start' => $block->getRangeStart(),
457  'ipb_range_end' => $block->getRangeEnd(),
458  'ipb_deleted' => intval( $block->getHideName() ), // typecast required for SQLite
459  'ipb_block_email' => $block->isEmailBlocked(),
460  'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
461  'ipb_parent_block_id' => $block->getParentBlockId(),
462  'ipb_sitewide' => $block->isSitewide(),
463  ];
464  $commentArray = $this->commentStore->insert(
465  $dbw,
466  'ipb_reason',
467  $block->getReasonComment()
468  );
469 
470  $combinedArray = $blockArray + $commentArray;
471  return $combinedArray;
472  }
473 
480  private function getArrayForAutoblockUpdate( DatabaseBlock $block ): array {
481  $blocker = $block->getBlocker();
482  if ( !$blocker ) {
483  throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
484  }
485  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
486  $blockerActor = $this->actorStoreFactory
487  ->getActorNormalization( $this->wikiId )
488  ->acquireActorId( $blocker, $dbw );
489  $blockArray = [
490  'ipb_by_actor' => $blockerActor,
491  'ipb_create_account' => $block->isCreateAccountBlocked(),
492  'ipb_deleted' => (int)$block->getHideName(), // typecast required for SQLite
493  'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
494  'ipb_sitewide' => $block->isSitewide(),
495  ];
496 
497  $commentArray = $this->commentStore->insert(
498  $dbw,
499  'ipb_reason',
500  $block->getReasonComment()
501  );
502 
503  $combinedArray = $blockArray + $commentArray;
504  return $combinedArray;
505  }
506 
514  private function doRetroactiveAutoblock( DatabaseBlock $block ): array {
515  $autoBlockIds = [];
516  // If autoblock is enabled, autoblock the LAST IP(s) used
517  if ( $block->isAutoblocking() && $block->getType() == AbstractBlock::TYPE_USER ) {
518  $this->logger->debug(
519  'Doing retroactive autoblocks for ' . $block->getTargetName()
520  );
521 
522  $hookAutoBlocked = [];
523  $continue = $this->hookRunner->onPerformRetroactiveAutoblock(
524  $block,
525  $hookAutoBlocked
526  );
527 
528  if ( $continue ) {
529  $coreAutoBlocked = $this->performRetroactiveAutoblock( $block );
530  $autoBlockIds = array_merge( $hookAutoBlocked, $coreAutoBlocked );
531  } else {
532  $autoBlockIds = $hookAutoBlocked;
533  }
534  }
535  return $autoBlockIds;
536  }
537 
545  private function performRetroactiveAutoblock( DatabaseBlock $block ): array {
546  if ( !$this->options->get( MainConfigNames::PutIPinRC ) ) {
547  // No IPs in the recent changes table to autoblock
548  return [];
549  }
550 
551  $type = $block->getType();
552  if ( $type !== AbstractBlock::TYPE_USER ) {
553  // Autoblocks only apply to users
554  return [];
555  }
556 
557  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA, [], $this->wikiId );
558 
559  $targetUser = $block->getTargetUserIdentity();
560  $actor = $targetUser ? $this->actorStoreFactory
561  ->getActorNormalization( $this->wikiId )
562  ->findActorId( $targetUser, $dbr ) : null;
563 
564  if ( !$actor ) {
565  $this->logger->debug( 'No actor found to retroactively autoblock' );
566  return [];
567  }
568 
569  $rcIp = $dbr->newSelectQueryBuilder()
570  ->select( 'rc_ip' )
571  ->from( 'recentchanges' )
572  ->where( [ 'rc_actor' => $actor ] )
573  ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC )
574  ->caller( __METHOD__ )->fetchField();
575 
576  if ( !$rcIp ) {
577  $this->logger->debug( 'No IP found to retroactively autoblock' );
578  return [];
579  }
580 
581  $id = $block->doAutoblock( $rcIp );
582  if ( !$id ) {
583  return [];
584  }
585  return [ $id ];
586  }
587 
588 }
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
Deferrable Update for closure/callback updates that should use auto-commit mode.
Defer callable updates to run later in the PHP process.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
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, $wikiId=DatabaseBlock::LOCAL)
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.(since 1.38) ?int
getRawRestrictions()
Get restrictions without loading from database if not yet loaded.
Handle database storage of comments such as edit summaries and log reasons.
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...
Definition: HookRunner.php:568
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.
Definition: UserFactory.php:41
Determine whether a site is currently in read-only mode.
Build SELECT queries with a fluent interface.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
newDeleteQueryBuilder()
Get an DeleteQueryBuilder bound to this connection.
This class is a delegate to ILBFactory for a given database cluster.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
buildComparison(string $op, array $conds)
Build a condition comparing multiple values, for use with indexes that cover multiple fields,...
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28