MediaWiki REL1_35
BlockManager.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Block;
22
23use DateTime;
24use DateTimeZone;
25use LogicException;
31use Message;
32use MWCryptHash;
33use Psr\Log\LoggerInterface;
34use User;
35use WebRequest;
36use WebResponse;
37use Wikimedia\IPSet;
38use Wikimedia\IPUtils;
39
49
51 private $options;
52
57 public const CONSTRUCTOR_OPTIONS = [
58 'ApplyIpBlocksToXff',
59 'CookieSetOnAutoblock',
60 'CookieSetOnIpBlock',
61 'DnsBlacklistUrls',
62 'EnableDnsBlacklist',
63 'ProxyList',
64 'ProxyWhitelist',
65 'SecretKey',
66 'SoftBlockRanges',
67 ];
68
70 private $logger;
71
73 private $hookRunner;
74
81 public function __construct(
84 LoggerInterface $logger,
85 HookContainer $hookContainer
86 ) {
87 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
88 $this->options = $options;
89 $this->permissionManager = $permissionManager;
90 $this->logger = $logger;
91 $this->hookRunner = new HookRunner( $hookContainer );
92 }
93
126 public function getUserBlock( User $user, $request, $fromReplica, $disableIpBlockExemptChecking = false ) {
127 $fromMaster = !$fromReplica;
128 $ip = null;
129
130 // If this is the global user, they may be affected by IP blocks (case #1),
131 // or they may be exempt (case #2). If affected, look for additional blocks
132 // against the IP address and referenced in a cookie.
133 $checkIpBlocks = $request &&
134 // Because calling getBlock within Autopromote leads back to here,
135 // thus causing a infinite recursion. We fix this by not checking for
136 // ipblock-exempt when calling getBlock within Autopromote.
137 // See T270145.
138 !$disableIpBlockExemptChecking &&
139 !$this->permissionManager->userHasRight( $user, 'ipblock-exempt' );
140
141 if ( $request && $checkIpBlocks ) {
142
143 // Case #1: checking the global user, including IP blocks
144 $ip = $request->getIP();
145 // TODO: remove dependency on DatabaseBlock (T221075)
146 $blocks = DatabaseBlock::newListFromTarget( $user, $ip, $fromMaster );
147 $this->getAdditionalIpBlocks( $blocks, $request, !$user->isRegistered(), $fromMaster );
148 $this->getCookieBlock( $blocks, $user, $request );
149
150 } else {
151
152 // Case #2: checking the global user, but they are exempt from IP blocks
153 // and cookie blocks, so we only check for a user account block.
154 // Case #3: checking whether another user's account is blocked.
155 // TODO: remove dependency on DatabaseBlock (T221075)
156 $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster );
157
158 }
159
160 // Filter out any duplicated blocks, e.g. from the cookie
161 $blocks = $this->getUniqueBlocks( $blocks );
162
163 $block = null;
164 if ( count( $blocks ) > 0 ) {
165 if ( count( $blocks ) === 1 ) {
166 $block = $blocks[ 0 ];
167 } else {
168 $block = new CompositeBlock( [
169 'address' => $ip,
170 'reason' => new Message( 'blockedtext-composite-reason' ),
171 'originalBlocks' => $blocks,
172 ] );
173 }
174 }
175
176 $this->hookRunner->onGetUserBlock( clone $user, $ip, $block );
177
178 return $block;
179 }
180
189 private function getCookieBlock( &$blocks, UserIdentity $user, WebRequest $request ) {
190 $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
191 if ( $cookieBlock instanceof DatabaseBlock ) {
192 $blocks[] = $cookieBlock;
193 }
194 }
195
205 private function getAdditionalIpBlocks( &$blocks, WebRequest $request, $isAnon, $fromMaster ) {
206 $ip = $request->getIP();
207
208 // Proxy blocking
209 if ( !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) {
210 // Local list
211 if ( $this->isLocallyBlockedProxy( $ip ) ) {
212 $blocks[] = new SystemBlock( [
213 'reason' => new Message( 'proxyblockreason' ),
214 'address' => $ip,
215 'systemBlock' => 'proxy',
216 ] );
217 } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
218 $blocks[] = new SystemBlock( [
219 'reason' => new Message( 'sorbsreason' ),
220 'address' => $ip,
221 'systemBlock' => 'dnsbl',
222 ] );
223 }
224 }
225
226 // Soft blocking
227 if ( $isAnon && IPUtils::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) ) {
228 $blocks[] = new SystemBlock( [
229 'address' => $ip,
230 'reason' => new Message( 'softblockrangesreason', [ $ip ] ),
231 'anonOnly' => true,
232 'systemBlock' => 'wgSoftBlockRanges',
233 ] );
234 }
235
236 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
237 if ( $this->options->get( 'ApplyIpBlocksToXff' )
238 && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
239 ) {
240 $xff = $request->getHeader( 'X-Forwarded-For' );
241 $xff = array_map( 'trim', explode( ',', $xff ) );
242 $xff = array_diff( $xff, [ $ip ] );
243 // TODO: remove dependency on DatabaseBlock (T221075)
244 $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromMaster );
245
246 // (T285159) Exclude autoblocks from XFF headers to prevent spoofed
247 // headers uncovering the IPs of autoblocked users
248 $xffblocks = array_filter( $xffblocks, static function ( $block ) {
249 return $block->getType() !== AbstractBlock::TYPE_AUTO;
250 } );
251
252 $blocks = array_merge( $blocks, $xffblocks );
253 }
254 }
255
266 private function getUniqueBlocks( array $blocks ) {
267 $systemBlocks = [];
268 $databaseBlocks = [];
269
270 foreach ( $blocks as $block ) {
271 if ( $block instanceof SystemBlock ) {
272 $systemBlocks[] = $block;
273 } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
275 '@phan-var DatabaseBlock $block';
276 if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
277 $databaseBlocks[$block->getParentBlockId()] = $block;
278 }
279 } else {
280 $databaseBlocks[$block->getId()] = $block;
281 }
282 }
283
284 return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
285 }
286
298 private function getBlockFromCookieValue(
299 UserIdentity $user,
300 WebRequest $request
301 ) {
302 $cookieValue = $request->getCookie( 'BlockID' );
303 if ( $cookieValue === null ) {
304 return false;
305 }
306
307 $blockCookieId = $this->getIdFromCookieValue( $cookieValue );
308 if ( $blockCookieId !== null ) {
309 // TODO: remove dependency on DatabaseBlock (T221075)
310 $block = DatabaseBlock::newFromID( $blockCookieId );
311 if (
312 $block instanceof DatabaseBlock &&
313 $this->shouldApplyCookieBlock( $block, !$user->isRegistered() )
314 ) {
315 return $block;
316 }
317 }
318
319 return false;
320 }
321
329 private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
330 if ( !$block->isExpired() ) {
331 switch ( $block->getType() ) {
334 // If block is type IP or IP range, load only
335 // if user is not logged in (T152462)
336 return $isAnon &&
337 $this->options->get( 'CookieSetOnIpBlock' );
339 return $block->isAutoblocking() &&
340 $this->options->get( 'CookieSetOnAutoblock' );
341 default:
342 return false;
343 }
344 }
345 return false;
346 }
347
354 private function isLocallyBlockedProxy( $ip ) {
355 $proxyList = $this->options->get( 'ProxyList' );
356 if ( !$proxyList ) {
357 return false;
358 }
359
360 if ( !is_array( $proxyList ) ) {
361 // Load values from the specified file
362 $proxyList = array_map( 'trim', file( $proxyList ) );
363 }
364
365 $proxyListIPSet = new IPSet( $proxyList );
366 return $proxyListIPSet->match( $ip );
367 }
368
376 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
377 if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
378 ( $checkWhitelist && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
379 ) {
380 return false;
381 }
382
383 return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
384 }
385
393 private function inDnsBlacklist( $ip, array $bases ) {
394 $found = false;
395 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
396 if ( IPUtils::isIPv4( $ip ) ) {
397 // Reverse IP, T23255
398 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
399
400 foreach ( $bases as $base ) {
401 // Make hostname
402 // If we have an access key, use that too (ProjectHoneypot, etc.)
403 $basename = $base;
404 if ( is_array( $base ) ) {
405 if ( count( $base ) >= 2 ) {
406 // Access key is 1, base URL is 0
407 $hostname = "{$base[1]}.$ipReversed.{$base[0]}";
408 } else {
409 $hostname = "$ipReversed.{$base[0]}";
410 }
411 $basename = $base[0];
412 } else {
413 $hostname = "$ipReversed.$base";
414 }
415
416 // Send query
417 $ipList = $this->checkHost( $hostname );
418
419 if ( $ipList ) {
420 $this->logger->info(
421 "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!"
422 );
423 $found = true;
424 break;
425 }
426
427 $this->logger->debug( "Requested $hostname, not found in $basename." );
428 }
429 }
430
431 return $found;
432 }
433
440 protected function checkHost( $hostname ) {
441 return gethostbynamel( $hostname );
442 }
443
463 public function trackBlockWithCookie( User $user, WebResponse $response ) {
464 $request = $user->getRequest();
465
466 if ( $request->getCookie( 'BlockID' ) !== null ) {
467 $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
468 if ( $cookieBlock && $this->shouldApplyCookieBlock( $cookieBlock, $user->isAnon() ) ) {
469 return;
470 }
471 // The block pointed to by the cookie is invalid or should not be tracked.
472 $this->clearBlockCookie( $response );
473 }
474
475 if ( !$user->isSafeToLoad() ) {
476 // Prevent a circular dependency by not allowing this method to be called
477 // before or while the user is being loaded.
478 // E.g. User > BlockManager > Block > Message > getLanguage > User.
479 // See also T180050 and T226777.
480 throw new LogicException( __METHOD__ . ' requires a loaded User object' );
481 }
482 if ( $response->headersSent() ) {
483 throw new LogicException( __METHOD__ . ' must be called pre-send' );
484 }
485
486 $block = $user->getBlock();
487 $isAnon = $user->isAnon();
488
489 if ( $block ) {
490 if ( $block instanceof CompositeBlock ) {
491 // TODO: Improve on simply tracking the first trackable block (T225654)
492 foreach ( $block->getOriginalBlocks() as $originalBlock ) {
493 if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
494 '@phan-var DatabaseBlock $originalBlock';
495 $this->setBlockCookie( $originalBlock, $response );
496 return;
497 }
498 }
499 } else {
500 if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
501 '@phan-var DatabaseBlock $block';
502 $this->setBlockCookie( $block, $response );
503 }
504 }
505 }
506 }
507
518 public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) {
519 // Calculate the default expiry time.
520 $maxExpiryTime = wfTimestamp( TS_MW, (int)wfTimestamp() + ( 24 * 60 * 60 ) );
521
522 // Use the block's expiry time only if it's less than the default.
523 $expiryTime = $block->getExpiry();
524 if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
525 $expiryTime = $maxExpiryTime;
526 }
527
528 // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
529 $expiryValue = DateTime::createFromFormat(
530 'YmdHis',
531 $expiryTime,
532 new DateTimeZone( 'UTC' )
533 )->format( 'U' );
534 $cookieOptions = [ 'httpOnly' => false ];
535 $cookieValue = $this->getCookieValue( $block );
536 $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
537 }
538
546 private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
547 if ( $block instanceof DatabaseBlock ) {
548 switch ( $block->getType() ) {
551 return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
553 return !$isAnon &&
554 $this->options->get( 'CookieSetOnAutoblock' ) &&
555 $block->isAutoblocking();
556 default:
557 return false;
558 }
559 }
560 return false;
561 }
562
569 public static function clearBlockCookie( WebResponse $response ) {
570 $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
571 }
572
583 public function getIdFromCookieValue( $cookieValue ) {
584 // The cookie value must start with a number
585 if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
586 return null;
587 }
588
589 // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
590 $bangPos = strpos( $cookieValue, '!' );
591 $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
592 if ( !$this->options->get( 'SecretKey' ) ) {
593 // If there's no secret key, just use the ID as given.
594 return (int)$id;
595 }
596 $storedHmac = substr( $cookieValue, $bangPos + 1 );
597 $calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
598 if ( $calculatedHmac === $storedHmac ) {
599 return (int)$id;
600 } else {
601 return null;
602 }
603 }
604
616 public function getCookieValue( DatabaseBlock $block ) {
617 $id = $block->getId();
618 if ( !$this->options->get( 'SecretKey' ) ) {
619 // If there's no secret key, don't append a HMAC.
620 return $id;
621 }
622 $hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
623 $cookieValue = $id . '!' . $hmac;
624 return $cookieValue;
625 }
626
627}
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.
__construct(ServiceOptions $options, PermissionManager $permissionManager, LoggerInterface $logger, HookContainer $hookContainer)
isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
setBlockCookie(DatabaseBlock $block, WebResponse $response)
Set the 'BlockID' cookie to this block's ID and expiry time.
getUserBlock(User $user, $request, $fromReplica, $disableIpBlockExemptChecking=false)
Get the blocks that apply to a user.
shouldTrackBlockWithCookie(AbstractBlock $block, $isAnon)
Check if the block should be tracked with a cookie.
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
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.
static clearBlockCookie(WebResponse $response)
Unset the 'BlockID' cookie.
getCookieValue(DatabaseBlock $block)
Get the BlockID cookie's value for this block.
getAdditionalIpBlocks(&$blocks, WebRequest $request, $isAnon, $fromMaster)
Check for any additional blocks against the IP address or any IPs in the XFF header.
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 getBlocksForIPList(array $ipChain, $isAnon, $fromMaster=false)
Get all blocks that match any IP from an array of IP addresses.
static newListFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=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.
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()-...
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:161
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:3205
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition User.php:3069
isSafeToLoad()
Test if it's safe to load this User object.
Definition User.php:304
getBlock( $fromReplica=true, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:1991
isAnon()
Get whether the user is anonymous.
Definition User.php:3087
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.