MediaWiki  master
BlockManager.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Block;
22 
25 use IP;
29 use User;
33 
40 class BlockManager {
41  // TODO: This should be UserIdentity instead of User
43  private $currentUser;
44 
46  private $currentRequest;
47 
54  public static $constructorOptions = [
55  'ApplyIpBlocksToXff',
56  'CookieSetOnAutoblock',
57  'CookieSetOnIpBlock',
58  'DnsBlacklistUrls',
59  'EnableDnsBlacklist',
60  'ProxyList',
61  'ProxyWhitelist',
62  'SecretKey',
63  'SoftBlockRanges',
64  ];
65 
71  public function __construct(
75  ) {
76  $options->assertRequiredOptions( self::$constructorOptions );
77  $this->options = $options;
78  $this->currentUser = $currentUser;
79  $this->currentRequest = $currentRequest;
80  }
81 
96  public function getUserBlock( User $user, $fromReplica ) {
97  $isAnon = $user->getId() === 0;
98 
99  // TODO: If $user is the current user, we should use the current request. Otherwise,
100  // we should not look for XFF or cookie blocks.
101  $request = $user->getRequest();
102 
103  # We only need to worry about passing the IP address to the block generator if the
104  # user is not immune to autoblocks/hardblocks, and they are the current user so we
105  # know which IP address they're actually coming from
106  $ip = null;
107  $sessionUser = $this->currentUser;
108  // the session user is set up towards the end of Setup.php. Until then,
109  // assume it's a logged-out user.
110  $globalUserName = $sessionUser->isSafeToLoad()
111  ? $sessionUser->getName()
112  : IP::sanitizeIP( $this->currentRequest->getIP() );
113  if ( $user->getName() === $globalUserName && !$user->isAllowed( 'ipblock-exempt' ) ) {
114  $ip = $this->currentRequest->getIP();
115  }
116 
117  // User/IP blocking
118  // After this, $blocks is an array of blocks or an empty array
119  // TODO: remove dependency on DatabaseBlock
120  $blocks = DatabaseBlock::newListFromTarget( $user, $ip, !$fromReplica );
121 
122  // Cookie blocking
123  $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
124  if ( $cookieBlock instanceof AbstractBlock ) {
125  $blocks[] = $cookieBlock;
126  }
127 
128  // Proxy blocking
129  if ( $ip !== null && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) ) {
130  // Local list
131  if ( $this->isLocallyBlockedProxy( $ip ) ) {
132  $blocks[] = new SystemBlock( [
133  'byText' => wfMessage( 'proxyblocker' )->text(),
134  'reason' => wfMessage( 'proxyblockreason' )->plain(),
135  'address' => $ip,
136  'systemBlock' => 'proxy',
137  ] );
138  } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
139  $blocks[] = new SystemBlock( [
140  'byText' => wfMessage( 'sorbs' )->text(),
141  'reason' => wfMessage( 'sorbsreason' )->plain(),
142  'address' => $ip,
143  'systemBlock' => 'dnsbl',
144  ] );
145  }
146  }
147 
148  // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
149  if ( $this->options->get( 'ApplyIpBlocksToXff' )
150  && $ip !== null
151  && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
152  ) {
153  $xff = $request->getHeader( 'X-Forwarded-For' );
154  $xff = array_map( 'trim', explode( ',', $xff ) );
155  $xff = array_diff( $xff, [ $ip ] );
156  // TODO: remove dependency on DatabaseBlock
157  $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
158  $blocks = array_merge( $blocks, $xffblocks );
159  }
160 
161  // Soft blocking
162  if ( $ip !== null
163  && $isAnon
164  && IP::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) )
165  ) {
166  $blocks[] = new SystemBlock( [
167  'address' => $ip,
168  'byText' => 'MediaWiki default',
169  'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
170  'anonOnly' => true,
171  'systemBlock' => 'wgSoftBlockRanges',
172  ] );
173  }
174 
175  // Filter out any duplicated blocks, e.g. from the cookie
176  $blocks = $this->getUniqueBlocks( $blocks );
177 
178  if ( count( $blocks ) > 0 ) {
179  if ( count( $blocks ) === 1 ) {
180  $block = $blocks[ 0 ];
181  } else {
182  $block = new CompositeBlock( [
183  'address' => $ip,
184  'byText' => 'MediaWiki default',
185  'reason' => wfMessage( 'blockedtext-composite-reason' )->plain(),
186  'originalBlocks' => $blocks,
187  ] );
188  }
189  return $block;
190  }
191 
192  return null;
193  }
194 
205  private function getUniqueBlocks( array $blocks ) {
206  $systemBlocks = [];
207  $databaseBlocks = [];
208 
209  foreach ( $blocks as $block ) {
210  if ( $block instanceof SystemBlock ) {
211  $systemBlocks[] = $block;
212  } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
213  if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
214  $databaseBlocks[$block->getParentBlockId()] = $block;
215  }
216  } else {
217  $databaseBlocks[$block->getId()] = $block;
218  }
219  }
220 
221  return array_merge( $systemBlocks, $databaseBlocks );
222  }
223 
232  private function getBlockFromCookieValue(
235  ) {
236  $blockCookieId = $this->getIdFromCookieValue( $request->getCookie( 'BlockID' ) );
237 
238  if ( $blockCookieId !== null ) {
239  // TODO: remove dependency on DatabaseBlock
240  $block = DatabaseBlock::newFromID( $blockCookieId );
241  if (
242  $block instanceof DatabaseBlock &&
243  $this->shouldApplyCookieBlock( $block, $user->isAnon() )
244  ) {
245  return $block;
246  }
247  $this->clearBlockCookie( $request->response() );
248  }
249 
250  return false;
251  }
252 
260  private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
261  if ( !$block->isExpired() ) {
262  switch ( $block->getType() ) {
265  // If block is type IP or IP range, load only
266  // if user is not logged in (T152462)
267  return $isAnon &&
268  $this->options->get( 'CookieSetOnIpBlock' );
270  return $block->isAutoblocking() &&
271  $this->options->get( 'CookieSetOnAutoblock' );
272  default:
273  return false;
274  }
275  }
276  return false;
277  }
278 
285  private function isLocallyBlockedProxy( $ip ) {
286  $proxyList = $this->options->get( 'ProxyList' );
287  if ( !$proxyList ) {
288  return false;
289  }
290 
291  if ( !is_array( $proxyList ) ) {
292  // Load values from the specified file
293  $proxyList = array_map( 'trim', file( $proxyList ) );
294  }
295 
296  $resultProxyList = [];
297  $deprecatedIPEntries = [];
298 
299  // backward compatibility: move all ip addresses in keys to values
300  foreach ( $proxyList as $key => $value ) {
301  $keyIsIP = IP::isIPAddress( $key );
302  $valueIsIP = IP::isIPAddress( $value );
303  if ( $keyIsIP && !$valueIsIP ) {
304  $deprecatedIPEntries[] = $key;
305  $resultProxyList[] = $key;
306  } elseif ( $keyIsIP && $valueIsIP ) {
307  $deprecatedIPEntries[] = $key;
308  $resultProxyList[] = $key;
309  $resultProxyList[] = $value;
310  } else {
311  $resultProxyList[] = $value;
312  }
313  }
314 
315  if ( $deprecatedIPEntries ) {
316  wfDeprecated(
317  'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
318  implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
319  }
320 
321  $proxyListIPSet = new IPSet( $resultProxyList );
322  return $proxyListIPSet->match( $ip );
323  }
324 
332  public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
333  if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
334  ( $checkWhitelist && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
335  ) {
336  return false;
337  }
338 
339  return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
340  }
341 
349  private function inDnsBlacklist( $ip, array $bases ) {
350  $found = false;
351  // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
352  if ( IP::isIPv4( $ip ) ) {
353  // Reverse IP, T23255
354  $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
355 
356  foreach ( $bases as $base ) {
357  // Make hostname
358  // If we have an access key, use that too (ProjectHoneypot, etc.)
359  $basename = $base;
360  if ( is_array( $base ) ) {
361  if ( count( $base ) >= 2 ) {
362  // Access key is 1, base URL is 0
363  $hostname = "{$base[1]}.$ipReversed.{$base[0]}";
364  } else {
365  $hostname = "$ipReversed.{$base[0]}";
366  }
367  $basename = $base[0];
368  } else {
369  $hostname = "$ipReversed.$base";
370  }
371 
372  // Send query
373  $ipList = $this->checkHost( $hostname );
374 
375  if ( $ipList ) {
376  wfDebugLog(
377  'dnsblacklist',
378  "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!"
379  );
380  $found = true;
381  break;
382  }
383 
384  wfDebugLog( 'dnsblacklist', "Requested $hostname, not found in $basename." );
385  }
386  }
387 
388  return $found;
389  }
390 
397  protected function checkHost( $hostname ) {
398  return gethostbynamel( $hostname );
399  }
400 
407  public function trackBlockWithCookie( User $user ) {
408  $request = $user->getRequest();
409  if ( $request->getCookie( 'BlockID' ) !== null ) {
410  // User already has a block cookie
411  return;
412  }
413 
414  // Defer checks until the user has been fully loaded to avoid circular dependency
415  // of User on itself (T180050 and T226777)
417  function () use ( $user, $request ) {
418  $block = $user->getBlock();
419  $response = $request->response();
420  $isAnon = $user->isAnon();
421 
422  if ( $block ) {
423  if ( $block instanceof CompositeBlock ) {
424  // TODO: Improve on simply tracking the first trackable block (T225654)
425  foreach ( $block->getOriginalBlocks() as $originalBlock ) {
426  if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
427  $this->setBlockCookie( $originalBlock, $response );
428  return;
429  }
430  }
431  } else {
432  if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
433  $this->setBlockCookie( $block, $response );
434  }
435  }
436  }
437  },
439  );
440  }
441 
452  public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) {
453  // Calculate the default expiry time.
454  $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) );
455 
456  // Use the block's expiry time only if it's less than the default.
457  $expiryTime = $block->getExpiry();
458  if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
459  $expiryTime = $maxExpiryTime;
460  }
461 
462  // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
463  $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' );
464  $cookieOptions = [ 'httpOnly' => false ];
465  $cookieValue = $this->getCookieValue( $block );
466  $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
467  }
468 
476  private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
477  if ( $block instanceof DatabaseBlock ) {
478  switch ( $block->getType() ) {
481  return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
483  return !$isAnon &&
484  $this->options->get( 'CookieSetOnAutoblock' ) &&
485  $block->isAutoblocking();
486  default:
487  return false;
488  }
489  }
490  return false;
491  }
492 
499  public static function clearBlockCookie( WebResponse $response ) {
500  $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
501  }
502 
513  public function getIdFromCookieValue( $cookieValue ) {
514  // The cookie value must start with a number
515  if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
516  return null;
517  }
518 
519  // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
520  $bangPos = strpos( $cookieValue, '!' );
521  $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
522  if ( !$this->options->get( 'SecretKey' ) ) {
523  // If there's no secret key, just use the ID as given.
524  return $id;
525  }
526  $storedHmac = substr( $cookieValue, $bangPos + 1 );
527  $calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
528  if ( $calculatedHmac === $storedHmac ) {
529  return $id;
530  } else {
531  return null;
532  }
533  }
534 
546  public function getCookieValue( DatabaseBlock $block ) {
547  $id = $block->getId();
548  if ( !$this->options->get( 'SecretKey' ) ) {
549  // If there's no secret key, don't append a HMAC.
550  return $id;
551  }
552  $hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
553  $cookieValue = $id . '!' . $hmac;
554  return $cookieValue;
555  }
556 
557 }
getUniqueBlocks(array $blocks)
Given a list of blocks, return a list of unique blocks.
static isIPAddress( $ip)
Determine if a string is as valid IP address or network (CIDR prefix).
Definition: IP.php:77
either a plain
Definition: hooks.txt:2032
setCookie( $name, $value, $expire=0, $options=[])
Set the browser cookie.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
getCookieValue(DatabaseBlock $block)
Get the BlockID cookie&#39;s value for this block.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
inDnsBlacklist( $ip, array $bases)
Whether the given IP is in a given DNS blacklist.
getIdFromCookieValue( $cookieValue)
Get the stored ID from the &#39;BlockID&#39; cookie.
getBlockFromCookieValue(UserIdentity $user, WebRequest $request)
Try to load a block from an ID given in a cookie value.
response()
Return a handle to WebResponse style object, for setting cookies, headers and other stuff...
Definition: WebRequest.php:988
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:2089
static clearBlockCookie(WebResponse $response)
Unset the &#39;BlockID&#39; cookie.
$value
Multiple Block class.
checkHost( $hostname)
Wrapper for mocking in tests.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
this hook is for auditing only $response
Definition: hooks.txt:767
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2251
clearCookie( $name, $options=[])
Unset a browser cookie.
A class for passing options to services.
static getBlocksForIPList(array $ipChain, $isAnon, $fromMaster=false)
Get all blocks that match any IP from an array of IP addresses.
Interface for objects representing user identity.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing we can concentrate it all in an extension file
Definition: hooks.txt:91
isDnsBlacklisted( $ip, $checkWhitelist=false)
Whether the given IP is in a DNS blacklist.
isExpired()
Has the block expired?
static sanitizeIP( $ip)
Convert an IP into a verbose, uppercase, normalized form.
Definition: IP.php:139
isAnon()
Get whether the user is anonymous.
Definition: User.php:3582
System blocks are temporary blocks that are created on enforcement (e.g.
Definition: SystemBlock.php:35
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3642
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1971
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
__construct(ServiceOptions $options, User $currentUser, WebRequest $currentRequest)
trackBlockWithCookie(User $user)
Set the &#39;BlockID&#39; cookie depending on block type and user authentication status.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getType()
Get the type of target for this particular block.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys, without regard for order.
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3685
$expiryTime
A service class for checking blocks.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing options(say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle
getId()
Get the user&#39;s ID.
Definition: User.php:2224
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static array $constructorOptions
TODO Make this a const when HHVM support is dropped (T192166)
isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
static isIPv4( $ip)
Given a string, determine if it as valid IP in IPv4 only.
Definition: IP.php:99
getUserBlock(User $user, $fromReplica)
Get the blocks that apply to a user.
static isInRanges( $ip, $ranges)
Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
Definition: IP.php:655
getExpiry()
Get the block expiry time.
shouldApplyCookieBlock(DatabaseBlock $block, $isAnon)
Check if the block loaded from the cookie should be applied.
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
Definition: WebRequest.php:791
static newListFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=false)
This is similar to DatabaseBlock::newFromTarget, but it returns all the relevant blocks.
static newFromID( $id)
Load a block from the block id.
setBlockCookie(DatabaseBlock $block, WebResponse $response)
Set the &#39;BlockID&#39; cookie to this block&#39;s ID and expiry time.
isSafeToLoad()
Test if it&#39;s safe to load this User object.
Definition: User.php:291
static hmac( $data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2620
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
shouldTrackBlockWithCookie(AbstractBlock $block, $isAnon)
Check if the block should be tracked with a cookie.