MediaWiki REL1_37
BlockManager.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Block;
22
23use DateTime;
24use DateTimeZone;
25use LogicException;
32use Message;
33use MWCryptHash;
34use Psr\Log\LoggerInterface;
35use User;
36use WebRequest;
37use WebResponse;
38use Wikimedia\IPSet;
39use Wikimedia\IPUtils;
40
50
52 private $userFactory;
53
55 private $options;
56
60 public const CONSTRUCTOR_OPTIONS = [
61 'ApplyIpBlocksToXff',
62 'CookieSetOnAutoblock',
63 'CookieSetOnIpBlock',
64 'DnsBlacklistUrls',
65 'EnableDnsBlacklist',
66 'ProxyList',
67 'ProxyWhitelist',
68 'SecretKey',
69 'SoftBlockRanges',
70 ];
71
73 private $logger;
74
76 private $hookRunner;
77
85 public function __construct(
89 LoggerInterface $logger,
90 HookContainer $hookContainer
91 ) {
92 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
93 $this->options = $options;
94 $this->permissionManager = $permissionManager;
95 $this->userFactory = $userFactory;
96 $this->logger = $logger;
97 $this->hookRunner = new HookRunner( $hookContainer );
98 }
99
132 public function getUserBlock(
133 UserIdentity $user,
134 $request,
135 $fromReplica,
136 $disableIpBlockExemptChecking = false
137 ) {
138 $fromPrimary = !$fromReplica;
139 $ip = null;
140
141 // If this is the global user, they may be affected by IP blocks (case #1),
142 // or they may be exempt (case #2). If affected, look for additional blocks
143 // against the IP address and referenced in a cookie.
144 $checkIpBlocks = $request &&
145 // Because calling getBlock within Autopromote leads back to here,
146 // thus causing a infinite recursion. We fix this by not checking for
147 // ipblock-exempt when calling getBlock within Autopromote.
148 // See T270145.
149 !$disableIpBlockExemptChecking &&
150 !$this->permissionManager->userHasRight( $user, 'ipblock-exempt' );
151
152 if ( $request && $checkIpBlocks ) {
153
154 // Case #1: checking the global user, including IP blocks
155 $ip = $request->getIP();
156 // TODO: remove dependency on DatabaseBlock (T221075)
157 $blocks = DatabaseBlock::newListFromTarget( $user, $ip, $fromPrimary );
158 $this->getAdditionalIpBlocks( $blocks, $request, !$user->isRegistered(), $fromPrimary );
159 $this->getCookieBlock( $blocks, $user, $request );
160
161 } else {
162
163 // Case #2: checking the global user, but they are exempt from IP blocks
164 // and cookie blocks, so we only check for a user account block.
165 // Case #3: checking whether another user's account is blocked.
166 // TODO: remove dependency on DatabaseBlock (T221075)
167 $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromPrimary );
168
169 }
170
171 // Filter out any duplicated blocks, e.g. from the cookie
172 $blocks = $this->getUniqueBlocks( $blocks );
173
174 $block = null;
175 if ( count( $blocks ) > 0 ) {
176 if ( count( $blocks ) === 1 ) {
177 $block = $blocks[ 0 ];
178 } else {
179 $block = new CompositeBlock( [
180 'address' => $ip,
181 'reason' => new Message( 'blockedtext-composite-reason' ),
182 'originalBlocks' => $blocks,
183 ] );
184 }
185 }
186
187 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
188 $this->hookRunner->onGetUserBlock( clone $legacyUser, $ip, $block );
189
190 return $block;
191 }
192
201 private function getCookieBlock( &$blocks, UserIdentity $user, WebRequest $request ) {
202 $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
203 if ( $cookieBlock instanceof DatabaseBlock ) {
204 $blocks[] = $cookieBlock;
205 }
206 }
207
217 private function getAdditionalIpBlocks( &$blocks, WebRequest $request, $isAnon, $fromPrimary ) {
218 $ip = $request->getIP();
219
220 // Proxy blocking
221 if ( !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) {
222 // Local list
223 if ( $this->isLocallyBlockedProxy( $ip ) ) {
224 $blocks[] = new SystemBlock( [
225 'reason' => new Message( 'proxyblockreason' ),
226 'address' => $ip,
227 'systemBlock' => 'proxy',
228 ] );
229 } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
230 $blocks[] = new SystemBlock( [
231 'reason' => new Message( 'sorbsreason' ),
232 'address' => $ip,
233 'anonOnly' => true,
234 'systemBlock' => 'dnsbl',
235 ] );
236 }
237 }
238
239 // Soft blocking
240 if ( $isAnon && IPUtils::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) ) {
241 $blocks[] = new SystemBlock( [
242 'address' => $ip,
243 'reason' => new Message( 'softblockrangesreason', [ $ip ] ),
244 'anonOnly' => true,
245 'systemBlock' => 'wgSoftBlockRanges',
246 ] );
247 }
248
249 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
250 if ( $this->options->get( 'ApplyIpBlocksToXff' )
251 && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
252 ) {
253 $xff = $request->getHeader( 'X-Forwarded-For' );
254 $xff = array_map( 'trim', explode( ',', $xff ) );
255 $xff = array_diff( $xff, [ $ip ] );
256 // TODO: remove dependency on DatabaseBlock (T221075)
257 $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromPrimary );
258 $blocks = array_merge( $blocks, $xffblocks );
259 }
260 }
261
272 private function getUniqueBlocks( array $blocks ) {
273 $systemBlocks = [];
274 $databaseBlocks = [];
275
276 foreach ( $blocks as $block ) {
277 if ( $block instanceof SystemBlock ) {
278 $systemBlocks[] = $block;
279 } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
281 '@phan-var DatabaseBlock $block';
282 if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
283 $databaseBlocks[$block->getParentBlockId()] = $block;
284 }
285 } else {
286 $databaseBlocks[$block->getId()] = $block;
287 }
288 }
289
290 return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
291 }
292
304 private function getBlockFromCookieValue(
305 UserIdentity $user,
306 WebRequest $request
307 ) {
308 $cookieValue = $request->getCookie( 'BlockID' );
309 if ( $cookieValue === null ) {
310 return false;
311 }
312
313 $blockCookieId = $this->getIdFromCookieValue( $cookieValue );
314 if ( $blockCookieId !== null ) {
315 // TODO: remove dependency on DatabaseBlock (T221075)
316 $block = DatabaseBlock::newFromID( $blockCookieId );
317 if (
318 $block instanceof DatabaseBlock &&
319 $this->shouldApplyCookieBlock( $block, !$user->isRegistered() )
320 ) {
321 return $block;
322 }
323 }
324
325 return false;
326 }
327
335 private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
336 if ( !$block->isExpired() ) {
337 switch ( $block->getType() ) {
340 // If block is type IP or IP range, load only
341 // if user is not logged in (T152462)
342 return $isAnon &&
343 $this->options->get( 'CookieSetOnIpBlock' );
345 return $block->isAutoblocking() &&
346 $this->options->get( 'CookieSetOnAutoblock' );
347 default:
348 return false;
349 }
350 }
351 return false;
352 }
353
360 private function isLocallyBlockedProxy( $ip ) {
361 $proxyList = $this->options->get( 'ProxyList' );
362 if ( !$proxyList ) {
363 return false;
364 }
365
366 if ( !is_array( $proxyList ) ) {
367 // Load values from the specified file
368 $proxyList = array_map( 'trim', file( $proxyList ) );
369 }
370
371 $proxyListIPSet = new IPSet( $proxyList );
372 return $proxyListIPSet->match( $ip );
373 }
374
382 public function isDnsBlacklisted( $ip, $checkAllowed = false ) {
383 if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
384 ( $checkAllowed && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
385 ) {
386 return false;
387 }
388
389 return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
390 }
391
399 private function inDnsBlacklist( $ip, array $bases ) {
400 $found = false;
401 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
402 if ( IPUtils::isIPv4( $ip ) ) {
403 // Reverse IP, T23255
404 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
405
406 foreach ( $bases as $base ) {
407 // Make hostname
408 // If we have an access key, use that too (ProjectHoneypot, etc.)
409 $basename = $base;
410 if ( is_array( $base ) ) {
411 if ( count( $base ) >= 2 ) {
412 // Access key is 1, base URL is 0
413 $hostname = "{$base[1]}.$ipReversed.{$base[0]}";
414 } else {
415 $hostname = "$ipReversed.{$base[0]}";
416 }
417 $basename = $base[0];
418 } else {
419 $hostname = "$ipReversed.$base";
420 }
421
422 // Send query
423 $ipList = $this->checkHost( $hostname );
424
425 if ( $ipList ) {
426 $this->logger->info(
427 'Hostname {hostname} is {ipList}, it\'s a proxy says {basename}!',
428 [
429 'hostname' => $hostname,
430 'ipList' => $ipList[0],
431 'basename' => $basename,
432 ]
433 );
434 $found = true;
435 break;
436 }
437
438 $this->logger->debug( "Requested $hostname, not found in $basename." );
439 }
440 }
441
442 return $found;
443 }
444
451 protected function checkHost( $hostname ) {
452 return gethostbynamel( $hostname );
453 }
454
474 public function trackBlockWithCookie( User $user, WebResponse $response ) {
475 $request = $user->getRequest();
476
477 if ( $request->getCookie( 'BlockID' ) !== null ) {
478 $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
479 if ( $cookieBlock && $this->shouldApplyCookieBlock( $cookieBlock, $user->isAnon() ) ) {
480 return;
481 }
482 // The block pointed to by the cookie is invalid or should not be tracked.
483 $this->clearBlockCookie( $response );
484 }
485
486 if ( !$user->isSafeToLoad() ) {
487 // Prevent a circular dependency by not allowing this method to be called
488 // before or while the user is being loaded.
489 // E.g. User > BlockManager > Block > Message > getLanguage > User.
490 // See also T180050 and T226777.
491 throw new LogicException( __METHOD__ . ' requires a loaded User object' );
492 }
493 if ( $response->headersSent() ) {
494 throw new LogicException( __METHOD__ . ' must be called pre-send' );
495 }
496
497 $block = $user->getBlock();
498 $isAnon = $user->isAnon();
499
500 if ( $block ) {
501 if ( $block instanceof CompositeBlock ) {
502 // TODO: Improve on simply tracking the first trackable block (T225654)
503 foreach ( $block->getOriginalBlocks() as $originalBlock ) {
504 if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
505 '@phan-var DatabaseBlock $originalBlock';
506 $this->setBlockCookie( $originalBlock, $response );
507 return;
508 }
509 }
510 } else {
511 if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
512 '@phan-var DatabaseBlock $block';
513 $this->setBlockCookie( $block, $response );
514 }
515 }
516 }
517 }
518
529 public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) {
530 // Calculate the default expiry time.
531 $maxExpiryTime = wfTimestamp( TS_MW, (int)wfTimestamp() + ( 24 * 60 * 60 ) );
532
533 // Use the block's expiry time only if it's less than the default.
534 $expiryTime = $block->getExpiry();
535 if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
536 $expiryTime = $maxExpiryTime;
537 }
538
539 // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
540 $expiryValue = DateTime::createFromFormat(
541 'YmdHis',
542 $expiryTime,
543 new DateTimeZone( 'UTC' )
544 )->format( 'U' );
545 $cookieOptions = [ 'httpOnly' => false ];
546 $cookieValue = $this->getCookieValue( $block );
547 $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
548 }
549
557 private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
558 if ( $block instanceof DatabaseBlock ) {
559 switch ( $block->getType() ) {
562 return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
564 return !$isAnon &&
565 $this->options->get( 'CookieSetOnAutoblock' ) &&
566 $block->isAutoblocking();
567 default:
568 return false;
569 }
570 }
571 return false;
572 }
573
580 public static function clearBlockCookie( WebResponse $response ) {
581 $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
582 }
583
594 public function getIdFromCookieValue( $cookieValue ) {
595 // The cookie value must start with a number
596 if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
597 return null;
598 }
599
600 // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
601 $bangPos = strpos( $cookieValue, '!' );
602 $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
603 if ( !$this->options->get( 'SecretKey' ) ) {
604 // If there's no secret key, just use the ID as given.
605 return (int)$id;
606 }
607 $storedHmac = substr( $cookieValue, $bangPos + 1 );
608 $calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
609 if ( $calculatedHmac === $storedHmac ) {
610 return (int)$id;
611 } else {
612 return null;
613 }
614 }
615
627 public function getCookieValue( DatabaseBlock $block ) {
628 $id = $block->getId();
629 if ( !$this->options->get( 'SecretKey' ) ) {
630 // If there's no secret key, don't append a HMAC.
631 return $id;
632 }
633 $hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
634 $cookieValue = $id . '!' . $hmac;
635 return $cookieValue;
636 }
637
638}
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static hmac( $data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
getType()
Get the type of target for this particular block.
getExpiry()
Get the block expiry time.
A service class for checking blocks.
PermissionManager $permissionManager
trackBlockWithCookie(User $user, WebResponse $response)
Set the 'BlockID' cookie depending on block type and user authentication status.
getCookieBlock(&$blocks, UserIdentity $user, WebRequest $request)
Get the cookie block, if there is one.
inDnsBlacklist( $ip, array $bases)
Whether the given IP is in a given DNS blacklist.
isDnsBlacklisted( $ip, $checkAllowed=false)
Whether the given IP is in a DNS blacklist.
isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
getAdditionalIpBlocks(&$blocks, WebRequest $request, $isAnon, $fromPrimary)
Check for any additional blocks against the IP address or any IPs in the XFF header.
setBlockCookie(DatabaseBlock $block, WebResponse $response)
Set the 'BlockID' cookie to this block's ID and expiry time.
__construct(ServiceOptions $options, PermissionManager $permissionManager, UserFactory $userFactory, LoggerInterface $logger, HookContainer $hookContainer)
shouldTrackBlockWithCookie(AbstractBlock $block, $isAnon)
Check if the block should be tracked with a cookie.
getUniqueBlocks(array $blocks)
Given a list of blocks, return a list of unique blocks.
checkHost( $hostname)
Wrapper for mocking in tests.
getIdFromCookieValue( $cookieValue)
Get the stored ID from the 'BlockID' cookie.
getUserBlock(UserIdentity $user, $request, $fromReplica, $disableIpBlockExemptChecking=false)
Get the blocks that apply to a user.
static clearBlockCookie(WebResponse $response)
Unset the 'BlockID' cookie.
getCookieValue(DatabaseBlock $block)
Get the BlockID cookie's value for this block.
getBlockFromCookieValue(UserIdentity $user, WebRequest $request)
Try to load a block from an ID given in a cookie value.
shouldApplyCookieBlock(DatabaseBlock $block, $isAnon)
Check if the block loaded from the cookie should be applied.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
static newListFromTarget( $specificTarget, $vagueTarget=null, $fromPrimary=false)
This is similar to DatabaseBlock::newFromTarget, but it returns all the relevant blocks.
getId()
Get the block ID.int|null
isExpired()
Has the block expired?
static newFromID( $id)
Load a block from the block id.
static getBlocksForIPList(array $ipChain, $isAnon, $fromPrimary=false)
Get all blocks that match any IP from an array of IP addresses.
getType()
Get the type of target for this particular block.int|null AbstractBlock::TYPE_ constant,...
System blocks are temporary blocks that are created on enforcement (e.g.
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 service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Creates User objects.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:138
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
getBlock( $freshness=self::READ_NORMAL, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:1941
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:3075
isSafeToLoad()
Test if it's safe to load this User object.
Definition User.php:362
isAnon()
Get whether the user is anonymous.
Definition User.php:2986
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
getHeader( $name, $flags=0)
Get a request header, or false if it isn't set.
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
setCookie( $name, $value, $expire=0, $options=[])
Set the browser cookie.
clearCookie( $name, $options=[])
Unset a browser cookie.
headersSent()
Test if headers have been sent.
Interface for objects representing user identity.