MediaWiki  master
BlockManager.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Block;
22 
23 use DateTime;
24 use DateTimeZone;
25 use Hooks;
26 use IP;
27 use LogicException;
31 use Message;
32 use MWCryptHash;
33 use Psr\Log\LoggerInterface;
34 use User;
35 use WebRequest;
36 use WebResponse;
37 use Wikimedia\IPSet;
38 
45 class BlockManager {
48 
50  private $options;
51 
56  public const CONSTRUCTOR_OPTIONS = [
57  'ApplyIpBlocksToXff',
58  'CookieSetOnAutoblock',
59  'CookieSetOnIpBlock',
60  'DnsBlacklistUrls',
61  'EnableDnsBlacklist',
62  'ProxyList',
63  'ProxyWhitelist',
64  'SecretKey',
65  'SoftBlockRanges',
66  ];
67 
69  private $logger;
70 
76  public function __construct(
79  LoggerInterface $logger
80  ) {
81  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
82  $this->options = $options;
83  $this->permissionManager = $permissionManager;
84  $this->logger = $logger;
85  }
86 
117  public function getUserBlock( User $user, $request, $fromReplica ) {
118  $fromMaster = !$fromReplica;
119  $ip = null;
120 
121  // If this is the global user, they may be affected by IP blocks (case #1),
122  // or they may be exempt (case #2). If affected, look for additional blocks
123  // against the IP address.
124  $checkIpBlocks = $request &&
125  !$this->permissionManager->userHasRight( $user, 'ipblock-exempt' );
126 
127  if ( $request && $checkIpBlocks ) {
128 
129  // Case #1: checking the global user, including IP blocks
130  $ip = $request->getIP();
131  // TODO: remove dependency on DatabaseBlock (T221075)
132  $blocks = DatabaseBlock::newListFromTarget( $user, $ip, $fromMaster );
133  $this->getAdditionalIpBlocks( $blocks, $request, !$user->isRegistered(), $fromMaster );
134  $this->getCookieBlock( $blocks, $user, $request );
135 
136  } elseif ( $request ) {
137 
138  // Case #2: checking the global user, but they are exempt from IP blocks
139  // TODO: remove dependency on DatabaseBlock (T221075)
140  $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster );
141  $this->getCookieBlock( $blocks, $user, $request );
142 
143  } else {
144 
145  // Case #3: checking whether a user's account is blocked
146  // TODO: remove dependency on DatabaseBlock (T221075)
147  $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster );
148 
149  }
150 
151  // Filter out any duplicated blocks, e.g. from the cookie
152  $blocks = $this->getUniqueBlocks( $blocks );
153 
154  $block = null;
155  if ( count( $blocks ) > 0 ) {
156  if ( count( $blocks ) === 1 ) {
157  $block = $blocks[ 0 ];
158  } else {
159  $block = new CompositeBlock( [
160  'address' => $ip,
161  'reason' => new Message( 'blockedtext-composite-reason' ),
162  'originalBlocks' => $blocks,
163  ] );
164  }
165  }
166 
167  Hooks::run( 'GetUserBlock', [ clone $user, $ip, &$block ] );
168 
169  return $block;
170  }
171 
180  private function getCookieBlock( &$blocks, UserIdentity $user, WebRequest $request ) {
181  $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
182  if ( $cookieBlock instanceof DatabaseBlock ) {
183  $blocks[] = $cookieBlock;
184  }
185  }
186 
196  private function getAdditionalIpBlocks( &$blocks, WebRequest $request, $isAnon, $fromMaster ) {
197  $ip = $request->getIP();
198 
199  // Proxy blocking
200  if ( !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) {
201  // Local list
202  if ( $this->isLocallyBlockedProxy( $ip ) ) {
203  $blocks[] = new SystemBlock( [
204  'reason' => new Message( 'proxyblockreason' ),
205  'address' => $ip,
206  'systemBlock' => 'proxy',
207  ] );
208  } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
209  $blocks[] = new SystemBlock( [
210  'reason' => new Message( 'sorbsreason' ),
211  'address' => $ip,
212  'systemBlock' => 'dnsbl',
213  ] );
214  }
215  }
216 
217  // Soft blocking
218  if ( $isAnon && IP::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) ) {
219  $blocks[] = new SystemBlock( [
220  'address' => $ip,
221  'reason' => new Message( 'softblockrangesreason', [ $ip ] ),
222  'anonOnly' => true,
223  'systemBlock' => 'wgSoftBlockRanges',
224  ] );
225  }
226 
227  // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
228  if ( $this->options->get( 'ApplyIpBlocksToXff' )
229  && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
230  ) {
231  $xff = $request->getHeader( 'X-Forwarded-For' );
232  $xff = array_map( 'trim', explode( ',', $xff ) );
233  $xff = array_diff( $xff, [ $ip ] );
234  // TODO: remove dependency on DatabaseBlock (T221075)
235  $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromMaster );
236  $blocks = array_merge( $blocks, $xffblocks );
237  }
238  }
239 
250  private function getUniqueBlocks( array $blocks ) {
251  $systemBlocks = [];
252  $databaseBlocks = [];
253 
254  foreach ( $blocks as $block ) {
255  if ( $block instanceof SystemBlock ) {
256  $systemBlocks[] = $block;
257  } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
259  '@phan-var DatabaseBlock $block';
260  if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
261  $databaseBlocks[$block->getParentBlockId()] = $block;
262  }
263  } else {
264  $databaseBlocks[$block->getId()] = $block;
265  }
266  }
267 
268  return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
269  }
270 
282  private function getBlockFromCookieValue(
283  UserIdentity $user,
284  WebRequest $request
285  ) {
286  $cookieValue = $request->getCookie( 'BlockID' );
287  if ( is_null( $cookieValue ) ) {
288  return false;
289  }
290 
291  $blockCookieId = $this->getIdFromCookieValue( $cookieValue );
292  if ( !is_null( $blockCookieId ) ) {
293  // TODO: remove dependency on DatabaseBlock (T221075)
294  $block = DatabaseBlock::newFromID( $blockCookieId );
295  if (
296  $block instanceof DatabaseBlock &&
297  $this->shouldApplyCookieBlock( $block, !$user->isRegistered() )
298  ) {
299  return $block;
300  }
301  }
302 
303  return false;
304  }
305 
313  private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
314  if ( !$block->isExpired() ) {
315  switch ( $block->getType() ) {
318  // If block is type IP or IP range, load only
319  // if user is not logged in (T152462)
320  return $isAnon &&
321  $this->options->get( 'CookieSetOnIpBlock' );
323  return $block->isAutoblocking() &&
324  $this->options->get( 'CookieSetOnAutoblock' );
325  default:
326  return false;
327  }
328  }
329  return false;
330  }
331 
338  private function isLocallyBlockedProxy( $ip ) {
339  $proxyList = $this->options->get( 'ProxyList' );
340  if ( !$proxyList ) {
341  return false;
342  }
343 
344  if ( !is_array( $proxyList ) ) {
345  // Load values from the specified file
346  $proxyList = array_map( 'trim', file( $proxyList ) );
347  }
348 
349  $proxyListIPSet = new IPSet( $proxyList );
350  return $proxyListIPSet->match( $ip );
351  }
352 
360  public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
361  if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
362  ( $checkWhitelist && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
363  ) {
364  return false;
365  }
366 
367  return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
368  }
369 
377  private function inDnsBlacklist( $ip, array $bases ) {
378  $found = false;
379  // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
380  if ( IP::isIPv4( $ip ) ) {
381  // Reverse IP, T23255
382  $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
383 
384  foreach ( $bases as $base ) {
385  // Make hostname
386  // If we have an access key, use that too (ProjectHoneypot, etc.)
387  $basename = $base;
388  if ( is_array( $base ) ) {
389  if ( count( $base ) >= 2 ) {
390  // Access key is 1, base URL is 0
391  $hostname = "{$base[1]}.$ipReversed.{$base[0]}";
392  } else {
393  $hostname = "$ipReversed.{$base[0]}";
394  }
395  $basename = $base[0];
396  } else {
397  $hostname = "$ipReversed.$base";
398  }
399 
400  // Send query
401  $ipList = $this->checkHost( $hostname );
402 
403  if ( $ipList ) {
404  $this->logger->info(
405  "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!"
406  );
407  $found = true;
408  break;
409  }
410 
411  $this->logger->debug( "Requested $hostname, not found in $basename." );
412  }
413  }
414 
415  return $found;
416  }
417 
424  protected function checkHost( $hostname ) {
425  return gethostbynamel( $hostname );
426  }
427 
447  public function trackBlockWithCookie( User $user, WebResponse $response ) {
448  $request = $user->getRequest();
449 
450  if ( $request->getCookie( 'BlockID' ) !== null ) {
451  $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
452  if ( $cookieBlock && $this->shouldApplyCookieBlock( $cookieBlock, $user->isAnon() ) ) {
453  return;
454  }
455  // The block pointed to by the cookie is invalid or should not be tracked.
456  $this->clearBlockCookie( $response );
457  }
458 
459  if ( !$user->isSafeToLoad() ) {
460  // Prevent a circular dependency by not allowing this method to be called
461  // before or while the user is being loaded.
462  // E.g. User > BlockManager > Block > Message > getLanguage > User.
463  // See also T180050 and T226777.
464  throw new LogicException( __METHOD__ . ' requires a loaded User object' );
465  }
466  if ( $response->headersSent() ) {
467  throw new LogicException( __METHOD__ . ' must be called pre-send' );
468  }
469 
470  $block = $user->getBlock();
471  $isAnon = $user->isAnon();
472 
473  if ( $block ) {
474  if ( $block instanceof CompositeBlock ) {
475  // TODO: Improve on simply tracking the first trackable block (T225654)
476  foreach ( $block->getOriginalBlocks() as $originalBlock ) {
477  if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
478  '@phan-var DatabaseBlock $originalBlock';
479  $this->setBlockCookie( $originalBlock, $response );
480  return;
481  }
482  }
483  } else {
484  if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
485  '@phan-var DatabaseBlock $block';
486  $this->setBlockCookie( $block, $response );
487  }
488  }
489  }
490  }
491 
502  public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) {
503  // Calculate the default expiry time.
504  $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) );
505 
506  // Use the block's expiry time only if it's less than the default.
507  $expiryTime = $block->getExpiry();
508  if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
509  $expiryTime = $maxExpiryTime;
510  }
511 
512  // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
513  $expiryValue = DateTime::createFromFormat(
514  'YmdHis',
515  $expiryTime,
516  new DateTimeZone( 'UTC' )
517  )->format( 'U' );
518  $cookieOptions = [ 'httpOnly' => false ];
519  $cookieValue = $this->getCookieValue( $block );
520  $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
521  }
522 
530  private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
531  if ( $block instanceof DatabaseBlock ) {
532  switch ( $block->getType() ) {
535  return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
537  return !$isAnon &&
538  $this->options->get( 'CookieSetOnAutoblock' ) &&
539  $block->isAutoblocking();
540  default:
541  return false;
542  }
543  }
544  return false;
545  }
546 
553  public static function clearBlockCookie( WebResponse $response ) {
554  $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
555  }
556 
567  public function getIdFromCookieValue( $cookieValue ) {
568  // The cookie value must start with a number
569  if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
570  return null;
571  }
572 
573  // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
574  $bangPos = strpos( $cookieValue, '!' );
575  $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
576  if ( !$this->options->get( 'SecretKey' ) ) {
577  // If there's no secret key, just use the ID as given.
578  return $id;
579  }
580  $storedHmac = substr( $cookieValue, $bangPos + 1 );
581  $calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
582  if ( $calculatedHmac === $storedHmac ) {
583  return $id;
584  } else {
585  return null;
586  }
587  }
588 
600  public function getCookieValue( DatabaseBlock $block ) {
601  $id = $block->getId();
602  if ( !$this->options->get( 'SecretKey' ) ) {
603  // If there's no secret key, don't append a HMAC.
604  return $id;
605  }
606  $hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
607  $cookieValue = $id . '!' . $hmac;
608  return $cookieValue;
609  }
610 
611 }
MediaWiki\Block\BlockManager\$logger
LoggerInterface $logger
Definition: BlockManager.php:52
MediaWiki\Block\BlockManager\trackBlockWithCookie
trackBlockWithCookie(User $user, WebResponse $response)
Set the 'BlockID' cookie depending on block type and user authentication status.
Definition: BlockManager.php:447
MWCryptHash\hmac
static hmac( $data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
Definition: MWCryptHash.php:106
User\isAnon
isAnon()
Get whether the user is anonymous.
Definition: User.php:3582
MediaWiki\Block
Definition: AbstractBlock.php:21
User\isRegistered
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition: User.php:3566
$response
$response
Definition: opensearch_desc.php:38
MediaWiki\Block\BlockManager
A service class for checking blocks.
Definition: BlockManager.php:45
IP\isInRanges
static isInRanges( $ip, $ranges)
Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
Definition: IP.php:655
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1871
MediaWiki\Block\DatabaseBlock\getType
getType()
Get the type of target for this particular block.int|null AbstractBlock::TYPE_ constant,...
Definition: DatabaseBlock.php:1384
MediaWiki\Block\BlockManager\shouldApplyCookieBlock
shouldApplyCookieBlock(DatabaseBlock $block, $isAnon)
Check if the block loaded from the cookie should be applied.
Definition: BlockManager.php:313
MediaWiki\Block\AbstractBlock\TYPE_AUTO
const TYPE_AUTO
Definition: AbstractBlock.php:89
IP
A collection of public static functions to play with IP address and IP ranges.
Definition: IP.php:67
User\isSafeToLoad
isSafeToLoad()
Test if it's safe to load this User object.
Definition: User.php:307
Message
MWCryptHash
Definition: MWCryptHash.php:26
$base
$base
Definition: generateLocalAutoload.php:11
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
User\getRequest
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3700
MediaWiki\Block\AbstractBlock\TYPE_RANGE
const TYPE_RANGE
Definition: AbstractBlock.php:88
MediaWiki\Block\DatabaseBlock
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
Definition: DatabaseBlock.php:53
MediaWiki\Block\DatabaseBlock\getBlocksForIPList
static getBlocksForIPList(array $ipChain, $isAnon, $fromMaster=false)
Get all blocks that match any IP from an array of IP addresses.
Definition: DatabaseBlock.php:1205
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
MediaWiki\Block\BlockManager\shouldTrackBlockWithCookie
shouldTrackBlockWithCookie(AbstractBlock $block, $isAnon)
Check if the block should be tracked with a cookie.
Definition: BlockManager.php:530
MediaWiki\Block\BlockManager\setBlockCookie
setBlockCookie(DatabaseBlock $block, WebResponse $response)
Set the 'BlockID' cookie to this block's ID and expiry time.
Definition: BlockManager.php:502
MediaWiki\Block\BlockManager\isLocallyBlockedProxy
isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
Definition: BlockManager.php:338
MediaWiki\Block\CompositeBlock
Multiple Block class.
Definition: CompositeBlock.php:35
MediaWiki\Block\BlockManager\getUniqueBlocks
getUniqueBlocks(array $blocks)
Given a list of blocks, return a list of unique blocks.
Definition: BlockManager.php:250
MediaWiki\Block\AbstractBlock\TYPE_IP
const TYPE_IP
Definition: AbstractBlock.php:87
MediaWiki\Block\BlockManager\inDnsBlacklist
inDnsBlacklist( $ip, array $bases)
Whether the given IP is in a given DNS blacklist.
Definition: BlockManager.php:377
MediaWiki\Block\BlockManager\getIdFromCookieValue
getIdFromCookieValue( $cookieValue)
Get the stored ID from the 'BlockID' cookie.
Definition: BlockManager.php:567
MediaWiki\Block\AbstractBlock\getType
getType()
Get the type of target for this particular block.
Definition: AbstractBlock.php:432
MediaWiki\Block\DatabaseBlock\isExpired
isExpired()
Has the block expired?
Definition: DatabaseBlock.php:919
User\getBlock
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:2116
MediaWiki\Block\DatabaseBlock\isAutoblocking
isAutoblocking( $x=null)
Definition: DatabaseBlock.php:1073
MediaWiki\Block\AbstractBlock\TYPE_USER
const TYPE_USER
Definition: AbstractBlock.php:86
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:48
MediaWiki\Block\BlockManager\getBlockFromCookieValue
getBlockFromCookieValue(UserIdentity $user, WebRequest $request)
Try to load a block from an ID given in a cookie value.
Definition: BlockManager.php:282
$expiryTime
$expiryTime
Definition: opensearch_desc.php:43
MediaWiki\Block\SystemBlock
System blocks are temporary blocks that are created on enforcement (e.g.
Definition: SystemBlock.php:33
MediaWiki\Block\BlockManager\$options
ServiceOptions $options
Definition: BlockManager.php:50
MediaWiki\Block\BlockManager\clearBlockCookie
static clearBlockCookie(WebResponse $response)
Unset the 'BlockID' cookie.
Definition: BlockManager.php:553
MediaWiki\Block\BlockManager\$permissionManager
PermissionManager $permissionManager
Definition: BlockManager.php:47
MediaWiki\Block\BlockManager\getCookieBlock
getCookieBlock(&$blocks, UserIdentity $user, WebRequest $request)
Get the cookie block, if there is one.
Definition: BlockManager.php:180
MediaWiki\Block\AbstractBlock\getExpiry
getExpiry()
Get the block expiry time.
Definition: AbstractBlock.php:466
WebRequest\getCookie
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
Definition: WebRequest.php:845
IP\isIPv4
static isIPv4( $ip)
Given a string, determine if it as valid IP in IPv4 only.
Definition: IP.php:99
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
MediaWiki\Block\BlockManager\getUserBlock
getUserBlock(User $user, $request, $fromReplica)
Get the blocks that apply to a user.
Definition: BlockManager.php:117
WebRequest\getIP
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
Definition: WebRequest.php:1233
MediaWiki\Block\DatabaseBlock\getId
getId()
Get the block ID.int|null
Definition: DatabaseBlock.php:1015
MediaWiki\Block\BlockManager\getCookieValue
getCookieValue(DatabaseBlock $block)
Get the BlockID cookie's value for this block.
Definition: BlockManager.php:600
MediaWiki\Block\BlockManager\isDnsBlacklisted
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
Definition: BlockManager.php:360
WebRequest\getHeader
getHeader( $name, $flags=0)
Get a request header, or false if it isn't set.
Definition: WebRequest.php:1098
MediaWiki\Block\AbstractBlock
Definition: AbstractBlock.php:36
MediaWiki\Block\BlockManager\checkHost
checkHost( $hostname)
Wrapper for mocking in tests.
Definition: BlockManager.php:424
WebResponse
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
Definition: WebResponse.php:28
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MediaWiki\Block\DatabaseBlock\newListFromTarget
static newListFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=false)
This is similar to DatabaseBlock::newFromTarget, but it returns all the relevant blocks.
Definition: DatabaseBlock.php:1172
Hooks
Hooks class.
Definition: Hooks.php:34
MediaWiki\Block\BlockManager\getAdditionalIpBlocks
getAdditionalIpBlocks(&$blocks, WebRequest $request, $isAnon, $fromMaster)
Check for any additional blocks against the IP address or any IPs in the XFF header.
Definition: BlockManager.php:196
MediaWiki\Block\BlockManager\__construct
__construct(ServiceOptions $options, PermissionManager $permissionManager, LoggerInterface $logger)
Definition: BlockManager.php:76
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:62
MediaWiki\Block\DatabaseBlock\newFromID
static newFromID( $id)
Load a block from the block id.
Definition: DatabaseBlock.php:159