MediaWiki REL1_34
BlockManager.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Block;
22
23use DateTime;
24use DateTimeZone;
26use Hooks;
27use IP;
31use MWCryptHash;
32use Psr\Log\LoggerInterface;
33use User;
34use WebRequest;
35use WebResponse;
36use Wikimedia\IPSet;
37
47
49 private $options;
50
55 public const CONSTRUCTOR_OPTIONS = [
56 'ApplyIpBlocksToXff',
57 'CookieSetOnAutoblock',
58 'CookieSetOnIpBlock',
59 'DnsBlacklistUrls',
60 'EnableDnsBlacklist',
61 'ProxyList',
62 'ProxyWhitelist',
63 'SecretKey',
64 'SoftBlockRanges',
65 ];
66
68 private $logger;
69
75 public function __construct(
78 LoggerInterface $logger
79 ) {
80 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
81 $this->options = $options;
82 $this->permissionManager = $permissionManager;
83 $this->logger = $logger;
84 }
85
116 public function getUserBlock( User $user, $request, $fromReplica ) {
117 $fromMaster = !$fromReplica;
118 $ip = null;
119
120 // If this is the global user, they may be affected by IP blocks (case #1),
121 // or they may be exempt (case #2). If affected, look for additional blocks
122 // against the IP address.
123 $checkIpBlocks = $request &&
124 !$this->permissionManager->userHasRight( $user, 'ipblock-exempt' );
125
126 if ( $request && $checkIpBlocks ) {
127
128 // Case #1: checking the global user, including IP blocks
129 $ip = $request->getIP();
130 // TODO: remove dependency on DatabaseBlock (T221075)
131 $blocks = DatabaseBlock::newListFromTarget( $user, $ip, $fromMaster );
132 $this->getAdditionalIpBlocks( $blocks, $request, !$user->isRegistered(), $fromMaster );
133 $this->getCookieBlock( $blocks, $user, $request );
134
135 } elseif ( $request ) {
136
137 // Case #2: checking the global user, but they are exempt from IP blocks
138 // TODO: remove dependency on DatabaseBlock (T221075)
139 $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster );
140 $this->getCookieBlock( $blocks, $user, $request );
141
142 } else {
143
144 // Case #3: checking whether a user's account is blocked
145 // TODO: remove dependency on DatabaseBlock (T221075)
146 $blocks = DatabaseBlock::newListFromTarget( $user, null, $fromMaster );
147
148 }
149
150 // Filter out any duplicated blocks, e.g. from the cookie
151 $blocks = $this->getUniqueBlocks( $blocks );
152
153 $block = null;
154 if ( count( $blocks ) > 0 ) {
155 if ( count( $blocks ) === 1 ) {
156 $block = $blocks[ 0 ];
157 } else {
158 $block = new CompositeBlock( [
159 'address' => $ip,
160 'byText' => 'MediaWiki default',
161 'reason' => wfMessage( 'blockedtext-composite-reason' )->plain(),
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 'byText' => wfMessage( 'proxyblocker' )->text(),
205 'reason' => wfMessage( 'proxyblockreason' )->plain(),
206 'address' => $ip,
207 'systemBlock' => 'proxy',
208 ] );
209 } elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
210 $blocks[] = new SystemBlock( [
211 'byText' => wfMessage( 'sorbs' )->text(),
212 'reason' => wfMessage( 'sorbsreason' )->plain(),
213 'address' => $ip,
214 'systemBlock' => 'dnsbl',
215 ] );
216 }
217 }
218
219 // Soft blocking
220 if ( $isAnon && IP::isInRanges( $ip, $this->options->get( 'SoftBlockRanges' ) ) ) {
221 $blocks[] = new SystemBlock( [
222 'address' => $ip,
223 'byText' => 'MediaWiki default',
224 'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
225 'anonOnly' => true,
226 'systemBlock' => 'wgSoftBlockRanges',
227 ] );
228 }
229
230 // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
231 if ( $this->options->get( 'ApplyIpBlocksToXff' )
232 && !in_array( $ip, $this->options->get( 'ProxyWhitelist' ) )
233 ) {
234 $xff = $request->getHeader( 'X-Forwarded-For' );
235 $xff = array_map( 'trim', explode( ',', $xff ) );
236 $xff = array_diff( $xff, [ $ip ] );
237 // TODO: remove dependency on DatabaseBlock (T221075)
238 $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromMaster );
239 $blocks = array_merge( $blocks, $xffblocks );
240 }
241 }
242
253 private function getUniqueBlocks( array $blocks ) {
254 $systemBlocks = [];
255 $databaseBlocks = [];
256
257 foreach ( $blocks as $block ) {
258 if ( $block instanceof SystemBlock ) {
259 $systemBlocks[] = $block;
260 } elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
262 '@phan-var DatabaseBlock $block';
263 if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
264 $databaseBlocks[$block->getParentBlockId()] = $block;
265 }
266 } else {
267 $databaseBlocks[$block->getId()] = $block;
268 }
269 }
270
271 return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
272 }
273
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 $this->clearBlockCookie( $request->response() );
304
305 return false;
306 }
307
315 private function shouldApplyCookieBlock( DatabaseBlock $block, $isAnon ) {
316 if ( !$block->isExpired() ) {
317 switch ( $block->getType() ) {
320 // If block is type IP or IP range, load only
321 // if user is not logged in (T152462)
322 return $isAnon &&
323 $this->options->get( 'CookieSetOnIpBlock' );
325 return $block->isAutoblocking() &&
326 $this->options->get( 'CookieSetOnAutoblock' );
327 default:
328 return false;
329 }
330 }
331 return false;
332 }
333
340 private function isLocallyBlockedProxy( $ip ) {
341 $proxyList = $this->options->get( 'ProxyList' );
342 if ( !$proxyList ) {
343 return false;
344 }
345
346 if ( !is_array( $proxyList ) ) {
347 // Load values from the specified file
348 $proxyList = array_map( 'trim', file( $proxyList ) );
349 }
350
351 $proxyListIPSet = new IPSet( $proxyList );
352 return $proxyListIPSet->match( $ip );
353 }
354
362 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
363 if ( !$this->options->get( 'EnableDnsBlacklist' ) ||
364 ( $checkWhitelist && in_array( $ip, $this->options->get( 'ProxyWhitelist' ) ) )
365 ) {
366 return false;
367 }
368
369 return $this->inDnsBlacklist( $ip, $this->options->get( 'DnsBlacklistUrls' ) );
370 }
371
379 private function inDnsBlacklist( $ip, array $bases ) {
380 $found = false;
381 // @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
382 if ( IP::isIPv4( $ip ) ) {
383 // Reverse IP, T23255
384 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
385
386 foreach ( $bases as $base ) {
387 // Make hostname
388 // If we have an access key, use that too (ProjectHoneypot, etc.)
389 $basename = $base;
390 if ( is_array( $base ) ) {
391 if ( count( $base ) >= 2 ) {
392 // Access key is 1, base URL is 0
393 $hostname = "{$base[1]}.$ipReversed.{$base[0]}";
394 } else {
395 $hostname = "$ipReversed.{$base[0]}";
396 }
397 $basename = $base[0];
398 } else {
399 $hostname = "$ipReversed.$base";
400 }
401
402 // Send query
403 $ipList = $this->checkHost( $hostname );
404
405 if ( $ipList ) {
406 $this->logger->info(
407 "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!"
408 );
409 $found = true;
410 break;
411 }
412
413 $this->logger->debug( "Requested $hostname, not found in $basename." );
414 }
415 }
416
417 return $found;
418 }
419
426 protected function checkHost( $hostname ) {
427 return gethostbynamel( $hostname );
428 }
429
436 public function trackBlockWithCookie( User $user ) {
437 $request = $user->getRequest();
438 if ( $request->getCookie( 'BlockID' ) !== null ) {
439 // User already has a block cookie
440 return;
441 }
442
443 // Defer checks until the user has been fully loaded to avoid circular dependency
444 // of User on itself (T180050 and T226777)
445 DeferredUpdates::addCallableUpdate(
446 function () use ( $user, $request ) {
447 $block = $user->getBlock();
448 $response = $request->response();
449 $isAnon = $user->isAnon();
450
451 if ( $block ) {
452 if ( $block instanceof CompositeBlock ) {
453 // TODO: Improve on simply tracking the first trackable block (T225654)
454 foreach ( $block->getOriginalBlocks() as $originalBlock ) {
455 if ( $this->shouldTrackBlockWithCookie( $originalBlock, $isAnon ) ) {
456 '@phan-var DatabaseBlock $originalBlock';
457 $this->setBlockCookie( $originalBlock, $response );
458 return;
459 }
460 }
461 } else {
462 if ( $this->shouldTrackBlockWithCookie( $block, $isAnon ) ) {
463 '@phan-var DatabaseBlock $block';
464 $this->setBlockCookie( $block, $response );
465 }
466 }
467 }
468 },
469 DeferredUpdates::PRESEND
470 );
471 }
472
484 // Calculate the default expiry time.
485 $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) );
486
487 // Use the block's expiry time only if it's less than the default.
488 $expiryTime = $block->getExpiry();
489 if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) {
490 $expiryTime = $maxExpiryTime;
491 }
492
493 // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie.
494 $expiryValue = DateTime::createFromFormat(
495 'YmdHis',
497 new DateTimeZone( 'UTC' )
498 )->format( 'U' );
499 $cookieOptions = [ 'httpOnly' => false ];
500 $cookieValue = $this->getCookieValue( $block );
501 $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions );
502 }
503
511 private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) {
512 if ( $block instanceof DatabaseBlock ) {
513 switch ( $block->getType() ) {
516 return $isAnon && $this->options->get( 'CookieSetOnIpBlock' );
518 return !$isAnon &&
519 $this->options->get( 'CookieSetOnAutoblock' ) &&
520 $block->isAutoblocking();
521 default:
522 return false;
523 }
524 }
525 return false;
526 }
527
534 public static function clearBlockCookie( WebResponse $response ) {
535 $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] );
536 }
537
548 public function getIdFromCookieValue( $cookieValue ) {
549 // The cookie value must start with a number
550 if ( !is_numeric( substr( $cookieValue, 0, 1 ) ) ) {
551 return null;
552 }
553
554 // Extract the ID prefix from the cookie value (may be the whole value, if no bang found).
555 $bangPos = strpos( $cookieValue, '!' );
556 $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos );
557 if ( !$this->options->get( 'SecretKey' ) ) {
558 // If there's no secret key, just use the ID as given.
559 return $id;
560 }
561 $storedHmac = substr( $cookieValue, $bangPos + 1 );
562 $calculatedHmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
563 if ( $calculatedHmac === $storedHmac ) {
564 return $id;
565 } else {
566 return null;
567 }
568 }
569
581 public function getCookieValue( DatabaseBlock $block ) {
582 $id = $block->getId();
583 if ( !$this->options->get( 'SecretKey' ) ) {
584 // If there's no secret key, don't append a HMAC.
585 return $id;
586 }
587 $hmac = MWCryptHash::hmac( $id, $this->options->get( 'SecretKey' ), false );
588 $cookieValue = $id . '!' . $hmac;
589 return $cookieValue;
590 }
591
592}
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Class for managing the deferred updates.
Hooks class.
Definition Hooks.php:34
A collection of public static functions to play with IP address and IP ranges.
Definition IP.php:67
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
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.
isLocallyBlockedProxy( $ip)
Check if an IP address is in the local proxy list.
trackBlockWithCookie(User $user)
Set the 'BlockID' cookie depending on block type and user authentication status.
setBlockCookie(DatabaseBlock $block, WebResponse $response)
Set the 'BlockID' cookie to this block's ID and expiry time.
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.
__construct(ServiceOptions $options, PermissionManager $permissionManager, LoggerInterface $logger)
getUserBlock(User $user, $request, $fromReplica)
Get the blocks that apply to a user.
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 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,...
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:3737
isRegistered()
Alias of isLoggedIn() with a name that describes its actual functionality.
Definition User.php:3622
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:2200
isAnon()
Get whether the user is anonymous.
Definition User.php:3638
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
response()
Return a handle to WebResponse style object, for setting cookies, headers and other stuff,...
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 ...
Interface for objects representing user identity.
$expiryTime