X-Git-Url: http://git.cyclocoop.org/?a=blobdiff_plain;f=includes%2Fblock%2FBlockManager.php;h=be240ca10bdcac16f1ec49f15e4014fc303b4cba;hb=c5991f614ff3183ca4272ea33b08ec87e779a786;hp=ba4c569e1ced9788c19bdd4079e1e8190842893a;hpb=9792a09c306a6604a127a3a06f7b10cbc7e7a23f;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/block/BlockManager.php b/includes/block/BlockManager.php index ba4c569e1c..be240ca10b 100644 --- a/includes/block/BlockManager.php +++ b/includes/block/BlockManager.php @@ -20,11 +20,13 @@ namespace MediaWiki\Block; -use Block; +use DateTime; use IP; use MediaWiki\User\UserIdentity; +use MWCryptHash; use User; use WebRequest; +use WebResponse; use Wikimedia\IPSet; /** @@ -62,6 +64,9 @@ class BlockManager { /** @var array */ private $proxyWhitelist; + /** @var string|bool */ + private $secretKey; + /** @var array */ private $softBlockRanges; @@ -75,6 +80,7 @@ class BlockManager { * @param bool $enableDnsBlacklist * @param array $proxyList * @param array $proxyWhitelist + * @param string $secretKey * @param array $softBlockRanges */ public function __construct( @@ -87,6 +93,7 @@ class BlockManager { $enableDnsBlacklist, $proxyList, $proxyWhitelist, + $secretKey, $softBlockRanges ) { $this->currentUser = $currentUser; @@ -98,6 +105,7 @@ class BlockManager { $this->enableDnsBlacklist = $enableDnsBlacklist; $this->proxyList = $proxyList; $this->proxyWhitelist = $proxyWhitelist; + $this->secretKey = $secretKey; $this->softBlockRanges = $softBlockRanges; } @@ -111,7 +119,7 @@ class BlockManager { * @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. + * @return AbstractBlock|null The most relevant block, or null if there is no block. */ public function getUserBlock( User $user, $fromReplica ) { $isAnon = $user->getId() === 0; @@ -120,7 +128,7 @@ class BlockManager { // 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 + # 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; @@ -135,8 +143,8 @@ class BlockManager { } // User/IP blocking - // TODO: remove dependency on Block - $block = Block::newFromTarget( $user, $ip, !$fromReplica ); + // TODO: remove dependency on DatabaseBlock + $block = DatabaseBlock::newFromTarget( $user, $ip, !$fromReplica ); // Cookie blocking if ( !$block instanceof AbstractBlock ) { @@ -175,10 +183,10 @@ class BlockManager { $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 ); + // TODO: remove dependency on DatabaseBlock + $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, !$fromReplica ); + // TODO: remove dependency on DatabaseBlock + $block = DatabaseBlock::chooseBlock( $xffblocks, $xff ); if ( $block instanceof AbstractBlock ) { # Mangle the reason to alert the user that the block # originated from matching the X-Forwarded-For header. @@ -204,11 +212,11 @@ class BlockManager { } /** - * Try to load a Block from an ID given in a cookie value. + * 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. + * @return DatabaseBlock|bool The block object, or false if none could be loaded. */ private function getBlockFromCookieValue( UserIdentity $user, @@ -221,21 +229,20 @@ class BlockManager { 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 ); + // Load the block from the ID in the cookie. + $blockCookieId = $this->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 ) { + // TODO: remove dependency on DatabaseBlock + $tmpBlock = DatabaseBlock::newFromID( $blockCookieId ); + if ( $tmpBlock instanceof DatabaseBlock ) { switch ( $tmpBlock->getType() ) { - case Block::TYPE_USER: + case DatabaseBlock::TYPE_USER: $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking(); $useBlockCookie = ( $this->cookieSetOnAutoblock === true ); break; - case Block::TYPE_IP: - case Block::TYPE_RANGE: + case DatabaseBlock::TYPE_IP: + case DatabaseBlock::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 ); @@ -249,15 +256,9 @@ class BlockManager { // 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 ); } + // If the block is invalid or doesn't exist, remove the cookie. + $this->clearBlockCookie( $response ); } return false; } @@ -345,29 +346,169 @@ class BlockManager { if ( is_array( $base ) ) { if ( count( $base ) >= 2 ) { // Access key is 1, base URL is 0 - $host = "{$base[1]}.$ipReversed.{$base[0]}"; + $hostname = "{$base[1]}.$ipReversed.{$base[0]}"; } else { - $host = "$ipReversed.{$base[0]}"; + $hostname = "$ipReversed.{$base[0]}"; } $basename = $base[0]; } else { - $host = "$ipReversed.$base"; + $hostname = "$ipReversed.$base"; } // Send query - $ipList = gethostbynamel( $host ); + $ipList = $this->checkHost( $hostname ); if ( $ipList ) { - wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" ); + wfDebugLog( + 'dnsblacklist', + "Hostname $hostname is {$ipList[0]}, it's a proxy says $basename!" + ); $found = true; break; } - wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." ); + wfDebugLog( 'dnsblacklist', "Requested $hostname, not found in $basename." ); } } return $found; } + /** + * Wrapper for mocking in tests. + * + * @param string $hostname DNSBL query + * @return string[]|bool IPv4 array, or false if the IP is not blacklisted + */ + protected function checkHost( $hostname ) { + return gethostbynamel( $hostname ); + } + + /** + * Set the 'BlockID' cookie depending on block type and user authentication status. + * + * @since 1.34 + * @param User $user + */ + public function trackBlockWithCookie( User $user ) { + $block = $user->getBlock(); + $request = $user->getRequest(); + + if ( + $block && + $request->getCookie( 'BlockID' ) === null && + $this->shouldTrackBlockWithCookie( $block, $user->isAnon() ) + ) { + $this->setBlockCookie( $block, $request->response() ); + } + } + + /** + * Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be + * the same as the block's, to a maximum of 24 hours. + * + * @since 1.34 + * @internal Should be private. + * Left public for backwards compatibility, until DatabaseBlock::setCookie is removed. + * @param DatabaseBlock $block + * @param WebResponse $response The response on which to set the cookie. + */ + public function setBlockCookie( DatabaseBlock $block, WebResponse $response ) { + // Calculate the default expiry time. + $maxExpiryTime = wfTimestamp( TS_MW, wfTimestamp() + ( 24 * 60 * 60 ) ); + + // Use the block's expiry time only if it's less than the default. + $expiryTime = $block->getExpiry(); + if ( $expiryTime === 'infinity' || $expiryTime > $maxExpiryTime ) { + $expiryTime = $maxExpiryTime; + } + + // Set the cookie. Reformat the MediaWiki datetime as a Unix timestamp for the cookie. + $expiryValue = DateTime::createFromFormat( 'YmdHis', $expiryTime )->format( 'U' ); + $cookieOptions = [ 'httpOnly' => false ]; + $cookieValue = $this->getCookieValue( $block ); + $response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions ); + } + + /** + * Check if the block should be tracked with a cookie. + * + * @param AbstractBlock $block + * @param bool $isAnon The user is logged out + * @return bool The block sould be tracked with a cookie + */ + private function shouldTrackBlockWithCookie( AbstractBlock $block, $isAnon ) { + if ( $block instanceof DatabaseBlock ) { + switch ( $block->getType() ) { + case DatabaseBlock::TYPE_IP: + case DatabaseBlock::TYPE_RANGE: + return $isAnon && $this->cookieSetOnIpBlock; + case DatabaseBlock::TYPE_USER: + return !$isAnon && $this->cookieSetOnAutoblock && $block->isAutoblocking(); + default: + return false; + } + } + return false; + } + + /** + * Unset the 'BlockID' cookie. + * + * @since 1.34 + * @param WebResponse $response + */ + public static function clearBlockCookie( WebResponse $response ) { + $response->clearCookie( 'BlockID', [ 'httpOnly' => false ] ); + } + + /** + * Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of + * the ID and a HMAC (see DatabaseBlock::setCookie), but will sometimes only be the ID. + * + * @since 1.34 + * @internal Should be private. + * Left public for backwards compatibility, until DatabaseBlock::getIdFromCookieValue is removed. + * @param string $cookieValue The string in which to find the ID. + * @return int|null The block ID, or null if the HMAC is present and invalid. + */ + public function getIdFromCookieValue( $cookieValue ) { + // Extract the ID prefix from the cookie value (may be the whole value, if no bang found). + $bangPos = strpos( $cookieValue, '!' ); + $id = ( $bangPos === false ) ? $cookieValue : substr( $cookieValue, 0, $bangPos ); + if ( !$this->secretKey ) { + // If there's no secret key, just use the ID as given. + return $id; + } + $storedHmac = substr( $cookieValue, $bangPos + 1 ); + $calculatedHmac = MWCryptHash::hmac( $id, $this->secretKey, false ); + if ( $calculatedHmac === $storedHmac ) { + return $id; + } else { + return null; + } + } + + /** + * Get the BlockID cookie's value for this block. This is usually the block ID concatenated + * with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just + * be the block ID. + * + * @since 1.34 + * @internal Should be private. + * Left public for backwards compatibility, until DatabaseBlock::getCookieValue is removed. + * @param DatabaseBlock $block + * @return string The block ID, probably concatenated with "!" and the HMAC. + */ + public function getCookieValue( DatabaseBlock $block ) { + $id = $block->getId(); + if ( !$this->secretKey ) { + // If there's no secret key, don't append a HMAC. + return $id; + } + $hmac = MWCryptHash::hmac( $id, $this->secretKey, false ); + $cookieValue = $id . '!' . $hmac; + return $cookieValue; + } + }