This introduces a minimal BlockManager service, for getting blocks that apply to a User. Move the part of User::getBlockedStatus that checks for the blocks into BlockManager::getUserBlock, and move the related helper methods from User to BlockManager. Hard deprecate or remove these helper methods, and move to private methods in the BlockManager: * User::getBlockFromCookieValue * User::isLocallyBlockedProxy * User::inDnsBlacklist Soft deprecate these helper methods, and move to public methods in the BlockManager: * User::isDnsBlacklisted Add tests to cover the methods moved to BlockManager. Bug: T219441 Change-Id: I0af658d71288376735cebe541215383b56bb72e5
370 lines
11 KiB
PHP
370 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
namespace MediaWiki\Block;
|
|
|
|
use Block;
|
|
use IP;
|
|
use User;
|
|
use WebRequest;
|
|
use Wikimedia\IPSet;
|
|
use MediaWiki\User\UserIdentity;
|
|
|
|
/**
|
|
* A service class for checking blocks.
|
|
* To obtain an instance, use MediaWikiServices::getInstance()->getBlockManager().
|
|
*
|
|
* @since 1.34 Refactored from User and Block.
|
|
*/
|
|
class BlockManager {
|
|
// TODO: This should be UserIdentity instead of User
|
|
/** @var User */
|
|
private $currentUser;
|
|
|
|
/** @var WebRequest */
|
|
private $currentRequest;
|
|
|
|
/** @var bool */
|
|
private $applyIpBlocksToXff;
|
|
|
|
/** @var bool */
|
|
private $cookieSetOnAutoblock;
|
|
|
|
/** @var bool */
|
|
private $cookieSetOnIpBlock;
|
|
|
|
/** @var array */
|
|
private $dnsBlacklistUrls;
|
|
|
|
/** @var bool */
|
|
private $enableDnsBlacklist;
|
|
|
|
/** @var array */
|
|
private $proxyList;
|
|
|
|
/** @var array */
|
|
private $proxyWhitelist;
|
|
|
|
/** @var array */
|
|
private $softBlockRanges;
|
|
|
|
/**
|
|
* @param User $currentUser
|
|
* @param WebRequest $currentRequest
|
|
* @param bool $applyIpBlocksToXff
|
|
* @param bool $cookieSetOnAutoblock
|
|
* @param bool $cookieSetOnIpBlock
|
|
* @param array $dnsBlacklistUrls
|
|
* @param bool $enableDnsBlacklist
|
|
* @param array $proxyList
|
|
* @param array $proxyWhitelist
|
|
* @param array $softBlockRanges
|
|
*/
|
|
public function __construct(
|
|
$currentUser,
|
|
$currentRequest,
|
|
$applyIpBlocksToXff,
|
|
$cookieSetOnAutoblock,
|
|
$cookieSetOnIpBlock,
|
|
$dnsBlacklistUrls,
|
|
$enableDnsBlacklist,
|
|
$proxyList,
|
|
$proxyWhitelist,
|
|
$softBlockRanges
|
|
) {
|
|
$this->currentUser = $currentUser;
|
|
$this->currentRequest = $currentRequest;
|
|
$this->applyIpBlocksToXff = $applyIpBlocksToXff;
|
|
$this->cookieSetOnAutoblock = $cookieSetOnAutoblock;
|
|
$this->cookieSetOnIpBlock = $cookieSetOnIpBlock;
|
|
$this->dnsBlacklistUrls = $dnsBlacklistUrls;
|
|
$this->enableDnsBlacklist = $enableDnsBlacklist;
|
|
$this->proxyList = $proxyList;
|
|
$this->proxyWhitelist = $proxyWhitelist;
|
|
$this->softBlockRanges = $softBlockRanges;
|
|
}
|
|
|
|
/**
|
|
* Get the blocks that apply to a user and return the most relevant one.
|
|
*
|
|
* TODO: $user should be UserIdentity instead of User
|
|
*
|
|
* @internal This should only be called by User::getBlockedStatus
|
|
* @param User $user
|
|
* @param bool $fromReplica Whether to check the replica DB first.
|
|
* To improve performance, non-critical checks are done against replica DBs.
|
|
* Check when actually saving should be done against master.
|
|
* @return Block|null The most relevant block, or null if there is no block.
|
|
*/
|
|
public function getUserBlock( User $user, $fromReplica ) {
|
|
$isAnon = $user->getId() === 0;
|
|
|
|
// TODO: If $user is the current user, we should use the current request. Otherwise,
|
|
// we should not look for XFF or cookie blocks.
|
|
$request = $user->getRequest();
|
|
|
|
# We only need to worry about passing the IP address to the Block generator if the
|
|
# user is not immune to autoblocks/hardblocks, and they are the current user so we
|
|
# know which IP address they're actually coming from
|
|
$ip = null;
|
|
$sessionUser = $this->currentUser;
|
|
// the session user is set up towards the end of Setup.php. Until then,
|
|
// assume it's a logged-out user.
|
|
$globalUserName = $sessionUser->isSafeToLoad()
|
|
? $sessionUser->getName()
|
|
: IP::sanitizeIP( $this->currentRequest->getIP() );
|
|
if ( $user->getName() === $globalUserName && !$user->isAllowed( 'ipblock-exempt' ) ) {
|
|
$ip = $this->currentRequest->getIP();
|
|
}
|
|
|
|
// User/IP blocking
|
|
// TODO: remove dependency on Block
|
|
$block = Block::newFromTarget( $user, $ip, !$fromReplica );
|
|
|
|
// Cookie blocking
|
|
if ( !$block instanceof Block ) {
|
|
$block = $this->getBlockFromCookieValue( $user, $request );
|
|
}
|
|
|
|
// Proxy blocking
|
|
if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $this->proxyWhitelist ) ) {
|
|
// Local list
|
|
if ( $this->isLocallyBlockedProxy( $ip ) ) {
|
|
$block = new Block( [
|
|
'byText' => wfMessage( 'proxyblocker' )->text(),
|
|
'reason' => wfMessage( 'proxyblockreason' )->plain(),
|
|
'address' => $ip,
|
|
'systemBlock' => 'proxy',
|
|
] );
|
|
} elseif ( $isAnon && $this->isDnsBlacklisted( $ip ) ) {
|
|
$block = new Block( [
|
|
'byText' => wfMessage( 'sorbs' )->text(),
|
|
'reason' => wfMessage( 'sorbsreason' )->plain(),
|
|
'address' => $ip,
|
|
'systemBlock' => 'dnsbl',
|
|
] );
|
|
}
|
|
}
|
|
|
|
// (T25343) Apply IP blocks to the contents of XFF headers, if enabled
|
|
if ( !$block instanceof Block
|
|
&& $this->applyIpBlocksToXff
|
|
&& $ip !== null
|
|
&& !in_array( $ip, $this->proxyWhitelist )
|
|
) {
|
|
$xff = $request->getHeader( 'X-Forwarded-For' );
|
|
$xff = array_map( 'trim', explode( ',', $xff ) );
|
|
$xff = array_diff( $xff, [ $ip ] );
|
|
// TODO: remove dependency on Block
|
|
$xffblocks = Block::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
|
|
// TODO: remove dependency on Block
|
|
$block = Block::chooseBlock( $xffblocks, $xff );
|
|
if ( $block instanceof Block ) {
|
|
# Mangle the reason to alert the user that the block
|
|
# originated from matching the X-Forwarded-For header.
|
|
$block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
|
|
}
|
|
}
|
|
|
|
if ( !$block instanceof Block
|
|
&& $ip !== null
|
|
&& $isAnon
|
|
&& IP::isInRanges( $ip, $this->softBlockRanges )
|
|
) {
|
|
$block = new Block( [
|
|
'address' => $ip,
|
|
'byText' => 'MediaWiki default',
|
|
'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
|
|
'anonOnly' => true,
|
|
'systemBlock' => 'wgSoftBlockRanges',
|
|
] );
|
|
}
|
|
|
|
return $block;
|
|
}
|
|
|
|
/**
|
|
* Try to load a Block from an ID given in a cookie value.
|
|
*
|
|
* @param UserIdentity $user
|
|
* @param WebRequest $request
|
|
* @return Block|bool The Block object, or false if none could be loaded.
|
|
*/
|
|
private function getBlockFromCookieValue(
|
|
UserIdentity $user,
|
|
WebRequest $request
|
|
) {
|
|
$blockCookieVal = $request->getCookie( 'BlockID' );
|
|
$response = $request->response();
|
|
|
|
// Make sure there's something to check. The cookie value must start with a number.
|
|
if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
|
|
return false;
|
|
}
|
|
// Load the Block from the ID in the cookie.
|
|
// TODO: remove dependency on Block
|
|
$blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
|
|
if ( $blockCookieId !== null ) {
|
|
// An ID was found in the cookie.
|
|
// TODO: remove dependency on Block
|
|
$tmpBlock = Block::newFromID( $blockCookieId );
|
|
if ( $tmpBlock instanceof Block ) {
|
|
switch ( $tmpBlock->getType() ) {
|
|
case Block::TYPE_USER:
|
|
$blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
|
|
$useBlockCookie = ( $this->cookieSetOnAutoblock === true );
|
|
break;
|
|
case Block::TYPE_IP:
|
|
case Block::TYPE_RANGE:
|
|
// If block is type IP or IP range, load only if user is not logged in (T152462)
|
|
$blockIsValid = !$tmpBlock->isExpired() && $user->getId() === 0;
|
|
$useBlockCookie = ( $this->cookieSetOnIpBlock === true );
|
|
break;
|
|
default:
|
|
$blockIsValid = false;
|
|
$useBlockCookie = false;
|
|
}
|
|
|
|
if ( $blockIsValid && $useBlockCookie ) {
|
|
// Use the block.
|
|
return $tmpBlock;
|
|
}
|
|
|
|
// If the block is not valid, remove the cookie.
|
|
// TODO: remove dependency on Block
|
|
Block::clearCookie( $response );
|
|
} else {
|
|
// If the block doesn't exist, remove the cookie.
|
|
// TODO: remove dependency on Block
|
|
Block::clearCookie( $response );
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if an IP address is in the local proxy list
|
|
*
|
|
* @param string $ip
|
|
* @return bool
|
|
*/
|
|
private function isLocallyBlockedProxy( $ip ) {
|
|
if ( !$this->proxyList ) {
|
|
return false;
|
|
}
|
|
|
|
if ( !is_array( $this->proxyList ) ) {
|
|
// Load values from the specified file
|
|
$this->proxyList = array_map( 'trim', file( $this->proxyList ) );
|
|
}
|
|
|
|
$resultProxyList = [];
|
|
$deprecatedIPEntries = [];
|
|
|
|
// backward compatibility: move all ip addresses in keys to values
|
|
foreach ( $this->proxyList as $key => $value ) {
|
|
$keyIsIP = IP::isIPAddress( $key );
|
|
$valueIsIP = IP::isIPAddress( $value );
|
|
if ( $keyIsIP && !$valueIsIP ) {
|
|
$deprecatedIPEntries[] = $key;
|
|
$resultProxyList[] = $key;
|
|
} elseif ( $keyIsIP && $valueIsIP ) {
|
|
$deprecatedIPEntries[] = $key;
|
|
$resultProxyList[] = $key;
|
|
$resultProxyList[] = $value;
|
|
} else {
|
|
$resultProxyList[] = $value;
|
|
}
|
|
}
|
|
|
|
if ( $deprecatedIPEntries ) {
|
|
wfDeprecated(
|
|
'IP addresses in the keys of $wgProxyList (found the following IP addresses in keys: ' .
|
|
implode( ', ', $deprecatedIPEntries ) . ', please move them to values)', '1.30' );
|
|
}
|
|
|
|
$proxyListIPSet = new IPSet( $resultProxyList );
|
|
return $proxyListIPSet->match( $ip );
|
|
}
|
|
|
|
/**
|
|
* Whether the given IP is in a DNS blacklist.
|
|
*
|
|
* @param string $ip IP to check
|
|
* @param bool $checkWhitelist Whether to check the whitelist first
|
|
* @return bool True if blacklisted.
|
|
*/
|
|
public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
|
|
if ( !$this->enableDnsBlacklist ||
|
|
( $checkWhitelist && in_array( $ip, $this->proxyWhitelist ) )
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return $this->inDnsBlacklist( $ip, $this->dnsBlacklistUrls );
|
|
}
|
|
|
|
/**
|
|
* Whether the given IP is in a given DNS blacklist.
|
|
*
|
|
* @param string $ip IP to check
|
|
* @param array $bases Array of Strings: URL of the DNS blacklist
|
|
* @return bool True if blacklisted.
|
|
*/
|
|
private function inDnsBlacklist( $ip, array $bases ) {
|
|
$found = false;
|
|
// @todo FIXME: IPv6 ??? (https://bugs.php.net/bug.php?id=33170)
|
|
if ( IP::isIPv4( $ip ) ) {
|
|
// Reverse IP, T23255
|
|
$ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
|
|
|
|
foreach ( $bases as $base ) {
|
|
// Make hostname
|
|
// If we have an access key, use that too (ProjectHoneypot, etc.)
|
|
$basename = $base;
|
|
if ( is_array( $base ) ) {
|
|
if ( count( $base ) >= 2 ) {
|
|
// Access key is 1, base URL is 0
|
|
$host = "{$base[1]}.$ipReversed.{$base[0]}";
|
|
} else {
|
|
$host = "$ipReversed.{$base[0]}";
|
|
}
|
|
$basename = $base[0];
|
|
} else {
|
|
$host = "$ipReversed.$base";
|
|
}
|
|
|
|
// Send query
|
|
$ipList = gethostbynamel( $host );
|
|
|
|
if ( $ipList ) {
|
|
wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
|
|
$found = true;
|
|
break;
|
|
}
|
|
|
|
wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
|
|
}
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
}
|