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 MWException;
36 use Psr\Log\LoggerInterface;
37 use ReadOnlyMode;
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() ) {
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->selectFieldValues(
144  'ipblocks',
145  'ipb_id',
146  [ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
147  $fname,
148  // Set a limit to avoid causing read-only mode (T301742)
149  [ 'LIMIT' => $limit ]
150  );
151  if ( $ids ) {
152  $store->deleteByBlockId( $ids );
153  $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
154  }
155  }
156  ) );
157  }
158 
166  private function checkDatabaseDomain( $expectedWiki, ?IDatabase $db = null ) {
167  if ( $db ) {
168  $dbDomain = $db->getDomainID();
169  $storeDomain = $this->loadBalancer->resolveDomainID( $expectedWiki );
170  if ( $dbDomain !== $storeDomain ) {
171  throw new InvalidArgumentException(
172  "DB connection domain '$dbDomain' does not match '$storeDomain'"
173  );
174  }
175  } else {
176  if ( $expectedWiki !== $this->wikiId ) {
177  throw new InvalidArgumentException(
178  "Must provide a database connection for wiki '$expectedWiki'."
179  );
180  }
181  }
182  }
183 
195  public function insertBlock(
196  DatabaseBlock $block,
197  IDatabase $database = null
198  ) {
199  $blocker = $block->getBlocker();
200  if ( !$blocker || $blocker->getName() === '' ) {
201  throw new MWException( 'Cannot insert a block without a blocker set' );
202  }
203 
204  $this->checkDatabaseDomain( $block->getWikiId(), $database );
205 
206  $this->logger->debug( 'Inserting block; timestamp ' . $block->getTimestamp() );
207 
208  $this->purgeExpiredBlocks();
209 
210  $dbw = $database ?: $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
211  $row = $this->getArrayForDatabaseBlock( $block, $dbw );
212 
213  $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
214  $affected = $dbw->affectedRows();
215 
216  if ( $affected ) {
217  $block->setId( $dbw->insertId() );
218  $restrictions = $block->getRawRestrictions();
219  if ( $restrictions ) {
220  $this->blockRestrictionStore->insert( $restrictions );
221  }
222  }
223 
224  // Don't collide with expired blocks.
225  // Do this after trying to insert to avoid locking.
226  if ( !$affected ) {
227  // T96428: The ipb_address index uses a prefix on a field, so
228  // use a standard SELECT + DELETE to avoid annoying gap locks.
229  $ids = $dbw->selectFieldValues(
230  'ipblocks',
231  'ipb_id',
232  [
233  'ipb_address' => $row['ipb_address'],
234  'ipb_user' => $row['ipb_user'],
235  'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
236  ],
237  __METHOD__
238  );
239  if ( $ids ) {
240  $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
241  $this->blockRestrictionStore->deleteByBlockId( $ids );
242  $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
243  $affected = $dbw->affectedRows();
244  $block->setId( $dbw->insertId() );
245  $restrictions = $block->getRawRestrictions();
246  if ( $restrictions ) {
247  $this->blockRestrictionStore->insert( $restrictions );
248  }
249  }
250  }
251 
252  if ( $affected ) {
253  $autoBlockIds = $this->doRetroactiveAutoblock( $block );
254 
255  if ( $this->options->get( MainConfigNames::BlockDisablesLogin ) ) {
256  $targetUserIdentity = $block->getTargetUserIdentity();
257  if ( $targetUserIdentity ) {
258  $targetUser = $this->userFactory->newFromUserIdentity( $targetUserIdentity );
259  // TODO: respect the wiki the block belongs to here
260  // Change user login token to force them to be logged out.
261  $targetUser->setToken();
262  $targetUser->saveSettings();
263  }
264  }
265 
266  return [ 'id' => $block->getId( $this->wikiId ), 'autoIds' => $autoBlockIds ];
267  }
268 
269  return false;
270  }
271 
280  public function updateBlock( DatabaseBlock $block ) {
281  $this->logger->debug( 'Updating block; timestamp ' . $block->getTimestamp() );
282 
283  $this->checkDatabaseDomain( $block->getWikiId() );
284 
285  $blockId = $block->getId( $this->wikiId );
286  if ( !$blockId ) {
287  throw new MWException(
288  __METHOD__ . " requires that a block id be set\n"
289  );
290  }
291 
292  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
293 
294  $row = $this->getArrayForDatabaseBlock( $block, $dbw );
295  $dbw->startAtomic( __METHOD__ );
296 
297  $result = $dbw->update(
298  'ipblocks',
299  $row,
300  [ 'ipb_id' => $blockId ],
301  __METHOD__
302  );
303 
304  // Only update the restrictions if they have been modified.
305  $restrictions = $block->getRawRestrictions();
306  if ( $restrictions !== null ) {
307  // An empty array should remove all of the restrictions.
308  if ( empty( $restrictions ) ) {
309  $success = $this->blockRestrictionStore->deleteByBlockId( $blockId );
310  } else {
311  $success = $this->blockRestrictionStore->update( $restrictions );
312  }
313  // Update the result. The first false is the result, otherwise, true.
314  $result = $result && $success;
315  }
316 
317  if ( $block->isAutoblocking() ) {
318  // update corresponding autoblock(s) (T50813)
319  $dbw->update(
320  'ipblocks',
321  $this->getArrayForAutoblockUpdate( $block ),
322  [ 'ipb_parent_block_id' => $blockId ],
323  __METHOD__
324  );
325 
326  // Only update the restrictions if they have been modified.
327  if ( $restrictions !== null ) {
328  $this->blockRestrictionStore->updateByParentBlockId(
329  $blockId,
330  $restrictions
331  );
332  }
333  } else {
334  // autoblock no longer required, delete corresponding autoblock(s)
335  $this->blockRestrictionStore->deleteByParentBlockId( $blockId );
336  $dbw->delete(
337  'ipblocks',
338  [ 'ipb_parent_block_id' => $blockId ],
339  __METHOD__
340  );
341  }
342 
343  $dbw->endAtomic( __METHOD__ );
344 
345  if ( $result ) {
346  $autoBlockIds = $this->doRetroactiveAutoblock( $block );
347  return [ 'id' => $blockId, 'autoIds' => $autoBlockIds ];
348  }
349 
350  return false;
351  }
352 
360  public function deleteBlock( DatabaseBlock $block ): bool {
361  if ( $this->readOnlyMode->isReadOnly() ) {
362  return false;
363  }
364 
365  $this->checkDatabaseDomain( $block->getWikiId() );
366 
367  $blockId = $block->getId( $this->wikiId );
368 
369  if ( !$blockId ) {
370  throw new MWException(
371  __METHOD__ . " requires that a block id be set\n"
372  );
373  }
374  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
375 
376  $this->blockRestrictionStore->deleteByParentBlockId( $blockId );
377  $dbw->delete(
378  'ipblocks',
379  [ 'ipb_parent_block_id' => $blockId ],
380  __METHOD__
381  );
382 
383  $this->blockRestrictionStore->deleteByBlockId( $blockId );
384  $dbw->delete(
385  'ipblocks',
386  [ 'ipb_id' => $blockId ],
387  __METHOD__
388  );
389 
390  return $dbw->affectedRows() > 0;
391  }
392 
401  private function getArrayForDatabaseBlock(
402  DatabaseBlock $block,
403  IDatabase $dbw
404  ): array {
405  $expiry = $dbw->encodeExpiry( $block->getExpiry() );
406 
407  if ( $block->getTargetUserIdentity() ) {
408  $userId = $block->getTargetUserIdentity()->getId( $this->wikiId );
409  } else {
410  $userId = 0;
411  }
412  $blocker = $block->getBlocker();
413  if ( !$blocker ) {
414  throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
415  }
416  // DatabaseBlockStore supports inserting cross-wiki blocks by passing non-local IDatabase and blocker.
417  $blockerActor = $this->actorStoreFactory
418  ->getActorStore( $dbw->getDomainID() )
419  ->acquireActorId( $blocker, $dbw );
420 
421  $blockArray = [
422  'ipb_address' => $block->getTargetName(),
423  'ipb_user' => $userId,
424  'ipb_by_actor' => $blockerActor,
425  'ipb_timestamp' => $dbw->timestamp( $block->getTimestamp() ),
426  'ipb_auto' => $block->getType() === AbstractBlock::TYPE_AUTO,
427  'ipb_anon_only' => !$block->isHardblock(),
428  'ipb_create_account' => $block->isCreateAccountBlocked(),
429  'ipb_enable_autoblock' => $block->isAutoblocking(),
430  'ipb_expiry' => $expiry,
431  'ipb_range_start' => $block->getRangeStart(),
432  'ipb_range_end' => $block->getRangeEnd(),
433  'ipb_deleted' => intval( $block->getHideName() ), // typecast required for SQLite
434  'ipb_block_email' => $block->isEmailBlocked(),
435  'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
436  'ipb_parent_block_id' => $block->getParentBlockId(),
437  'ipb_sitewide' => $block->isSitewide(),
438  ];
439  $commentArray = $this->commentStore->insert(
440  $dbw,
441  'ipb_reason',
442  $block->getReasonComment()
443  );
444 
445  $combinedArray = $blockArray + $commentArray;
446  return $combinedArray;
447  }
448 
455  private function getArrayForAutoblockUpdate( DatabaseBlock $block ): array {
456  $blocker = $block->getBlocker();
457  if ( !$blocker ) {
458  throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
459  }
460  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY, [], $this->wikiId );
461  $blockerActor = $this->actorStoreFactory
462  ->getActorNormalization( $this->wikiId )
463  ->acquireActorId( $blocker, $dbw );
464  $blockArray = [
465  'ipb_by_actor' => $blockerActor,
466  'ipb_create_account' => $block->isCreateAccountBlocked(),
467  'ipb_deleted' => (int)$block->getHideName(), // typecast required for SQLite
468  'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
469  'ipb_sitewide' => $block->isSitewide(),
470  ];
471 
472  $commentArray = $this->commentStore->insert(
473  $dbw,
474  'ipb_reason',
475  $block->getReasonComment()
476  );
477 
478  $combinedArray = $blockArray + $commentArray;
479  return $combinedArray;
480  }
481 
489  private function doRetroactiveAutoblock( DatabaseBlock $block ): array {
490  $autoBlockIds = [];
491  // If autoblock is enabled, autoblock the LAST IP(s) used
492  if ( $block->isAutoblocking() && $block->getType() == AbstractBlock::TYPE_USER ) {
493  $this->logger->debug(
494  'Doing retroactive autoblocks for ' . $block->getTargetName()
495  );
496 
497  $hookAutoBlocked = [];
498  $continue = $this->hookRunner->onPerformRetroactiveAutoblock(
499  $block,
500  $hookAutoBlocked
501  );
502 
503  if ( $continue ) {
504  $coreAutoBlocked = $this->performRetroactiveAutoblock( $block );
505  $autoBlockIds = array_merge( $hookAutoBlocked, $coreAutoBlocked );
506  } else {
507  $autoBlockIds = $hookAutoBlocked;
508  }
509  }
510  return $autoBlockIds;
511  }
512 
520  private function performRetroactiveAutoblock( DatabaseBlock $block ): array {
521  if ( !$this->options->get( MainConfigNames::PutIPinRC ) ) {
522  // No IPs in the recent changes table to autoblock
523  return [];
524  }
525 
526  $type = $block->getType();
527  if ( $type !== AbstractBlock::TYPE_USER ) {
528  // Autoblocks only apply to users
529  return [];
530  }
531 
532  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA, [], $this->wikiId );
533 
534  $targetUser = $block->getTargetUserIdentity();
535  $actor = $targetUser ? $this->actorStoreFactory
536  ->getActorNormalization( $this->wikiId )
537  ->findActorId( $targetUser, $dbr ) : null;
538 
539  if ( !$actor ) {
540  $this->logger->debug( 'No actor found to retroactively autoblock' );
541  return [];
542  }
543 
544  $rcIp = $dbr->selectField(
545  [ 'recentchanges' ],
546  'rc_ip',
547  [ 'rc_actor' => $actor ],
548  __METHOD__,
549  [ 'ORDER BY' => 'rc_timestamp DESC' ]
550  );
551 
552  if ( !$rcIp ) {
553  $this->logger->debug( 'No IP found to retroactively autoblock' );
554  return [];
555  }
556 
557  $id = $block->doAutoblock( $rcIp );
558  if ( !$id ) {
559  return [];
560  }
561  return [ $id ];
562  }
563 
564 }
$success
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
Deferrable Update for closure/callback updates that should use auto-commit mode.
Class for managing the deferral of updates within the scope of a PHP script invocation.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
MediaWiki exception.
Definition: MWException.php:32
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:38
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:36
delete( $table, $conds, $fname=__METHOD__)
Delete all rows in a table that match a condition.
This class is a delegate to ILBFactory for a given database cluster.
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
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