Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
38.00% |
19 / 50 |
CRAP | |
70.49% |
418 / 593 |
Block | |
0.00% |
0 / 1 |
|
39.22% |
20 / 51 |
1379.03 | |
70.49% |
418 / 593 |
__construct | |
0.00% |
0 / 1 |
3.00 | |
93.75% |
15 / 16 |
|||
newFromID | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 12 |
|||
selectFields | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
getQueryInfo | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
equals | |
0.00% |
0 / 1 |
26.30 | |
57.14% |
8 / 14 |
|||
newLoad | |
0.00% |
0 / 1 |
16.18 | |
82.61% |
38 / 46 |
|||
getRangeCond | |
100.00% |
1 / 1 |
2 | |
100.00% |
12 / 12 |
|||
getIpFragment | |
0.00% |
0 / 1 |
2.06 | |
75.00% |
3 / 4 |
|||
initFromRow | |
100.00% |
1 / 1 |
1 | |
100.00% |
20 / 20 |
|||
newFromRow | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
delete | |
0.00% |
0 / 1 |
3.07 | |
80.00% |
8 / 10 |
|||
insert | |
0.00% |
0 / 1 |
15.09 | |
72.22% |
26 / 36 |
|||
update | |
0.00% |
0 / 1 |
7.56 | |
77.42% |
24 / 31 |
|||
getDatabaseArray | |
0.00% |
0 / 1 |
3 | |
95.45% |
21 / 22 |
|||
getAutoblockUpdateArray | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
doRetroactiveAutoblock | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
defaultRetroactiveAutoblock | |
0.00% |
0 / 1 |
7.20 | |
84.00% |
21 / 25 |
|||
isWhitelistedFromAutoblocks | |
0.00% |
0 / 1 |
4.69 | |
65.00% |
13 / 20 |
|||
anonymous function | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
doAutoblock | |
0.00% |
0 / 1 |
9.10 | |
89.19% |
33 / 37 |
|||
deleteIfExpired | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 7 |
|||
isExpired | |
0.00% |
0 / 1 |
2.03 | |
80.00% |
4 / 5 |
|||
isValid | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
updateTimestamp | |
100.00% |
1 / 1 |
2 | |
100.00% |
10 / 10 |
|||
getRangeStart | |
0.00% |
0 / 1 |
4.02 | |
88.89% |
8 / 9 |
|||
getRangeEnd | |
0.00% |
0 / 1 |
4.02 | |
88.89% |
8 / 9 |
|||
getId | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
setId | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
fromMaster | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
isHardblock | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
isAutoblocking | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
getRedactedName | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
getAutoblockExpiry | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
purgeExpired | |
0.00% |
0 / 1 |
3.17 | |
73.33% |
11 / 15 |
|||
newFromTarget | |
0.00% |
0 / 1 |
8.02 | |
93.33% |
14 / 15 |
|||
getBlocksForIPList | |
0.00% |
0 / 1 |
10.08 | |
90.91% |
30 / 33 |
|||
chooseBlock | |
0.00% |
0 / 1 |
825.14 | |
8.16% |
4 / 49 |
|||
getType | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
setCookie | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 9 |
|||
clearCookie | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
getCookieValue | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
getIdFromCookieValue | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 11 |
|||
getPermissionsError | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
getRestrictions | |
0.00% |
0 / 1 |
3.07 | |
80.00% |
4 / 5 |
|||
setRestrictions | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
appliesToTitle | |
0.00% |
0 / 1 |
4.05 | |
85.71% |
6 / 7 |
|||
appliesToNamespace | |
0.00% |
0 / 1 |
3.33 | |
66.67% |
4 / 6 |
|||
appliesToPage | |
0.00% |
0 / 1 |
3.04 | |
83.33% |
5 / 6 |
|||
findRestriction | |
100.00% |
1 / 1 |
4 | |
100.00% |
7 / 7 |
|||
shouldTrackWithCookie | |
100.00% |
1 / 1 |
7 | |
100.00% |
8 / 8 |
|||
getBlockRestrictionStore | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
<?php | |
/** | |
* Class for blocks stored in the database. | |
* | |
* 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 | |
*/ | |
use Wikimedia\Rdbms\Database; | |
use Wikimedia\Rdbms\IDatabase; | |
use MediaWiki\Block\AbstractBlock; | |
use MediaWiki\Block\BlockRestrictionStore; | |
use MediaWiki\Block\Restriction\Restriction; | |
use MediaWiki\Block\Restriction\NamespaceRestriction; | |
use MediaWiki\Block\Restriction\PageRestriction; | |
use MediaWiki\MediaWikiServices; | |
/** | |
* Blocks (as opposed to system blocks) are stored in the database, may | |
* give rise to autoblocks and may be tracked with cookies. Blocks are | |
* more customizable than system blocks: they may be hardblocks, and | |
* they may be sitewide or partial. | |
*/ | |
class Block extends AbstractBlock { | |
/** @var bool */ | |
public $mAuto; | |
/** @var int */ | |
public $mParentBlockId; | |
/** @var int */ | |
private $mId; | |
/** @var bool */ | |
private $mFromMaster; | |
/** @var int Hack for foreign blocking (CentralAuth) */ | |
private $forcedTargetID; | |
/** @var bool */ | |
private $isHardblock; | |
/** @var bool */ | |
private $isAutoblocking; | |
/** @var Restriction[] */ | |
private $restrictions; | |
/** | |
* Create a new block with specified option parameters on a user, IP or IP range. | |
* | |
* @param array $options Parameters of the block: | |
* user int Override target user ID (for foreign users) | |
* auto bool Is this an automatic block? | |
* expiry string Timestamp of expiration of the block or 'infinity' | |
* anonOnly bool Only disallow anonymous actions | |
* createAccount bool Disallow creation of new accounts | |
* enableAutoblock bool Enable automatic blocking | |
* hideName bool Hide the target user name | |
* blockEmail bool Disallow sending emails | |
* allowUsertalk bool Allow the target to edit its own talk page | |
* sitewide bool Disallow editing all pages and all contribution | |
* actions, except those specifically allowed by | |
* other block flags | |
* | |
* @since 1.26 $options array | |
*/ | |
public function __construct( array $options = [] ) { | |
parent::__construct( $options ); | |
$defaults = [ | |
'user' => null, | |
'auto' => false, | |
'expiry' => '', | |
'anonOnly' => false, | |
'createAccount' => false, | |
'enableAutoblock' => false, | |
'hideName' => false, | |
'blockEmail' => false, | |
'allowUsertalk' => false, | |
'sitewide' => true, | |
]; | |
$options += $defaults; | |
if ( $this->target instanceof User && $options['user'] ) { | |
# Needed for foreign users | |
$this->forcedTargetID = $options['user']; | |
} | |
$this->setExpiry( wfGetDB( DB_REPLICA )->decodeExpiry( $options['expiry'] ) ); | |
# Boolean settings | |
$this->mAuto = (bool)$options['auto']; | |
$this->setHideName( (bool)$options['hideName'] ); | |
$this->isHardblock( !$options['anonOnly'] ); | |
$this->isAutoblocking( (bool)$options['enableAutoblock'] ); | |
$this->isSitewide( (bool)$options['sitewide'] ); | |
$this->isEmailBlocked( (bool)$options['blockEmail'] ); | |
$this->isCreateAccountBlocked( (bool)$options['createAccount'] ); | |
$this->isUsertalkEditAllowed( (bool)$options['allowUsertalk'] ); | |
$this->mFromMaster = false; | |
} | |
/** | |
* Load a block from the block id. | |
* | |
* @param int $id Block id to search for | |
* @return Block|null | |
*/ | |
public static function newFromID( $id ) { | |
$dbr = wfGetDB( DB_REPLICA ); | |
$blockQuery = self::getQueryInfo(); | |
$res = $dbr->selectRow( | |
$blockQuery['tables'], | |
$blockQuery['fields'], | |
[ 'ipb_id' => $id ], | |
__METHOD__, | |
[], | |
$blockQuery['joins'] | |
); | |
if ( $res ) { | |
return self::newFromRow( $res ); | |
} else { | |
return null; | |
} | |
} | |
/** | |
* Return the list of ipblocks fields that should be selected to create | |
* a new block. | |
* @deprecated since 1.31, use self::getQueryInfo() instead. | |
* @return array | |
*/ | |
public static function selectFields() { | |
global $wgActorTableSchemaMigrationStage; | |
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) { | |
// If code is using this instead of self::getQueryInfo(), there's a | |
// decent chance it's going to try to directly access | |
// $row->ipb_by or $row->ipb_by_text and we can't give it | |
// useful values here once those aren't being used anymore. | |
throw new BadMethodCallException( | |
'Cannot use ' . __METHOD__ | |
. ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW' | |
); | |
} | |
wfDeprecated( __METHOD__, '1.31' ); | |
return [ | |
'ipb_id', | |
'ipb_address', | |
'ipb_by', | |
'ipb_by_text', | |
'ipb_by_actor' => 'NULL', | |
'ipb_timestamp', | |
'ipb_auto', | |
'ipb_anon_only', | |
'ipb_create_account', | |
'ipb_enable_autoblock', | |
'ipb_expiry', | |
'ipb_deleted', | |
'ipb_block_email', | |
'ipb_allow_usertalk', | |
'ipb_parent_block_id', | |
'ipb_sitewide', | |
] + CommentStore::getStore()->getFields( 'ipb_reason' ); | |
} | |
/** | |
* Return the tables, fields, and join conditions to be selected to create | |
* a new block object. | |
* @since 1.31 | |
* @return array With three keys: | |
* - tables: (string[]) to include in the `$table` to `IDatabase->select()` | |
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()` | |
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()` | |
*/ | |
public static function getQueryInfo() { | |
$commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' ); | |
$actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' ); | |
return [ | |
'tables' => [ 'ipblocks' ] + $commentQuery['tables'] + $actorQuery['tables'], | |
'fields' => [ | |
'ipb_id', | |
'ipb_address', | |
'ipb_timestamp', | |
'ipb_auto', | |
'ipb_anon_only', | |
'ipb_create_account', | |
'ipb_enable_autoblock', | |
'ipb_expiry', | |
'ipb_deleted', | |
'ipb_block_email', | |
'ipb_allow_usertalk', | |
'ipb_parent_block_id', | |
'ipb_sitewide', | |
] + $commentQuery['fields'] + $actorQuery['fields'], | |
'joins' => $commentQuery['joins'] + $actorQuery['joins'], | |
]; | |
} | |
/** | |
* Check if two blocks are effectively equal. Doesn't check irrelevant things like | |
* the blocking user or the block timestamp, only things which affect the blocked user | |
* | |
* @param Block $block | |
* | |
* @return bool | |
*/ | |
public function equals( Block $block ) { | |
return ( | |
(string)$this->target == (string)$block->target | |
&& $this->type == $block->type | |
&& $this->mAuto == $block->mAuto | |
&& $this->isHardblock() == $block->isHardblock() | |
&& $this->isCreateAccountBlocked() == $block->isCreateAccountBlocked() | |
&& $this->getExpiry() == $block->getExpiry() | |
&& $this->isAutoblocking() == $block->isAutoblocking() | |
&& $this->getHideName() == $block->getHideName() | |
&& $this->isEmailBlocked() == $block->isEmailBlocked() | |
&& $this->isUsertalkEditAllowed() == $block->isUsertalkEditAllowed() | |
&& $this->getReason() == $block->getReason() | |
&& $this->isSitewide() == $block->isSitewide() | |
// Block::getRestrictions() may perform a database query, so keep it at | |
// the end. | |
&& $this->getBlockRestrictionStore()->equals( | |
$this->getRestrictions(), $block->getRestrictions() | |
) | |
); | |
} | |
/** | |
* Load a block from the database which affects the already-set $this->target: | |
* 1) A block directly on the given user or IP | |
* 2) A rangeblock encompassing the given IP (smallest first) | |
* 3) An autoblock on the given IP | |
* @param User|string|null $vagueTarget Also search for blocks affecting this target. Doesn't | |
* make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups. | |
* @throws MWException | |
* @return bool Whether a relevant block was found | |
*/ | |
protected function newLoad( $vagueTarget = null ) { | |
$db = wfGetDB( $this->mFromMaster ? DB_MASTER : DB_REPLICA ); | |
if ( $this->type !== null ) { | |
$conds = [ | |
'ipb_address' => [ (string)$this->target ], | |
]; | |
} else { | |
$conds = [ 'ipb_address' => [] ]; | |
} | |
# Be aware that the != '' check is explicit, since empty values will be | |
# passed by some callers (T31116) | |
if ( $vagueTarget != '' ) { | |
list( $target, $type ) = self::parseTarget( $vagueTarget ); | |
switch ( $type ) { | |
case self::TYPE_USER: | |
# Slightly weird, but who are we to argue? | |
$conds['ipb_address'][] = (string)$target; | |
break; | |
case self::TYPE_IP: | |
$conds['ipb_address'][] = (string)$target; | |
$conds[] = self::getRangeCond( IP::toHex( $target ) ); | |
$conds = $db->makeList( $conds, LIST_OR ); | |
break; | |
case self::TYPE_RANGE: | |
list( $start, $end ) = IP::parseRange( $target ); | |
$conds['ipb_address'][] = (string)$target; | |
$conds[] = self::getRangeCond( $start, $end ); | |
$conds = $db->makeList( $conds, LIST_OR ); | |
break; | |
default: | |
throw new MWException( "Tried to load block with invalid type" ); | |
} | |
} | |
$blockQuery = self::getQueryInfo(); | |
$res = $db->select( | |
$blockQuery['tables'], $blockQuery['fields'], $conds, __METHOD__, [], $blockQuery['joins'] | |
); | |
# This result could contain a block on the user, a block on the IP, and a russian-doll | |
# set of rangeblocks. We want to choose the most specific one, so keep a leader board. | |
$bestRow = null; | |
# Lower will be better | |
$bestBlockScore = 100; | |
foreach ( $res as $row ) { | |
$block = self::newFromRow( $row ); | |
# Don't use expired blocks | |
if ( $block->isExpired() ) { | |
continue; | |
} | |
# Don't use anon only blocks on users | |
if ( $this->type == self::TYPE_USER && !$block->isHardblock() ) { | |
continue; | |
} | |
if ( $block->getType() == self::TYPE_RANGE ) { | |
# This is the number of bits that are allowed to vary in the block, give | |
# or take some floating point errors | |
$target = $block->getTarget(); | |
$max = IP::isIPv6( $target ) ? 128 : 32; | |
list( $network, $bits ) = IP::parseCIDR( $target ); | |
$size = $max - $bits; | |
# Rank a range block covering a single IP equally with a single-IP block | |
$score = self::TYPE_RANGE - 1 + ( $size / $max ); | |
} else { | |
$score = $block->getType(); | |
} | |
if ( $score < $bestBlockScore ) { | |
$bestBlockScore = $score; | |
$bestRow = $row; | |
} | |
} | |
if ( $bestRow !== null ) { | |
$this->initFromRow( $bestRow ); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Get a set of SQL conditions which will select rangeblocks encompassing a given range | |
* @param string $start Hexadecimal IP representation | |
* @param string|null $end Hexadecimal IP representation, or null to use $start = $end | |
* @return string | |
*/ | |
public static function getRangeCond( $start, $end = null ) { | |
if ( $end === null ) { | |
$end = $start; | |
} | |
# Per T16634, we want to include relevant active rangeblocks; for | |
# rangeblocks, we want to include larger ranges which enclose the given | |
# range. We know that all blocks must be smaller than $wgBlockCIDRLimit, | |
# so we can improve performance by filtering on a LIKE clause | |
$chunk = self::getIpFragment( $start ); | |
$dbr = wfGetDB( DB_REPLICA ); | |
$like = $dbr->buildLike( $chunk, $dbr->anyString() ); | |
# Fairly hard to make a malicious SQL statement out of hex characters, | |
# but stranger things have happened... | |
$safeStart = $dbr->addQuotes( $start ); | |
$safeEnd = $dbr->addQuotes( $end ); | |
return $dbr->makeList( | |
[ | |
"ipb_range_start $like", | |
"ipb_range_start <= $safeStart", | |
"ipb_range_end >= $safeEnd", | |
], | |
LIST_AND | |
); | |
} | |
/** | |
* Get the component of an IP address which is certain to be the same between an IP | |
* address and a rangeblock containing that IP address. | |
* @param string $hex Hexadecimal IP representation | |
* @return string | |
*/ | |
protected static function getIpFragment( $hex ) { | |
global $wgBlockCIDRLimit; | |
if ( substr( $hex, 0, 3 ) == 'v6-' ) { | |
return 'v6-' . substr( substr( $hex, 3 ), 0, floor( $wgBlockCIDRLimit['IPv6'] / 4 ) ); | |
} else { | |
return substr( $hex, 0, floor( $wgBlockCIDRLimit['IPv4'] / 4 ) ); | |
} | |
} | |
/** | |
* Given a database row from the ipblocks table, initialize | |
* member variables | |
* @param stdClass $row A row from the ipblocks table | |
*/ | |
protected function initFromRow( $row ) { | |
$this->setTarget( $row->ipb_address ); | |
$this->setBlocker( User::newFromAnyId( | |
$row->ipb_by, $row->ipb_by_text, $row->ipb_by_actor ?? null | |
) ); | |
$this->setTimestamp( wfTimestamp( TS_MW, $row->ipb_timestamp ) ); | |
$this->mAuto = $row->ipb_auto; | |
$this->setHideName( $row->ipb_deleted ); | |
$this->mId = (int)$row->ipb_id; | |
$this->mParentBlockId = $row->ipb_parent_block_id; | |
// I wish I didn't have to do this | |
$db = wfGetDB( DB_REPLICA ); | |
$this->setExpiry( $db->decodeExpiry( $row->ipb_expiry ) ); | |
$this->setReason( | |
CommentStore::getStore() | |
// Legacy because $row may have come from self::selectFields() | |
->getCommentLegacy( $db, 'ipb_reason', $row )->text | |
); | |
$this->isHardblock( !$row->ipb_anon_only ); | |
$this->isAutoblocking( $row->ipb_enable_autoblock ); | |
$this->isSitewide( (bool)$row->ipb_sitewide ); | |
$this->isCreateAccountBlocked( $row->ipb_create_account ); | |
$this->isEmailBlocked( $row->ipb_block_email ); | |
$this->isUsertalkEditAllowed( $row->ipb_allow_usertalk ); | |
} | |
/** | |
* Create a new Block object from a database row | |
* @param stdClass $row Row from the ipblocks table | |
* @return Block | |
*/ | |
public static function newFromRow( $row ) { | |
$block = new Block; | |
$block->initFromRow( $row ); | |
return $block; | |
} | |
/** | |
* Delete the row from the IP blocks table. | |
* | |
* @throws MWException | |
* @return bool | |
*/ | |
public function delete() { | |
if ( wfReadOnly() ) { | |
return false; | |
} | |
if ( !$this->getId() ) { | |
throw new MWException( "Block::delete() requires that the mId member be filled\n" ); | |
} | |
$dbw = wfGetDB( DB_MASTER ); | |
$this->getBlockRestrictionStore()->deleteByParentBlockId( $this->getId() ); | |
$dbw->delete( 'ipblocks', [ 'ipb_parent_block_id' => $this->getId() ], __METHOD__ ); | |
$this->getBlockRestrictionStore()->deleteByBlockId( $this->getId() ); | |
$dbw->delete( 'ipblocks', [ 'ipb_id' => $this->getId() ], __METHOD__ ); | |
return $dbw->affectedRows() > 0; | |
} | |
/** | |
* Insert a block into the block table. Will fail if there is a conflicting | |
* block (same name and options) already in the database. | |
* | |
* @param IDatabase|null $dbw If you have one available | |
* @return bool|array False on failure, assoc array on success: | |
* ('id' => block ID, 'autoIds' => array of autoblock IDs) | |
*/ | |
public function insert( $dbw = null ) { | |
global $wgBlockDisablesLogin; | |
if ( !$this->getBlocker() || $this->getBlocker()->getName() === '' ) { | |
throw new MWException( 'Cannot insert a block without a blocker set' ); | |
} | |
wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); | |
if ( $dbw === null ) { | |
$dbw = wfGetDB( DB_MASTER ); | |
} | |
self::purgeExpired(); | |
$row = $this->getDatabaseArray( $dbw ); | |
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] ); | |
$affected = $dbw->affectedRows(); | |
if ( $affected ) { | |
$this->setId( $dbw->insertId() ); | |
if ( $this->restrictions ) { | |
$this->getBlockRestrictionStore()->insert( $this->restrictions ); | |
} | |
} | |
# Don't collide with expired blocks. | |
# Do this after trying to insert to avoid locking. | |
if ( !$affected ) { | |
# T96428: The ipb_address index uses a prefix on a field, so | |
# use a standard SELECT + DELETE to avoid annoying gap locks. | |
$ids = $dbw->selectFieldValues( 'ipblocks', | |
'ipb_id', | |
[ | |
'ipb_address' => $row['ipb_address'], | |
'ipb_user' => $row['ipb_user'], | |
'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) | |
], | |
__METHOD__ | |
); | |
if ( $ids ) { | |
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ ); | |
$this->getBlockRestrictionStore()->deleteByBlockId( $ids ); | |
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] ); | |
$affected = $dbw->affectedRows(); | |
$this->setId( $dbw->insertId() ); | |
if ( $this->restrictions ) { | |
$this->getBlockRestrictionStore()->insert( $this->restrictions ); | |
} | |
} | |
} | |
if ( $affected ) { | |
$auto_ipd_ids = $this->doRetroactiveAutoblock(); | |
if ( $wgBlockDisablesLogin && $this->target instanceof User ) { | |
// Change user login token to force them to be logged out. | |
$this->target->setToken(); | |
$this->target->saveSettings(); | |
} | |
return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ]; | |
} | |
return false; | |
} | |
/** | |
* Update a block in the DB with new parameters. | |
* The ID field needs to be loaded first. | |
* | |
* @return bool|array False on failure, array on success: | |
* ('id' => block ID, 'autoIds' => array of autoblock IDs) | |
*/ | |
public function update() { | |
wfDebug( "Block::update; timestamp {$this->mTimestamp}\n" ); | |
$dbw = wfGetDB( DB_MASTER ); | |
$dbw->startAtomic( __METHOD__ ); | |
$result = $dbw->update( | |
'ipblocks', | |
$this->getDatabaseArray( $dbw ), | |
[ 'ipb_id' => $this->getId() ], | |
__METHOD__ | |
); | |
// Only update the restrictions if they have been modified. | |
if ( $this->restrictions !== null ) { | |
// An empty array should remove all of the restrictions. | |
if ( empty( $this->restrictions ) ) { | |
$success = $this->getBlockRestrictionStore()->deleteByBlockId( $this->getId() ); | |
} else { | |
$success = $this->getBlockRestrictionStore()->update( $this->restrictions ); | |
} | |
// Update the result. The first false is the result, otherwise, true. | |
$result = $result && $success; | |
} | |
if ( $this->isAutoblocking() ) { | |
// update corresponding autoblock(s) (T50813) | |
$dbw->update( | |
'ipblocks', | |
$this->getAutoblockUpdateArray( $dbw ), | |
[ 'ipb_parent_block_id' => $this->getId() ], | |
__METHOD__ | |
); | |
// Only update the restrictions if they have been modified. | |
if ( $this->restrictions !== null ) { | |
$this->getBlockRestrictionStore()->updateByParentBlockId( $this->getId(), $this->restrictions ); | |
} | |
} else { | |
// autoblock no longer required, delete corresponding autoblock(s) | |
$this->getBlockRestrictionStore()->deleteByParentBlockId( $this->getId() ); | |
$dbw->delete( | |
'ipblocks', | |
[ 'ipb_parent_block_id' => $this->getId() ], | |
__METHOD__ | |
); | |
} | |
$dbw->endAtomic( __METHOD__ ); | |
if ( $result ) { | |
$auto_ipd_ids = $this->doRetroactiveAutoblock(); | |
return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ]; | |
} | |
return $result; | |
} | |
/** | |
* Get an array suitable for passing to $dbw->insert() or $dbw->update() | |
* @param IDatabase $dbw | |
* @return array | |
*/ | |
protected function getDatabaseArray( IDatabase $dbw ) { | |
$expiry = $dbw->encodeExpiry( $this->getExpiry() ); | |
if ( $this->forcedTargetID ) { | |
$uid = $this->forcedTargetID; | |
} else { | |
$uid = $this->target instanceof User ? $this->target->getId() : 0; | |
} | |
$a = [ | |
'ipb_address' => (string)$this->target, | |
'ipb_user' => $uid, | |
'ipb_timestamp' => $dbw->timestamp( $this->getTimestamp() ), | |
'ipb_auto' => $this->mAuto, | |
'ipb_anon_only' => !$this->isHardblock(), | |
'ipb_create_account' => $this->isCreateAccountBlocked(), | |
'ipb_enable_autoblock' => $this->isAutoblocking(), | |
'ipb_expiry' => $expiry, | |
'ipb_range_start' => $this->getRangeStart(), | |
'ipb_range_end' => $this->getRangeEnd(), | |
'ipb_deleted' => intval( $this->getHideName() ), // typecast required for SQLite | |
'ipb_block_email' => $this->isEmailBlocked(), | |
'ipb_allow_usertalk' => $this->isUsertalkEditAllowed(), | |
'ipb_parent_block_id' => $this->mParentBlockId, | |
'ipb_sitewide' => $this->isSitewide(), | |
] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->getReason() ) | |
+ ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() ); | |
return $a; | |
} | |
/** | |
* @param IDatabase $dbw | |
* @return array | |
*/ | |
protected function getAutoblockUpdateArray( IDatabase $dbw ) { | |
return [ | |
'ipb_create_account' => $this->isCreateAccountBlocked(), | |
'ipb_deleted' => (int)$this->getHideName(), // typecast required for SQLite | |
'ipb_allow_usertalk' => $this->isUsertalkEditAllowed(), | |
'ipb_sitewide' => $this->isSitewide(), | |
] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->getReason() ) | |
+ ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() ); | |
} | |
/** | |
* Retroactively autoblocks the last IP used by the user (if it is a user) | |
* blocked by this Block. | |
* | |
* @return array Block IDs of retroactive autoblocks made | |
*/ | |
protected function doRetroactiveAutoblock() { | |
$blockIds = []; | |
# If autoblock is enabled, autoblock the LAST IP(s) used | |
if ( $this->isAutoblocking() && $this->getType() == self::TYPE_USER ) { | |
wfDebug( "Doing retroactive autoblocks for " . $this->getTarget() . "\n" ); | |
$continue = Hooks::run( | |
'PerformRetroactiveAutoblock', [ $this, &$blockIds ] ); | |
if ( $continue ) { | |
self::defaultRetroactiveAutoblock( $this, $blockIds ); | |
} | |
} | |
return $blockIds; | |
} | |
/** | |
* Retroactively autoblocks the last IP used by the user (if it is a user) | |
* blocked by this Block. This will use the recentchanges table. | |
* | |
* @param Block $block | |
* @param array &$blockIds | |
*/ | |
protected static function defaultRetroactiveAutoblock( Block $block, array &$blockIds ) { | |
global $wgPutIPinRC; | |
// No IPs are in recentchanges table, so nothing to select | |
if ( !$wgPutIPinRC ) { | |
return; | |
} | |
// Autoblocks only apply to TYPE_USER | |
if ( $block->getType() !== self::TYPE_USER ) { | |
return; | |
} | |
$target = $block->getTarget(); // TYPE_USER => always a User object | |
$dbr = wfGetDB( DB_REPLICA ); | |
$rcQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $target, false ); | |
$options = [ 'ORDER BY' => 'rc_timestamp DESC' ]; | |
// Just the last IP used. | |
$options['LIMIT'] = 1; | |
$res = $dbr->select( | |
[ 'recentchanges' ] + $rcQuery['tables'], | |
[ 'rc_ip' ], | |
$rcQuery['conds'], | |
__METHOD__, | |
$options, | |
$rcQuery['joins'] | |
); | |
if ( !$res->numRows() ) { | |
# No results, don't autoblock anything | |
wfDebug( "No IP found to retroactively autoblock\n" ); | |
} else { | |
foreach ( $res as $row ) { | |
if ( $row->rc_ip ) { | |
$id = $block->doAutoblock( $row->rc_ip ); | |
if ( $id ) { | |
$blockIds[] = $id; | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Checks whether a given IP is on the autoblock whitelist. | |
* TODO: this probably belongs somewhere else, but not sure where... | |
* | |
* @param string $ip The IP to check | |
* @return bool | |
*/ | |
public static function isWhitelistedFromAutoblocks( $ip ) { | |
// Try to get the autoblock_whitelist from the cache, as it's faster | |
// than getting the msg raw and explode()'ing it. | |
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); | |
$lines = $cache->getWithSetCallback( | |
$cache->makeKey( 'ip-autoblock', 'whitelist' ), | |
$cache::TTL_DAY, | |
function ( $curValue, &$ttl, array &$setOpts ) { | |
$setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); | |
return explode( "\n", | |
wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() ); | |
} | |
); | |
wfDebug( "Checking the autoblock whitelist..\n" ); | |
foreach ( $lines as $line ) { | |
# List items only | |
if ( substr( $line, 0, 1 ) !== '*' ) { | |
continue; | |
} | |
$wlEntry = substr( $line, 1 ); | |
$wlEntry = trim( $wlEntry ); | |
wfDebug( "Checking $ip against $wlEntry..." ); | |
# Is the IP in this range? | |
if ( IP::isInRange( $ip, $wlEntry ) ) { | |
wfDebug( " IP $ip matches $wlEntry, not autoblocking\n" ); | |
return true; | |
} else { | |
wfDebug( " No match\n" ); | |
} | |
} | |
return false; | |
} | |
/** | |
* Autoblocks the given IP, referring to this Block. | |
* | |
* @param string $autoblockIP The IP to autoblock. | |
* @return int|bool Block ID if an autoblock was inserted, false if not. | |
*/ | |
public function doAutoblock( $autoblockIP ) { | |
# If autoblocks are disabled, go away. | |
if ( !$this->isAutoblocking() ) { | |
return false; | |
} | |
# Check for presence on the autoblock whitelist. | |
if ( self::isWhitelistedFromAutoblocks( $autoblockIP ) ) { | |
return false; | |
} | |
// Avoid PHP 7.1 warning of passing $this by reference | |
$block = $this; | |
# Allow hooks to cancel the autoblock. | |
if ( !Hooks::run( 'AbortAutoblock', [ $autoblockIP, &$block ] ) ) { | |
wfDebug( "Autoblock aborted by hook.\n" ); | |
return false; | |
} | |
# It's okay to autoblock. Go ahead and insert/update the block... | |
# Do not add a *new* block if the IP is already blocked. | |
$ipblock = self::newFromTarget( $autoblockIP ); | |
if ( $ipblock ) { | |
# Check if the block is an autoblock and would exceed the user block | |
# if renewed. If so, do nothing, otherwise prolong the block time... | |
if ( $ipblock->mAuto && // @todo Why not compare $ipblock->mExpiry? | |
$this->getExpiry() > self::getAutoblockExpiry( $ipblock->getTimestamp() ) | |
) { | |
# Reset block timestamp to now and its expiry to | |
# $wgAutoblockExpiry in the future | |
$ipblock->updateTimestamp(); | |
} | |
return false; | |
} | |
# Make a new block object with the desired properties. | |
$autoblock = new Block; | |
wfDebug( "Autoblocking {$this->getTarget()}@" . $autoblockIP . "\n" ); | |
$autoblock->setTarget( $autoblockIP ); | |
$autoblock->setBlocker( $this->getBlocker() ); | |
$autoblock->setReason( | |
wfMessage( 'autoblocker', $this->getTarget(), $this->getReason() ) | |
->inContentLanguage()->plain() | |
); | |
$timestamp = wfTimestampNow(); | |
$autoblock->setTimestamp( $timestamp ); | |
$autoblock->mAuto = 1; | |
$autoblock->isCreateAccountBlocked( $this->isCreateAccountBlocked() ); | |
# Continue suppressing the name if needed | |
$autoblock->setHideName( $this->getHideName() ); | |
$autoblock->isUsertalkEditAllowed( $this->isUsertalkEditAllowed() ); | |
$autoblock->mParentBlockId = $this->mId; | |
$autoblock->isSitewide( $this->isSitewide() ); | |
$autoblock->setRestrictions( $this->getRestrictions() ); | |
if ( $this->getExpiry() == 'infinity' ) { | |
# Original block was indefinite, start an autoblock now | |
$autoblock->setExpiry( self::getAutoblockExpiry( $timestamp ) ); | |
} else { | |
# If the user is already blocked with an expiry date, we don't | |
# want to pile on top of that. | |
$autoblock->setExpiry( min( $this->getExpiry(), self::getAutoblockExpiry( $timestamp ) ) ); | |
} | |
# Insert the block... | |
$status = $autoblock->insert(); | |
return $status | |
? $status['id'] | |
: false; | |
} | |
/** | |
* Check if a block has expired. Delete it if it is. | |
* @return bool | |
*/ | |
public function deleteIfExpired() { | |
if ( $this->isExpired() ) { | |
wfDebug( "Block::deleteIfExpired() -- deleting\n" ); | |
$this->delete(); | |
$retVal = true; | |
} else { | |
wfDebug( "Block::deleteIfExpired() -- not expired\n" ); | |
$retVal = false; | |
} | |
return $retVal; | |
} | |
/** | |
* Has the block expired? | |
* @return bool | |
*/ | |
public function isExpired() { | |
$timestamp = wfTimestampNow(); | |
wfDebug( "Block::isExpired() checking current " . $timestamp . " vs $this->mExpiry\n" ); | |
if ( !$this->getExpiry() ) { | |
return false; | |
} else { | |
return $timestamp > $this->getExpiry(); | |
} | |
} | |
/** | |
* Is the block address valid (i.e. not a null string?) | |
* | |
* @deprecated since 1.33 No longer needed in core. | |
* @return bool | |
*/ | |
public function isValid() { | |
wfDeprecated( __METHOD__, '1.33' ); | |
return $this->getTarget() != null; | |
} | |
/** | |
* Update the timestamp on autoblocks. | |
*/ | |
public function updateTimestamp() { | |
if ( $this->mAuto ) { | |
$this->setTimestamp( wfTimestamp() ); | |
$this->setExpiry( self::getAutoblockExpiry( $this->getTimestamp() ) ); | |
$dbw = wfGetDB( DB_MASTER ); | |
$dbw->update( 'ipblocks', | |
[ /* SET */ | |
'ipb_timestamp' => $dbw->timestamp( $this->getTimestamp() ), | |
'ipb_expiry' => $dbw->timestamp( $this->getExpiry() ), | |
], | |
[ /* WHERE */ | |
'ipb_id' => $this->getId(), | |
], | |
__METHOD__ | |
); | |
} | |
} | |
/** | |
* Get the IP address at the start of the range in Hex form | |
* @throws MWException | |
* @return string IP in Hex form | |
*/ | |
public function getRangeStart() { | |
switch ( $this->type ) { | |
case self::TYPE_USER: | |
return ''; | |
case self::TYPE_IP: | |
return IP::toHex( $this->target ); | |
case self::TYPE_RANGE: | |
list( $start, /*...*/ ) = IP::parseRange( $this->target ); | |
return $start; | |
default: | |
throw new MWException( "Block with invalid type" ); | |
} | |
} | |
/** | |
* Get the IP address at the end of the range in Hex form | |
* @throws MWException | |
* @return string IP in Hex form | |
*/ | |
public function getRangeEnd() { | |
switch ( $this->type ) { | |
case self::TYPE_USER: | |
return ''; | |
case self::TYPE_IP: | |
return IP::toHex( $this->target ); | |
case self::TYPE_RANGE: | |
list( /*...*/, $end ) = IP::parseRange( $this->target ); | |
return $end; | |
default: | |
throw new MWException( "Block with invalid type" ); | |
} | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function getId() { | |
return $this->mId; | |
} | |
/** | |
* Set the block ID | |
* | |
* @param int $blockId | |
* @return int | |
*/ | |
private function setId( $blockId ) { | |
$this->mId = (int)$blockId; | |
if ( is_array( $this->restrictions ) ) { | |
$this->restrictions = $this->getBlockRestrictionStore()->setBlockId( | |
$blockId, $this->restrictions | |
); | |
} | |
return $this; | |
} | |
/** | |
* Get/set a flag determining whether the master is used for reads | |
* | |
* @param bool|null $x | |
* @return bool | |
*/ | |
public function fromMaster( $x = null ) { | |
return wfSetVar( $this->mFromMaster, $x ); | |
} | |
/** | |
* Get/set whether the Block is a hardblock (affects logged-in users on a given IP/range) | |
* @param bool|null $x | |
* @return bool | |
*/ | |
public function isHardblock( $x = null ) { | |
wfSetVar( $this->isHardblock, $x ); | |
# You can't *not* hardblock a user | |
return $this->getType() == self::TYPE_USER | |
? true | |
: $this->isHardblock; | |
} | |
/** | |
* @param null|bool $x | |
* @return bool | |
*/ | |
public function isAutoblocking( $x = null ) { | |
wfSetVar( $this->isAutoblocking, $x ); | |
# You can't put an autoblock on an IP or range as we don't have any history to | |
# look over to get more IPs from | |
return $this->getType() == self::TYPE_USER | |
? $this->isAutoblocking | |
: false; | |
} | |
/** | |
* Get the block name, but with autoblocked IPs hidden as per standard privacy policy | |
* @return string Text is escaped | |
*/ | |
public function getRedactedName() { | |
if ( $this->mAuto ) { | |
return Html::element( | |
'span', | |
[ 'class' => 'mw-autoblockid' ], | |
wfMessage( 'autoblockid', $this->mId )->text() | |
); | |
} else { | |
return htmlspecialchars( $this->getTarget() ); | |
} | |
} | |
/** | |
* Get a timestamp of the expiry for autoblocks | |
* | |
* @param string|int $timestamp | |
* @return string | |
*/ | |
public static function getAutoblockExpiry( $timestamp ) { | |
global $wgAutoblockExpiry; | |
return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); | |
} | |
/** | |
* Purge expired blocks from the ipblocks table | |
*/ | |
public static function purgeExpired() { | |
if ( wfReadOnly() ) { | |
return; | |
} | |
DeferredUpdates::addUpdate( new AutoCommitUpdate( | |
wfGetDB( DB_MASTER ), | |
__METHOD__, | |
function ( IDatabase $dbw, $fname ) { | |
$ids = $dbw->selectFieldValues( 'ipblocks', | |
'ipb_id', | |
[ 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ], | |
$fname | |
); | |
if ( $ids ) { | |
$blockRestrictionStore = MediaWikiServices::getInstance()->getBlockRestrictionStore(); | |
$blockRestrictionStore->deleteByBlockId( $ids ); | |
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname ); | |
} | |
} | |
) ); | |
} | |
/** | |
* Given a target and the target's type, get an existing Block object if possible. | |
* @param string|User|int $specificTarget A block target, which may be one of several types: | |
* * A user to block, in which case $target will be a User | |
* * An IP to block, in which case $target will be a User generated by using | |
* User::newFromName( $ip, false ) to turn off name validation | |
* * An IP range, in which case $target will be a String "123.123.123.123/18" etc | |
* * The ID of an existing block, in the format "#12345" (since pure numbers are valid | |
* usernames | |
* Calling this with a user, IP address or range will not select autoblocks, and will | |
* only select a block where the targets match exactly (so looking for blocks on | |
* 1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32) | |
* @param string|User|int|null $vagueTarget As above, but we will search for *any* block which | |
* affects that target (so for an IP address, get ranges containing that IP; and also | |
* get any relevant autoblocks). Leave empty or blank to skip IP-based lookups. | |
* @param bool $fromMaster Whether to use the DB_MASTER database | |
* @return Block|null (null if no relevant block could be found). The target and type | |
* of the returned Block will refer to the actual block which was found, which might | |
* not be the same as the target you gave if you used $vagueTarget! | |
*/ | |
public static function newFromTarget( $specificTarget, $vagueTarget = null, $fromMaster = false ) { | |
list( $target, $type ) = self::parseTarget( $specificTarget ); | |
if ( $type == self::TYPE_ID || $type == self::TYPE_AUTO ) { | |
return self::newFromID( $target ); | |
} elseif ( $target === null && $vagueTarget == '' ) { | |
# We're not going to find anything useful here | |
# Be aware that the == '' check is explicit, since empty values will be | |
# passed by some callers (T31116) | |
return null; | |
} elseif ( in_array( | |
$type, | |
[ self::TYPE_USER, self::TYPE_IP, self::TYPE_RANGE, null ] ) | |
) { | |
$block = new Block(); | |
$block->fromMaster( $fromMaster ); | |
if ( $type !== null ) { | |
$block->setTarget( $target ); | |
} | |
if ( $block->newLoad( $vagueTarget ) ) { | |
return $block; | |
} | |
} | |
return null; | |
} | |
/** | |
* Get all blocks that match any IP from an array of IP addresses | |
* | |
* @param array $ipChain List of IPs (strings), usually retrieved from the | |
* X-Forwarded-For header of the request | |
* @param bool $isAnon Exclude anonymous-only blocks if false | |
* @param bool $fromMaster Whether to query the master or replica DB | |
* @return array Array of Blocks | |
* @since 1.22 | |
*/ | |
public static function getBlocksForIPList( array $ipChain, $isAnon, $fromMaster = false ) { | |
if ( $ipChain === [] ) { | |
return []; | |
} | |
$conds = []; | |
$proxyLookup = MediaWikiServices::getInstance()->getProxyLookup(); | |
foreach ( array_unique( $ipChain ) as $ipaddr ) { | |
# Discard invalid IP addresses. Since XFF can be spoofed and we do not | |
# necessarily trust the header given to us, make sure that we are only | |
# checking for blocks on well-formatted IP addresses (IPv4 and IPv6). | |
# Do not treat private IP spaces as special as it may be desirable for wikis | |
# to block those IP ranges in order to stop misbehaving proxies that spoof XFF. | |
if ( !IP::isValid( $ipaddr ) ) { | |
continue; | |
} | |
# Don't check trusted IPs (includes local squids which will be in every request) | |
if ( $proxyLookup->isTrustedProxy( $ipaddr ) ) { | |
continue; | |
} | |
# Check both the original IP (to check against single blocks), as well as build | |
# the clause to check for rangeblocks for the given IP. | |
$conds['ipb_address'][] = $ipaddr; | |
$conds[] = self::getRangeCond( IP::toHex( $ipaddr ) ); | |
} | |
if ( $conds === [] ) { | |
return []; | |
} | |
if ( $fromMaster ) { | |
$db = wfGetDB( DB_MASTER ); | |
} else { | |
$db = wfGetDB( DB_REPLICA ); | |
} | |
$conds = $db->makeList( $conds, LIST_OR ); | |
if ( !$isAnon ) { | |
$conds = [ $conds, 'ipb_anon_only' => 0 ]; | |
} | |
$blockQuery = self::getQueryInfo(); | |
$rows = $db->select( | |
$blockQuery['tables'], | |
array_merge( [ 'ipb_range_start', 'ipb_range_end' ], $blockQuery['fields'] ), | |
$conds, | |
__METHOD__, | |
[], | |
$blockQuery['joins'] | |
); | |
$blocks = []; | |
foreach ( $rows as $row ) { | |
$block = self::newFromRow( $row ); | |
if ( !$block->isExpired() ) { | |
$blocks[] = $block; | |
} | |
} | |
return $blocks; | |
} | |
/** | |
* From a list of multiple blocks, find the most exact and strongest Block. | |
* | |
* The logic for finding the "best" block is: | |
* - Blocks that match the block's target IP are preferred over ones in a range | |
* - Hardblocks are chosen over softblocks that prevent account creation | |
* - Softblocks that prevent account creation are chosen over other softblocks | |
* - Other softblocks are chosen over autoblocks | |
* - If there are multiple exact or range blocks at the same level, the one chosen | |
* is random | |
* This should be used when $blocks where retrieved from the user's IP address | |
* and $ipChain is populated from the same IP address information. | |
* | |
* @param array $blocks Array of Block objects | |
* @param array $ipChain List of IPs (strings). This is used to determine how "close" | |
* a block is to the server, and if a block matches exactly, or is in a range. | |
* The order is furthest from the server to nearest e.g., (Browser, proxy1, proxy2, | |
* local-squid, ...) | |
* @throws MWException | |
* @return Block|null The "best" block from the list | |
*/ | |
public static function chooseBlock( array $blocks, array $ipChain ) { | |
if ( $blocks === [] ) { | |
return null; | |
} elseif ( count( $blocks ) == 1 ) { | |
return $blocks[0]; | |
} | |
// Sort hard blocks before soft ones and secondarily sort blocks | |
// that disable account creation before those that don't. | |
usort( $blocks, function ( Block $a, Block $b ) { | |
$aWeight = (int)$a->isHardblock() . (int)$a->appliesToRight( 'createaccount' ); | |
$bWeight = (int)$b->isHardblock() . (int)$b->appliesToRight( 'createaccount' ); | |
return strcmp( $bWeight, $aWeight ); // highest weight first | |
} ); | |
$blocksListExact = [ | |
'hard' => false, | |
'disable_create' => false, | |
'other' => false, | |
'auto' => false | |
]; | |
$blocksListRange = [ | |
'hard' => false, | |
'disable_create' => false, | |
'other' => false, | |
'auto' => false | |
]; | |
$ipChain = array_reverse( $ipChain ); | |
/** @var Block $block */ | |
foreach ( $blocks as $block ) { | |
// Stop searching if we have already have a "better" block. This | |
// is why the order of the blocks matters | |
if ( !$block->isHardblock() && $blocksListExact['hard'] ) { | |
break; | |
} elseif ( !$block->appliesToRight( 'createaccount' ) && $blocksListExact['disable_create'] ) { | |
break; | |
} | |
foreach ( $ipChain as $checkip ) { | |
$checkipHex = IP::toHex( $checkip ); | |
if ( (string)$block->getTarget() === $checkip ) { | |
if ( $block->isHardblock() ) { | |
$blocksListExact['hard'] = $blocksListExact['hard'] ?: $block; | |
} elseif ( $block->appliesToRight( 'createaccount' ) ) { | |
$blocksListExact['disable_create'] = $blocksListExact['disable_create'] ?: $block; | |
} elseif ( $block->mAuto ) { | |
$blocksListExact['auto'] = $blocksListExact['auto'] ?: $block; | |
} else { | |
$blocksListExact['other'] = $blocksListExact['other'] ?: $block; | |
} | |
// We found closest exact match in the ip list, so go to the next Block | |
break; | |
} elseif ( array_filter( $blocksListExact ) == [] | |
&& $block->getRangeStart() <= $checkipHex | |
&& $block->getRangeEnd() >= $checkipHex | |
) { | |
if ( $block->isHardblock() ) { | |
$blocksListRange['hard'] = $blocksListRange['hard'] ?: $block; | |
} elseif ( $block->appliesToRight( 'createaccount' ) ) { | |
$blocksListRange['disable_create'] = $blocksListRange['disable_create'] ?: $block; | |
} elseif ( $block->mAuto ) { | |
$blocksListRange['auto'] = $blocksListRange['auto'] ?: $block; | |
} else { | |
$blocksListRange['other'] = $blocksListRange['other'] ?: $block; | |
} | |
break; | |
} | |
} | |
} | |
if ( array_filter( $blocksListExact ) == [] ) { | |
$blocksList = &$blocksListRange; | |
} else { | |
$blocksList = &$blocksListExact; | |
} | |
$chosenBlock = null; | |
if ( $blocksList['hard'] ) { | |
$chosenBlock = $blocksList['hard']; | |
} elseif ( $blocksList['disable_create'] ) { | |
$chosenBlock = $blocksList['disable_create']; | |
} elseif ( $blocksList['other'] ) { | |
$chosenBlock = $blocksList['other']; | |
} elseif ( $blocksList['auto'] ) { | |
$chosenBlock = $blocksList['auto']; | |
} else { | |
throw new MWException( "Proxy block found, but couldn't be classified." ); | |
} | |
return $chosenBlock; | |
} | |
/** | |
* @inheritDoc | |
* | |
* Autoblocks have whichever type corresponds to their target, so to detect if a block is an | |
* autoblock, we have to check the mAuto property instead. | |
*/ | |
public function getType() { | |
return $this->mAuto | |
? self::TYPE_AUTO | |
: parent::getType(); | |
} | |
/** | |
* 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.29 | |
* | |
* @param WebResponse $response The response on which to set the cookie. | |
*/ | |
public function setCookie( 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 = $this->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(); | |
$response->setCookie( 'BlockID', $cookieValue, $expiryValue, $cookieOptions ); | |
} | |
/** | |
* Unset the 'BlockID' cookie. | |
* | |
* @since 1.29 | |
* | |
* @param WebResponse $response The response on which to unset the cookie. | |
*/ | |
public static function clearCookie( WebResponse $response ) { | |
$response->clearCookie( 'BlockID', [ 'httpOnly' => false ] ); | |
} | |
/** | |
* 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.29 | |
* | |
* @return string The block ID, probably concatenated with "!" and the HMAC. | |
*/ | |
public function getCookieValue() { | |
$config = RequestContext::getMain()->getConfig(); | |
$id = $this->getId(); | |
$secretKey = $config->get( 'SecretKey' ); | |
if ( !$secretKey ) { | |
// If there's no secret key, don't append a HMAC. | |
return $id; | |
} | |
$hmac = MWCryptHash::hmac( $id, $secretKey, false ); | |
$cookieValue = $id . '!' . $hmac; | |
return $cookieValue; | |
} | |
/** | |
* Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of | |
* the ID and a HMAC (see Block::setCookie), but will sometimes only be the ID. | |
* | |
* @since 1.29 | |
* | |
* @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 static 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 ); | |
// Get the site-wide secret key. | |
$config = RequestContext::getMain()->getConfig(); | |
$secretKey = $config->get( 'SecretKey' ); | |
if ( !$secretKey ) { | |
// If there's no secret key, just use the ID as given. | |
return $id; | |
} | |
$storedHmac = substr( $cookieValue, $bangPos + 1 ); | |
$calculatedHmac = MWCryptHash::hmac( $id, $secretKey, false ); | |
if ( $calculatedHmac === $storedHmac ) { | |
return $id; | |
} else { | |
return null; | |
} | |
} | |
/** | |
* @inheritDoc | |
* | |
* Build different messages for autoblocks and partial blocks. | |
*/ | |
public function getPermissionsError( IContextSource $context ) { | |
$params = $this->getBlockErrorParams( $context ); | |
$msg = 'blockedtext'; | |
if ( $this->mAuto ) { | |
$msg = 'autoblockedtext'; | |
} elseif ( !$this->isSitewide() ) { | |
$msg = 'blockedtext-partial'; | |
} | |
array_unshift( $params, $msg ); | |
return $params; | |
} | |
/** | |
* Get Restrictions. | |
* | |
* Getting the restrictions will perform a database query if the restrictions | |
* are not already loaded. | |
* | |
* @since 1.33 | |
* @return Restriction[] | |
*/ | |
public function getRestrictions() { | |
if ( $this->restrictions === null ) { | |
// If the block id has not been set, then do not attempt to load the | |
// restrictions. | |
if ( !$this->mId ) { | |
return []; | |
} | |
$this->restrictions = $this->getBlockRestrictionStore()->loadByBlockId( $this->mId ); | |
} | |
return $this->restrictions; | |
} | |
/** | |
* Set Restrictions. | |
* | |
* @since 1.33 | |
* @param Restriction[] $restrictions | |
* @return self | |
*/ | |
public function setRestrictions( array $restrictions ) { | |
$this->restrictions = array_filter( $restrictions, function ( $restriction ) { | |
return $restriction instanceof Restriction; | |
} ); | |
return $this; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function appliesToTitle( Title $title ) { | |
if ( $this->isSitewide() ) { | |
return true; | |
} | |
$restrictions = $this->getRestrictions(); | |
foreach ( $restrictions as $restriction ) { | |
if ( $restriction->matches( $title ) ) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function appliesToNamespace( $ns ) { | |
if ( $this->isSitewide() ) { | |
return true; | |
} | |
// Blocks do not apply to virtual namespaces. | |
if ( $ns < 0 ) { | |
return false; | |
} | |
$restriction = $this->findRestriction( NamespaceRestriction::TYPE, $ns ); | |
return (bool)$restriction; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function appliesToPage( $pageId ) { | |
if ( $this->isSitewide() ) { | |
return true; | |
} | |
// If the pageId is not over zero, the block cannot apply to it. | |
if ( $pageId <= 0 ) { | |
return false; | |
} | |
$restriction = $this->findRestriction( PageRestriction::TYPE, $pageId ); | |
return (bool)$restriction; | |
} | |
/** | |
* Find Restriction by type and value. | |
* | |
* @param string $type | |
* @param int $value | |
* @return Restriction|null | |
*/ | |
private function findRestriction( $type, $value ) { | |
$restrictions = $this->getRestrictions(); | |
foreach ( $restrictions as $restriction ) { | |
if ( $restriction->getType() !== $type ) { | |
continue; | |
} | |
if ( $restriction->getValue() === $value ) { | |
return $restriction; | |
} | |
} | |
return null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function shouldTrackWithCookie( $isAnon ) { | |
$config = RequestContext::getMain()->getConfig(); | |
switch ( $this->getType() ) { | |
case self::TYPE_IP: | |
case self::TYPE_RANGE: | |
return $isAnon && $config->get( 'CookieSetOnIpBlock' ); | |
case self::TYPE_USER: | |
return !$isAnon && $config->get( 'CookieSetOnAutoblock' ) && $this->isAutoblocking(); | |
default: | |
return false; | |
} | |
} | |
/** | |
* Get a BlockRestrictionStore instance | |
* | |
* @return BlockRestrictionStore | |
*/ | |
private function getBlockRestrictionStore() : BlockRestrictionStore { | |
return MediaWikiServices::getInstance()->getBlockRestrictionStore(); | |
} | |
} |