MediaWiki REL1_37
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 = [
56 'PutIPinRC',
57 'BlockDisablesLogin',
58 ];
59
61 private $logger;
62
65
68
71
73 private $hookRunner;
74
77
80
82 private $userFactory;
83
95 public function __construct(
97 LoggerInterface $logger,
101 HookContainer $hookContainer,
105 ) {
106 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
107
108 $this->options = $options;
109 $this->logger = $logger;
110 $this->actorStoreFactory = $actorStoreFactory;
111 $this->blockRestrictionStore = $blockRestrictionStore;
112 $this->commentStore = $commentStore;
113 $this->hookRunner = new HookRunner( $hookContainer );
114 $this->loadBalancer = $loadBalancer;
115 $this->readOnlyMode = $readOnlyMode;
116 $this->userFactory = $userFactory;
117 }
118
124 public function purgeExpiredBlocks() {
125 if ( $this->readOnlyMode->isReadOnly() ) {
126 return;
127 }
128
129 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
131
132 DeferredUpdates::addUpdate( new AutoCommitUpdate(
133 $dbw,
134 __METHOD__,
135 static function ( IDatabase $dbw, $fname ) use ( $blockRestrictionStore ) {
136 $ids = $dbw->selectFieldValues(
137 'ipblocks',
138 'ipb_id',
139 [ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
140 $fname
141 );
142 if ( $ids ) {
144 $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
145 }
146 }
147 ) );
148 }
149
157 private function checkDatabaseDomain( ?IDatabase $db, $expectedWiki ) {
158 if ( $db ) {
159 $dbDomain = $db->getDomainID();
160 $storeDomain = $this->loadBalancer->resolveDomainID( $expectedWiki );
161 if ( $dbDomain !== $storeDomain ) {
162 throw new InvalidArgumentException(
163 "DB connection domain '$dbDomain' does not match '$storeDomain'"
164 );
165 }
166 } else {
167 if ( $expectedWiki !== UserIdentity::LOCAL ) {
168 throw new InvalidArgumentException(
169 "Must provide a database connection for wiki '$expectedWiki'."
170 );
171 }
172 }
173 }
174
186 public function insertBlock(
187 DatabaseBlock $block,
188 IDatabase $database = null
189 ) {
190 if ( !$block->getBlocker() || $block->getBlocker()->getName() === '' ) {
191 throw new MWException( 'Cannot insert a block without a blocker set' );
192 }
193
194 $this->checkDatabaseDomain( $database, $block->getBlocker()->getWikiId() );
195
196 $this->logger->debug( 'Inserting block; timestamp ' . $block->getTimestamp() );
197
198 // TODO T258866 - consider passing the database
199 $this->purgeExpiredBlocks();
200
201 $dbw = $database ?: $this->loadBalancer->getConnectionRef( DB_PRIMARY );
202 $row = $this->getArrayForDatabaseBlock( $block, $dbw );
203
204 $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
205 $affected = $dbw->affectedRows();
206
207 if ( $affected ) {
208 $block->setId( $dbw->insertId() );
209 if ( $block->getRawRestrictions() ) {
210 $this->blockRestrictionStore->insert( $block->getRawRestrictions() );
211 }
212 }
213
214 // Don't collide with expired blocks.
215 // Do this after trying to insert to avoid locking.
216 if ( !$affected ) {
217 // T96428: The ipb_address index uses a prefix on a field, so
218 // use a standard SELECT + DELETE to avoid annoying gap locks.
219 $ids = $dbw->selectFieldValues(
220 'ipblocks',
221 'ipb_id',
222 [
223 'ipb_address' => $row['ipb_address'],
224 'ipb_user' => $row['ipb_user'],
225 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
226 ],
227 __METHOD__
228 );
229 if ( $ids ) {
230 $dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
231 $this->blockRestrictionStore->deleteByBlockId( $ids );
232 $dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
233 $affected = $dbw->affectedRows();
234 $block->setId( $dbw->insertId() );
235 if ( $block->getRawRestrictions() ) {
236 $this->blockRestrictionStore->insert( $block->getRawRestrictions() );
237 }
238 }
239 }
240
241 if ( $affected ) {
242 $autoBlockIds = $this->doRetroactiveAutoblock( $block );
243
244 if ( $this->options->get( 'BlockDisablesLogin' ) ) {
245 $targetUserIdentity = $block->getTargetUserIdentity();
246 if ( $targetUserIdentity ) {
247 $targetUser = $this->userFactory->newFromUserIdentity( $targetUserIdentity );
248 // Change user login token to force them to be logged out.
249 $targetUser->setToken();
250 $targetUser->saveSettings();
251 }
252 }
253
254 return [ 'id' => $block->getId(), 'autoIds' => $autoBlockIds ];
255 }
256
257 return false;
258 }
259
268 public function updateBlock( DatabaseBlock $block ) {
269 $this->logger->debug( 'Updating block; timestamp ' . $block->getTimestamp() );
270
271 // We could allow cross-wiki updates here, just like we do in insertBlock().
272 Assert::parameter(
273 $block->getBlocker()->getWikiId() === UserIdentity::LOCAL,
274 '$block->getBlocker()',
275 'must belong to the local wiki.'
276 );
277
278 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
279 $row = $this->getArrayForDatabaseBlock( $block, $dbw );
280 $dbw->startAtomic( __METHOD__ );
281
282 $result = $dbw->update(
283 'ipblocks',
284 $row,
285 [ 'ipb_id' => $block->getId() ],
286 __METHOD__
287 );
288
289 // Only update the restrictions if they have been modified.
290 $restrictions = $block->getRawRestrictions();
291 if ( $restrictions !== null ) {
292 // An empty array should remove all of the restrictions.
293 if ( empty( $restrictions ) ) {
294 $success = $this->blockRestrictionStore->deleteByBlockId( $block->getId() );
295 } else {
296 $success = $this->blockRestrictionStore->update( $restrictions );
297 }
298 // Update the result. The first false is the result, otherwise, true.
299 $result = $result && $success;
300 }
301
302 if ( $block->isAutoblocking() ) {
303 // update corresponding autoblock(s) (T50813)
304 $dbw->update(
305 'ipblocks',
306 $this->getArrayForAutoblockUpdate( $block ),
307 [ 'ipb_parent_block_id' => $block->getId() ],
308 __METHOD__
309 );
310
311 // Only update the restrictions if they have been modified.
312 if ( $restrictions !== null ) {
313 $this->blockRestrictionStore->updateByParentBlockId(
314 $block->getId(),
315 $restrictions
316 );
317 }
318 } else {
319 // autoblock no longer required, delete corresponding autoblock(s)
320 $this->blockRestrictionStore->deleteByParentBlockId( $block->getId() );
321 $dbw->delete(
322 'ipblocks',
323 [ 'ipb_parent_block_id' => $block->getId() ],
324 __METHOD__
325 );
326 }
327
328 $dbw->endAtomic( __METHOD__ );
329
330 if ( $result ) {
331 $autoBlockIds = $this->doRetroactiveAutoblock( $block );
332 return [ 'id' => $block->getId(), 'autoIds' => $autoBlockIds ];
333 }
334
335 return false;
336 }
337
345 public function deleteBlock( DatabaseBlock $block ): bool {
346 if ( $this->readOnlyMode->isReadOnly() ) {
347 return false;
348 }
349
350 $blockId = $block->getId();
351
352 if ( !$blockId ) {
353 throw new MWException(
354 __METHOD__ . " requires that a block id be set\n"
355 );
356 }
357 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
358
359 $this->blockRestrictionStore->deleteByParentBlockId( $blockId );
360 $dbw->delete(
361 'ipblocks',
362 [ 'ipb_parent_block_id' => $blockId ],
363 __METHOD__
364 );
365
366 $this->blockRestrictionStore->deleteByBlockId( $blockId );
367 $dbw->delete(
368 'ipblocks',
369 [ 'ipb_id' => $blockId ],
370 __METHOD__
371 );
372
373 return $dbw->affectedRows() > 0;
374 }
375
384 private function getArrayForDatabaseBlock(
385 DatabaseBlock $block,
386 IDatabase $dbw
387 ): array {
388 $expiry = $dbw->encodeExpiry( $block->getExpiry() );
389
390 if ( $block->getTargetUserIdentity() ) {
391 $userId = $block->getTargetUserIdentity()->getId();
392 } else {
393 $userId = 0;
394 }
395 if ( !$block->getBlocker() ) {
396 throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
397 }
398 // DatabaseBlockStore supports inserting cross-wiki blocks by passing non-local IDatabase and blocker.
399 $blockerActor = $this->actorStoreFactory
400 ->getActorStore( $dbw->getDomainID() )
401 ->acquireActorId( $block->getBlocker(), $dbw );
402
403 $blockArray = [
404 'ipb_address' => $block->getTargetName(),
405 'ipb_user' => $userId,
406 'ipb_by_actor' => $blockerActor,
407 'ipb_timestamp' => $dbw->timestamp( $block->getTimestamp() ),
408 'ipb_auto' => $block->getType() === AbstractBlock::TYPE_AUTO,
409 'ipb_anon_only' => !$block->isHardblock(),
410 'ipb_create_account' => $block->isCreateAccountBlocked(),
411 'ipb_enable_autoblock' => $block->isAutoblocking(),
412 'ipb_expiry' => $expiry,
413 'ipb_range_start' => $block->getRangeStart(),
414 'ipb_range_end' => $block->getRangeEnd(),
415 'ipb_deleted' => intval( $block->getHideName() ), // typecast required for SQLite
416 'ipb_block_email' => $block->isEmailBlocked(),
417 'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
418 'ipb_parent_block_id' => $block->getParentBlockId(),
419 'ipb_sitewide' => $block->isSitewide(),
420 ];
421 $commentArray = $this->commentStore->insert(
422 $dbw,
423 'ipb_reason',
424 $block->getReasonComment()
425 );
426
427 $combinedArray = $blockArray + $commentArray;
428 return $combinedArray;
429 }
430
437 private function getArrayForAutoblockUpdate( DatabaseBlock $block ): array {
438 if ( !$block->getBlocker() ) {
439 throw new \RuntimeException( __METHOD__ . ': this block does not have a blocker' );
440 }
441 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
442 $blockerActor = $this->actorStoreFactory
443 ->getActorNormalization()
444 ->acquireActorId( $block->getBlocker(), $dbw );
445 $blockArray = [
446 'ipb_by_actor' => $blockerActor,
447 'ipb_create_account' => $block->isCreateAccountBlocked(),
448 'ipb_deleted' => (int)$block->getHideName(), // typecast required for SQLite
449 'ipb_allow_usertalk' => $block->isUsertalkEditAllowed(),
450 'ipb_sitewide' => $block->isSitewide(),
451 ];
452
453 $commentArray = $this->commentStore->insert(
454 $dbw,
455 'ipb_reason',
456 $block->getReasonComment()
457 );
458
459 $combinedArray = $blockArray + $commentArray;
460 return $combinedArray;
461 }
462
470 private function doRetroactiveAutoblock( DatabaseBlock $block ): array {
471 $autoBlockIds = [];
472 // If autoblock is enabled, autoblock the LAST IP(s) used
473 if ( $block->isAutoblocking() && $block->getType() == AbstractBlock::TYPE_USER ) {
474 $this->logger->debug(
475 'Doing retroactive autoblocks for ' . $block->getTargetName()
476 );
477
478 $hookAutoBlocked = [];
479 $continue = $this->hookRunner->onPerformRetroactiveAutoblock(
480 $block,
481 $hookAutoBlocked
482 );
483
484 if ( $continue ) {
485 $coreAutoBlocked = $this->performRetroactiveAutoblock( $block );
486 $autoBlockIds = array_merge( $hookAutoBlocked, $coreAutoBlocked );
487 } else {
488 $autoBlockIds = $hookAutoBlocked;
489 }
490 }
491 return $autoBlockIds;
492 }
493
501 private function performRetroactiveAutoblock( DatabaseBlock $block ): array {
502 if ( !$this->options->get( 'PutIPinRC' ) ) {
503 // No IPs in the recent changes table to autoblock
504 return [];
505 }
506
507 $type = $block->getType();
508 if ( $type !== AbstractBlock::TYPE_USER ) {
509 // Autoblocks only apply to users
510 return [];
511 }
512
513 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
514
515 $targetUser = $block->getTargetUserIdentity();
516 $actor = $targetUser ? $this->actorStoreFactory->getActorNormalization()->findActorId(
517 $targetUser,
518 $dbr
519 ) : null;
520
521 if ( !$actor ) {
522 $this->logger->debug( 'No actor found to retroactively autoblock' );
523 return [];
524 }
525
526 $rcIp = $dbr->selectField(
527 [ 'recentchanges' ],
528 'rc_ip',
529 [ 'rc_actor' => $actor ],
530 __METHOD__,
531 [ 'ORDER BY' => 'rc_timestamp DESC' ]
532 );
533
534 if ( !$rcIp ) {
535 $this->logger->debug( 'No IP found to retroactively autoblock' );
536 return [];
537 }
538
539 $id = $block->doAutoblock( $rcIp );
540 if ( !$id ) {
541 return [];
542 }
543 return [ $id ];
544 }
545
546}
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
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.
isUsertalkEditAllowed( $x=null)
Get or set the flag indicating whether this block blocks the target from editing their own user talk ...
getHideName()
Get whether the block hides the target's username.
isCreateAccountBlocked( $x=null)
Get or set the flag indicating whether this block blocks the target from creating an account.
getTimestamp()
Get the timestamp indicating when the block was created.
getReasonComment()
Get the reason for creating the block.
isEmailBlocked( $x=null)
Get or set the flag indicating whether this block blocks the target from sending emails.
isSitewide( $x=null)
Indicates that the block is a sitewide block.
deleteByBlockId( $blockId)
Delete the restrictions by block ID.
BlockRestrictionStore $blockRestrictionStore
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.
getArrayForAutoblockUpdate(DatabaseBlock $block)
Get an array suitable for autoblock updates.
deleteBlock(DatabaseBlock $block)
Delete a DatabaseBlock from the database.
doRetroactiveAutoblock(DatabaseBlock $block)
Handles retroactively autoblocking the last IP used by the user (if it is a user) blocked by an auto ...
purgeExpiredBlocks()
Delete expired blocks from the ipblocks table.
performRetroactiveAutoblock(DatabaseBlock $block)
Actually retroactively autoblocks the last IP used by the user (if it is a user) blocked by this bloc...
checkDatabaseDomain(?IDatabase $db, $expectedWiki)
Throws an exception if the given database connection does not match the given wiki ID.
getArrayForDatabaseBlock(DatabaseBlock $block, IDatabase $dbw)
Get an array suitable for passing to $dbw->insert() or $dbw->update()
__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...
getRangeStart()
Get the IP address at the start of the range in Hex form.
setId( $blockId)
Set the block ID.
getId()
Get the block ID.int|null
isHardblock( $x=null)
Get/set whether the block is a hardblock (affects logged-in users on a given IP/range)
getRangeEnd()
Get the IP address at the end of the range in Hex form.
getBlocker()
Get the user who implemented this block.
getRawRestrictions()
Get restrictions without loading from database if not yet loaded.
doAutoblock( $autoblockIP)
Autoblocks the given IP, referring to this block.
getType()
Get the type of target for this particular block.int|null AbstractBlock::TYPE_ constant,...
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...
Creates User objects.
A service class for fetching the wiki's current read-only mode.
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
getDomainID()
Return the currently selected domain ID.
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
affectedRows()
Get the number of rows affected by the last write query.
delete( $table, $conds, $fname=__METHOD__)
Delete all rows in a table that match a condition.
update( $table, $set, $conds, $fname=__METHOD__, $options=[])
Update all rows in a table that match a given condition.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
insertId()
Get the inserted value of an auto-increment row.
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27