diff options
Diffstat (limited to 'includes/block')
26 files changed, 1176 insertions, 495 deletions
diff --git a/includes/block/AbstractBlock.php b/includes/block/AbstractBlock.php index 5204d2c56c40..667a5bd586c2 100644 --- a/includes/block/AbstractBlock.php +++ b/includes/block/AbstractBlock.php @@ -21,6 +21,7 @@ namespace MediaWiki\Block; use InvalidArgumentException; +use LogicException; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\DAO\WikiAwareEntityTrait; use MediaWiki\MainConfigNames; @@ -61,15 +62,9 @@ abstract class AbstractBlock implements Block { /** @var bool */ protected $isHardblock; - /** @var UserIdentity|string|null */ + /** @var BlockTarget|null */ protected $target; - /** - * @var int|null AbstractBlock::TYPE_ constant. After the block has been loaded - * from the database, this can only be USER, IP or RANGE. - */ - protected $type; - /** @var bool */ protected $isSitewide = true; @@ -80,8 +75,9 @@ abstract class AbstractBlock implements Block { * Create a new block with specified parameters on a user, IP or IP range. * * @param array $options Parameters of the block, with supported options: + * - target: (BlockTarget) The target object (since 1.44) * - address: (string|UserIdentity) Target user name, user identity object, - * IP address or IP range + * IP address or IP range. * - wiki: (string|false) The wiki the block has been issued in, * self::LOCAL for the local wiki (since 1.38) * - reason: (string|Message|CommentStoreComment) Reason for the block @@ -95,7 +91,6 @@ abstract class AbstractBlock implements Block { */ public function __construct( array $options = [] ) { $defaults = [ - 'address' => '', 'wiki' => self::LOCAL, 'reason' => '', 'timestamp' => '', @@ -106,7 +101,16 @@ abstract class AbstractBlock implements Block { $options += $defaults; $this->wikiId = $options['wiki']; - $this->setTarget( $options['address'] ); + if ( isset( $options['target'] ) ) { + if ( !( $options['target'] instanceof BlockTarget ) ) { + throw new InvalidArgumentException( 'Invalid block target' ); + } + $this->setTarget( $options['target'] ); + } elseif ( isset( $options['address'] ) ) { + $this->setTarget( $options['address'] ); + } else { + $this->setTarget( null ); + } $this->setReason( $options['reason'] ); if ( isset( $options['decodedTimestamp'] ) ) { $this->setTimestamp( $options['decodedTimestamp'] ); @@ -297,12 +301,30 @@ abstract class AbstractBlock implements Block { return $res; } + public function getTarget(): ?BlockTarget { + return $this->target; + } + + public function getRedactedTarget(): ?BlockTarget { + $target = $this->getTarget(); + if ( $this->getType() === Block::TYPE_AUTO + && !( $target instanceof AutoBlockTarget ) + ) { + $id = $this->getId( $this->wikiId ); + if ( $id === null ) { + throw new LogicException( 'no ID available for autoblock redaction' ); + } + $target = new AutoBlockTarget( $id, $this->wikiId ); + } + return $target; + } + /** * Get the type of target for this particular block. - * @return int|null AbstractBlock::TYPE_ constant, will never be TYPE_ID + * @return int|null AbstractBlock::TYPE_ constant */ public function getType(): ?int { - return $this->type; + return $this->target ? $this->target->getType() : null; } /** @@ -310,7 +332,8 @@ abstract class AbstractBlock implements Block { * @return ?UserIdentity */ public function getTargetUserIdentity(): ?UserIdentity { - return $this->target instanceof UserIdentity ? $this->target : null; + return $this->target instanceof BlockTargetWithUserIdentity + ? $this->target->getUserIdentity() : null; } /** @@ -318,13 +341,11 @@ abstract class AbstractBlock implements Block { * @return string */ public function getTargetName(): string { - return $this->target instanceof UserIdentity - ? $this->target->getName() - : (string)$this->target; + return (string)$this->target; } /** - * @param UserIdentity|string $target + * @param BlockTarget|UserIdentity|string $target * * @return bool * @since 1.37 @@ -385,22 +406,22 @@ abstract class AbstractBlock implements Block { } /** - * Set the target for this block, and update $this->type accordingly - * @param string|UserIdentity|null $target + * Set the target for this block + * @param BlockTarget|string|UserIdentity|null $target */ public function setTarget( $target ) { // Small optimization to make this code testable, this is what would happen anyway - if ( $target === '' ) { + if ( $target === '' || $target === null ) { $this->target = null; - $this->type = null; + } elseif ( $target instanceof BlockTarget ) { + $this->assertWiki( $target->getWikiId() ); + $this->target = $target; } else { - [ $parsedTarget, $this->type ] = MediaWikiServices::getInstance() - ->getBlockUtilsFactory() - ->getBlockUtils( $this->wikiId ) - ->parseBlockTarget( $target ); - if ( $parsedTarget !== null ) { - $this->assertWiki( is_string( $parsedTarget ) ? self::LOCAL : $parsedTarget->getWikiId() ); - } + $parsedTarget = MediaWikiServices::getInstance() + ->getCrossWikiBlockTargetFactory() + ->getFactory( $this->wikiId ) + ->newFromLegacyUnion( $target ); + $this->assertWiki( $parsedTarget->getWikiId() ); $this->target = $parsedTarget; } } @@ -442,10 +463,10 @@ abstract class AbstractBlock implements Block { */ public function appliesToUsertalk( ?Title $usertalk = null ) { if ( !$usertalk ) { - if ( $this->target instanceof UserIdentity ) { + if ( $this->target instanceof BlockTargetWithUserPage ) { $usertalk = Title::makeTitle( NS_USER_TALK, - $this->target->getName() + $this->target->getUserPage()->getDBkey() ); } else { throw new InvalidArgumentException( diff --git a/includes/block/AnonIpBlockTarget.php b/includes/block/AnonIpBlockTarget.php new file mode 100644 index 000000000000..e3acfedbf933 --- /dev/null +++ b/includes/block/AnonIpBlockTarget.php @@ -0,0 +1,79 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityValue; +use StatusValue; +use Wikimedia\IPUtils; + +/** + * A block target for a single IP address with an associated user page + * + * @since 1.44 + */ +class AnonIpBlockTarget extends BlockTarget implements BlockTargetWithUserPage, BlockTargetWithIp { + private string $addr; + + /** + * @param string $addr + * @param string|false $wikiId + */ + public function __construct( string $addr, $wikiId ) { + parent::__construct( $wikiId ); + $this->addr = $addr; + } + + public function toString(): string { + return $this->addr; + } + + public function getType(): int { + return Block::TYPE_IP; + } + + public function getSpecificity() { + return 2; + } + + public function getLogPage(): PageReference { + return $this->getUserPage(); + } + + public function getUserPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->addr, $this->wikiId ); + } + + public function getUserIdentity(): UserIdentity { + return new UserIdentityValue( 0, $this->addr, $this->wikiId ); + } + + public function validateForCreation(): StatusValue { + return StatusValue::newGood(); + } + + /** + * Get the IP address in hexadecimal form + * + * @return string + */ + public function toHex(): string { + return IPUtils::toHex( $this->addr ); + } + + /** + * Get the IP address as a hex "range" tuple, with the start and end equal + * + * @return string[] + */ + public function toHexRange(): array { + $hex = $this->toHex(); + return [ $hex, $hex ]; + } + + protected function getLegacyUnion() { + return $this->getUserIdentity(); + } +} diff --git a/includes/block/AutoBlockTarget.php b/includes/block/AutoBlockTarget.php new file mode 100644 index 000000000000..373c694aa31a --- /dev/null +++ b/includes/block/AutoBlockTarget.php @@ -0,0 +1,60 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use StatusValue; + +/** + * A block target of the form #1234 where the number is the block ID. For user + * input or display when the IP address needs to be hidden. + * + * @since 1.44 + */ +class AutoBlockTarget extends BlockTarget { + private int $id; + + /** + * @param int $id The block ID + * @param string|false $wikiId + */ + public function __construct( int $id, $wikiId ) { + parent::__construct( $wikiId ); + $this->id = $id; + } + + public function toString(): string { + return '#' . $this->id; + } + + public function getType(): int { + return Block::TYPE_AUTO; + } + + public function getLogPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->toString(), $this->wikiId ); + } + + public function getSpecificity() { + return 2; + } + + public function validateForCreation(): StatusValue { + // Autoblocks are never valid for creation + return StatusValue::newFatal( 'badipaddress' ); + } + + /** + * Get the block ID + * + * @return int + */ + public function getId(): int { + return $this->id; + } + + protected function getLegacyUnion() { + return (string)$this->id; + } +} diff --git a/includes/block/Block.php b/includes/block/Block.php index 244aab5fc4c6..a1f5b9ed0f43 100644 --- a/includes/block/Block.php +++ b/includes/block/Block.php @@ -50,7 +50,6 @@ interface Block extends WikiAwareEntity { public const TYPE_IP = 2; public const TYPE_RANGE = 3; public const TYPE_AUTO = 4; - public const TYPE_ID = 5; /** * Map block types to strings, to allow convenient logging. @@ -60,7 +59,6 @@ interface Block extends WikiAwareEntity { self::TYPE_IP => 'ip', self::TYPE_RANGE => 'range', self::TYPE_AUTO => 'autoblock', - self::TYPE_ID => 'id', ]; /** @@ -97,6 +95,27 @@ interface Block extends WikiAwareEntity { public function getReasonComment(): CommentStoreComment; /** + * Get the target as an object. + * + * For autoblocks this can be either the IP address or the autoblock ID + * depending on how the block was loaded. Use getRedactedTarget() to safely + * get a target for display. + * + * @since 1.44 + * @return BlockTarget|null + */ + public function getTarget(): ?BlockTarget; + + /** + * Get the target, with the IP address hidden behind an AutoBlockTarget + * if the block is an autoblock. + * + * @since 1.44 + * @return BlockTarget|null + */ + public function getRedactedTarget(): ?BlockTarget; + + /** * Get the UserIdentity identifying the blocked user, * if the target is indeed a user (that is, if getType() returns TYPE_USER). * @@ -139,7 +158,7 @@ interface Block extends WikiAwareEntity { /** * Get the type of target for this particular block. - * @return int|null Block::TYPE_ constant, will never be TYPE_ID + * @return int|null Block::TYPE_ constant */ public function getType(): ?int; diff --git a/includes/block/BlockErrorFormatter.php b/includes/block/BlockErrorFormatter.php index 77348d456fbd..06adf7c85f15 100644 --- a/includes/block/BlockErrorFormatter.php +++ b/includes/block/BlockErrorFormatter.php @@ -20,6 +20,8 @@ namespace MediaWiki\Block; +use MediaWiki\Api\ApiBlockInfoHelper; +use MediaWiki\Api\ApiMessage; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; @@ -68,7 +70,7 @@ class BlockErrorFormatter { /** * Get a block error message. Different message keys are chosen depending on the * block features. Message parameters are formatted for the specified user and - * language. + * language. The message includes machine-readable data for API error responses. * * If passed a CompositeBlock, will get a generic message stating that there are * multiple blocks. To get all the block messages, use getMessages instead. @@ -77,7 +79,7 @@ class BlockErrorFormatter { * @param UserIdentity $user * @param mixed $language Unused since 1.42 * @param string $ip - * @return Message + * @return ApiMessage */ public function getMessage( Block $block, @@ -87,7 +89,14 @@ class BlockErrorFormatter { ): Message { $key = $this->getBlockErrorMessageKey( $block, $user ); $params = $this->getBlockErrorMessageParams( $block, $user, $ip ); - return $this->uiContext->msg( $key, $params ); + $apiHelper = new ApiBlockInfoHelper; + + // @phan-suppress-next-line PhanTypeMismatchReturnSuperType + return ApiMessage::create( + $this->uiContext->msg( $key, $params ), + $apiHelper->getBlockCode( $block ), + [ 'blockinfo' => $apiHelper->getBlockDetails( $block, $this->getLanguage(), $user ) ] + ); } /** diff --git a/includes/block/BlockPermissionChecker.php b/includes/block/BlockPermissionChecker.php index edce233144c1..10eafbbdef6c 100644 --- a/includes/block/BlockPermissionChecker.php +++ b/includes/block/BlockPermissionChecker.php @@ -40,14 +40,14 @@ use Wikimedia\Rdbms\IDBAccessObject; class BlockPermissionChecker { /** * Legacy target state - * @var UserIdentity|string|null Block target or null when unknown + * @var BlockTarget|null Block target or null when unknown */ private $target; /** - * @var BlockUtils + * @var BlockTargetFactory */ - private $blockUtils; + private $blockTargetFactory; /** * @var Authority Block performer @@ -65,17 +65,17 @@ class BlockPermissionChecker { /** * @param ServiceOptions $options - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory For legacy branches only * @param Authority $performer */ public function __construct( ServiceOptions $options, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, Authority $performer ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->performer = $performer; } @@ -85,7 +85,7 @@ class BlockPermissionChecker { * @return void */ public function setTarget( $target ) { - [ $this->target, ] = $this->blockUtils->parseBlockTarget( $target ); + $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target ); } /** @@ -119,9 +119,11 @@ class BlockPermissionChecker { * * T208965: Partially blocked admins can block and unblock others as normal. * - * @param UserIdentity|string|null $target Passing null for this parameter - * is deprecated. This parameter will soon be required. It is the target - * of the proposed block. + * @param BlockTarget|UserIdentity|string|null $target The target of the + * proposed block or unblock operation. Passing null for this parameter + * is deprecated. This parameter will soon be required. Passing a + * UserIdentity or string for this parameter is deprecated. Pass a + * BlockTarget in new code. * @param int $freshness Indicates whether slightly stale data is acceptable * in exchange for a fast response. * @return bool|string True when checks passed, message code for failures @@ -139,9 +141,13 @@ class BlockPermissionChecker { } else { throw new InvalidArgumentException( 'A target is required' ); } - } else { - [ $target, ] = $this->blockUtils->parseBlockTarget( $target ); + } elseif ( !( $target instanceof BlockTarget ) ) { + $target = $this->blockTargetFactory->newFromLegacyUnion( $target ); + if ( !$target ) { + throw new InvalidArgumentException( 'Invalid block target' ); + } } + $block = $this->performer->getBlock( $freshness ); if ( !$block ) { // User is not blocked, process as normal @@ -156,8 +162,8 @@ class BlockPermissionChecker { $performerIdentity = $this->performer->getUser(); if ( - $target instanceof UserIdentity && - $target->getId() === $performerIdentity->getId() + $target instanceof UserBlockTarget && + $target->getUserIdentity()->getId() === $performerIdentity->getId() ) { // Blocked admin is trying to alter their own block @@ -175,9 +181,9 @@ class BlockPermissionChecker { } if ( - $target instanceof UserIdentity && + $target instanceof UserBlockTarget && $block->getBlocker() && - $target->equals( $block->getBlocker() ) + $target->getUserIdentity()->equals( $block->getBlocker() ) ) { // T150826: Blocked admins can always block the admin who blocked them return true; diff --git a/includes/block/BlockPermissionCheckerFactory.php b/includes/block/BlockPermissionCheckerFactory.php index 8e43f17394d4..da9c21648836 100644 --- a/includes/block/BlockPermissionCheckerFactory.php +++ b/includes/block/BlockPermissionCheckerFactory.php @@ -37,15 +37,15 @@ class BlockPermissionCheckerFactory { public const CONSTRUCTOR_OPTIONS = BlockPermissionChecker::CONSTRUCTOR_OPTIONS; private ServiceOptions $options; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; public function __construct( ServiceOptions $options, - BlockUtils $blockUtils + BlockTargetFactory $blockTargetFactory ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; } /** @@ -73,7 +73,7 @@ class BlockPermissionCheckerFactory { public function newChecker( Authority $performer ) { return new BlockPermissionChecker( $this->options, - $this->blockUtils, + $this->blockTargetFactory, $performer ); } diff --git a/includes/block/BlockTarget.php b/includes/block/BlockTarget.php new file mode 100644 index 000000000000..4b963d2cc53b --- /dev/null +++ b/includes/block/BlockTarget.php @@ -0,0 +1,117 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\DAO\WikiAwareEntityTrait; +use MediaWiki\Page\PageReference; +use MediaWiki\User\UserIdentity; +use StatusValue; +use Stringable; + +/** + * Base class for block targets + * + * @since 1.44 + */ +abstract class BlockTarget implements WikiAwareEntity, Stringable { + use WikiAwareEntityTrait; + + /** @var string|false */ + protected $wikiId; + + /** + * @param string|false $wikiId UserIdentity and Block extend WikiAwareEntity + * and so we must ask for a wiki ID as well, to forward it through, even + * though we don't use it. + */ + protected function __construct( $wikiId ) { + $this->wikiId = $wikiId; + } + + public function getWikiId() { + return $this->wikiId; + } + + public function __toString() { + return $this->toString(); + } + + /** + * Compare this object with another one + * + * @param BlockTarget|null $other + * @return bool + */ + public function equals( ?BlockTarget $other ) { + return $other !== null + && static::class === get_class( $other ) + && $this->toString() === $other->toString(); + } + + /** + * Get the username, the IP address, range, or autoblock ID prefixed with + * a "#". Such a string will round-trip through BlockTarget::newFromString(), + * giving back the same target. + * + * @return string + */ + abstract public function toString(): string; + + /** + * Get one of the Block::TYPE_xxx constants associated with this target + * @return int + */ + abstract public function getType(): int; + + /** + * Get the title to be used when logging an action on this block. For an + * autoblock, the title is technically invalid, with a hash character in + * the DB key. For a range block, the title is valid but is not a user + * page for a specific user. + * + * See also getUserPage(), which exists only for subclasses which relate to + * a specific user with a talk page. + * + * @return PageReference + */ + abstract public function getLogPage(): PageReference; + + /** + * Get the score of this block for purposes of choosing a more specific + * block, where lower is more specific. + * + * - 1: user block + * - 2: single IP block + * - 2-3: range block scaled according to the size of the range + * + * @return float|int + */ + abstract public function getSpecificity(); + + /** + * Get the target and type tuple conventionally returned by + * BlockUtils::parseBlockTarget() + * + * @return array + */ + public function getLegacyTuple(): array { + return [ $this->getLegacyUnion(), $this->getType() ]; + } + + /** + * Check the target data against more stringent requirements imposed when + * a block is created from user input. This is in addition to the loose + * validation done by BlockTargetFactory::newFromString(). + * + * @return StatusValue + */ + abstract public function validateForCreation(): StatusValue; + + /** + * Get the first part of the legacy tuple. + * + * @return UserIdentity|string + */ + abstract protected function getLegacyUnion(); +} diff --git a/includes/block/BlockTargetFactory.php b/includes/block/BlockTargetFactory.php new file mode 100644 index 000000000000..8b09be8eaa07 --- /dev/null +++ b/includes/block/BlockTargetFactory.php @@ -0,0 +1,280 @@ +<?php + +namespace MediaWiki\Block; + +use InvalidArgumentException; +use MediaWiki\Config\ServiceOptions; +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\DAO\WikiAwareEntityTrait; +use MediaWiki\MainConfigNames; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityLookup; +use MediaWiki\User\UserIdentityValue; +use MediaWiki\User\UserNameUtils; +use RuntimeException; +use stdClass; +use Wikimedia\IPUtils; +use Wikimedia\Rdbms\IDBAccessObject; + +/** + * Factory for BlockTarget objects + * + * @since 1.44 + */ +class BlockTargetFactory implements WikiAwareEntity { + use WikiAwareEntityTrait; + + private UserIdentityLookup $userIdentityLookup; + private UserNameUtils $userNameUtils; + + /** @var string|false */ + private $wikiId; + + /** + * @var array The range block minimum prefix lengths indexed by protocol (IPv4 or IPv6) + */ + private $rangePrefixLimits; + + /** + * @internal Only for use by ServiceWiring + */ + public const CONSTRUCTOR_OPTIONS = [ + MainConfigNames::BlockCIDRLimit, + ]; + + /** + * @param ServiceOptions $options + * @param UserIdentityLookup $userIdentityLookup + * @param UserNameUtils $userNameUtils + * @param string|false $wikiId + */ + public function __construct( + ServiceOptions $options, + UserIdentityLookup $userIdentityLookup, + UserNameUtils $userNameUtils, + /* string|false */ $wikiId = Block::LOCAL + ) { + $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); + $this->rangePrefixLimits = $options->get( MainConfigNames::BlockCIDRLimit ); + $this->userIdentityLookup = $userIdentityLookup; + $this->userNameUtils = $userNameUtils; + $this->wikiId = $wikiId; + } + + public function getWikiId() { + return $this->wikiId; + } + + /** + * Try to create a block target from a user input string. + * + * @param string|null $str + * @return BlockTarget|null + */ + public function newFromString( ?string $str ): ?BlockTarget { + if ( $str === null ) { + return null; + } + + $str = trim( $str ); + + if ( IPUtils::isValid( $str ) ) { + return new AnonIpBlockTarget( IPUtils::sanitizeIP( $str ), $this->wikiId ); + } elseif ( IPUtils::isValidRange( $str ) ) { + return new RangeBlockTarget( + IPUtils::sanitizeRange( $str ), + $this->rangePrefixLimits, + $this->wikiId + ); + } + + if ( preg_match( '/^#\d+$/', $str ) ) { + // Autoblock reference in the form "#12345" + return new AutoBlockTarget( + (int)substr( $str, 1 ), + $this->wikiId + ); + } + + $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $str ); + if ( $userFromDB instanceof UserIdentity ) { + return new UserBlockTarget( $userFromDB ); + } + + // Wrap the invalid user in a UserIdentityValue. + // This allows validateTarget() to return a "nosuchusershort" message, + // which is needed for Special:Block. + $canonicalName = $this->userNameUtils->getCanonical( $str ); + if ( $canonicalName !== false ) { + return new UserBlockTarget( new UserIdentityValue( 0, $canonicalName ) ); + } + + return null; + } + + /** + * Create a BlockTarget from a UserIdentity, which may refer to a + * registered user, an IP address or range. + * + * @param UserIdentity $user + * @return BlockTarget + */ + public function newFromUser( UserIdentity $user ): BlockTarget { + $this->assertWiki( $user->getWikiId() ); + $name = $user->getName(); + if ( $user->getId( $this->wikiId ) !== 0 ) { + // We'll trust the caller and skip IP validity checks + return new UserBlockTarget( $user ); + } elseif ( IPUtils::isValidRange( $name ) ) { + return $this->newRangeBlockTarget( $name ); + } elseif ( IPUtils::isValid( $name ) ) { + return $this->newAnonIpBlockTarget( $name ); + } else { + return new UserBlockTarget( $user ); + } + } + + /** + * Try to create a BlockTarget from a UserIdentity|string|null, a union type + * previously used as a target by various methods. + * + * @param UserIdentity|string|null $union + * @return BlockTarget|null + */ + public function newFromLegacyUnion( $union ): ?BlockTarget { + if ( $union instanceof UserIdentity ) { + if ( IPUtils::isValid( $union->getName() ) ) { + return new AnonIpBlockTarget( $union->getName(), $this->wikiId ); + } else { + return new UserBlockTarget( $union ); + } + } elseif ( is_string( $union ) ) { + return $this->newFromString( $union ); + } else { + return null; + } + } + + /** + * Try to create a BlockTarget from a row which must contain bt_user, + * bt_address and optionally bt_user_text. + * + * bt_auto is ignored, so this is suitable for permissions and for block + * creation but not for display. + * + * @param stdClass $row + * @return BlockTarget|null + */ + public function newFromRowRaw( $row ): ?BlockTarget { + return $this->newFromRowInternal( $row, false ); + } + + /** + * Try to create a BlockTarget from a row which must contain bt_auto, + * bt_user, bt_address and bl_id, and optionally bt_user_text. + * + * If bt_auto is set, the address will be redacted to avoid disclosing it. + * The ID will be wrapped in an AutoblockTarget. + * + * @param stdClass $row + * @return BlockTarget|null + */ + public function newFromRowRedacted( $row ): ?BlockTarget { + return $this->newFromRowInternal( $row, true ); + } + + /** + * @param stdClass $row + * @param bool $redact + * @return BlockTarget|null + */ + private function newFromRowInternal( $row, $redact ): ?BlockTarget { + if ( $redact && $row->bt_auto ) { + return $this->newAutoBlockTarget( $row->bl_id ); + } elseif ( isset( $row->bt_user ) ) { + if ( isset( $row->bt_user_text ) ) { + $user = new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId ); + } else { + $user = $this->userIdentityLookup->getUserIdentityByUserId( $row->bt_user ); + if ( !$user ) { + $user = $this->userIdentityLookup->getUserIdentityByUserId( + $row->bt_user, IDBAccessObject::READ_LATEST ); + if ( !$user ) { + throw new RuntimeException( + "Unable to find name for user ID {$row->bt_user}" ); + } + } + } + return new UserBlockTarget( $user ); + } elseif ( $row->bt_address === null ) { + return null; + } elseif ( IPUtils::isValid( $row->bt_address ) ) { + return $this->newAnonIpBlockTarget( IPUtils::sanitizeIP( $row->bt_address ) ); + } elseif ( IPUtils::isValidRange( $row->bt_address ) ) { + return $this->newRangeBlockTarget( IPUtils::sanitizeRange( $row->bt_address ) ); + } else { + return null; + } + } + + /** + * Create an AutoBlockTarget for the given ID + * + * A simple constructor proxy for pre-validated input. + * + * @param int $id + * @return AutoBlockTarget + */ + public function newAutoBlockTarget( int $id ): AutoBlockTarget { + return new AutoBlockTarget( $id, $this->wikiId ); + } + + /** + * Create a UserBlockTarget for the given user. + * + * A simple constructor proxy for pre-validated input. + * + * The user must be a real registered user. Use newFromUser() to create a + * block target from a UserIdentity which may represent an IP address. + * + * @param UserIdentity $user + * @return UserBlockTarget + */ + public function newUserBlockTarget( UserIdentity $user ): UserBlockTarget { + $this->assertWiki( $user->getWikiId() ); + if ( IPUtils::isValid( $user->getName() ) ) { + throw new InvalidArgumentException( 'IP address passed to newUserBlockTarget' ); + } + return new UserBlockTarget( $user ); + } + + /** + * Create an IP block target + * + * A simple constructor proxy for pre-validated input. + * + * @param string $ip + * @return AnonIpBlockTarget + */ + public function newAnonIpBlockTarget( string $ip ): AnonIpBlockTarget { + if ( !IPUtils::isValid( $ip ) ) { + throw new InvalidArgumentException( 'Invalid IP address for block target' ); + } + return new AnonIpBlockTarget( $ip, $this->wikiId ); + } + + /** + * Create a range block target. + * + * A simple constructor proxy for pre-validated input. + * + * @param string $cidr + * @return RangeBlockTarget + */ + public function newRangeBlockTarget( string $cidr ): RangeBlockTarget { + if ( !IPUtils::isValidRange( $cidr ) ) { + throw new InvalidArgumentException( 'Invalid IP range for block target' ); + } + return new RangeBlockTarget( $cidr, $this->rangePrefixLimits, $this->wikiId ); + } +} diff --git a/includes/block/BlockTargetWithIp.php b/includes/block/BlockTargetWithIp.php new file mode 100644 index 000000000000..edce20544327 --- /dev/null +++ b/includes/block/BlockTargetWithIp.php @@ -0,0 +1,17 @@ +<?php + +namespace MediaWiki\Block; + +/** + * Shared interface for IP or range blocks + * + * @since 1.44 + */ +interface BlockTargetWithIp { + /** + * Get the range as a hexadecimal tuple. + * + * @return string[] + */ + public function toHexRange(); +} diff --git a/includes/block/BlockTargetWithUserIdentity.php b/includes/block/BlockTargetWithUserIdentity.php new file mode 100644 index 000000000000..ecfb0b752505 --- /dev/null +++ b/includes/block/BlockTargetWithUserIdentity.php @@ -0,0 +1,19 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\User\UserIdentity; + +/** + * Interface for block targets which can be converted to a UserIdentity. + * + * @since 1.44 + */ +interface BlockTargetWithUserIdentity { + /** + * Get a UserIdentity associated with this target. + * + * @return UserIdentity + */ + public function getUserIdentity(): UserIdentity; +} diff --git a/includes/block/BlockTargetWithUserPage.php b/includes/block/BlockTargetWithUserPage.php new file mode 100644 index 000000000000..717123566bcc --- /dev/null +++ b/includes/block/BlockTargetWithUserPage.php @@ -0,0 +1,22 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; + +/** + * Shared interface for user and single IP targets, that is, for targets with a + * meaningful user page link. Single IP addresses are traditionally treated as + * the names of anonymous users. + * + * @since 1.44 + */ +interface BlockTargetWithUserPage extends BlockTargetWithUserIdentity { + /** + * Get the target's user page. The page has an associated talk page which + * can be used to talk to the target. + * + * @return PageReference + */ + public function getUserPage(): PageReference; +} diff --git a/includes/block/BlockUser.php b/includes/block/BlockUser.php index 178722d2cb14..bd24a795d9f5 100644 --- a/includes/block/BlockUser.php +++ b/includes/block/BlockUser.php @@ -60,24 +60,13 @@ class BlockUser { public const CONFLICT_REBLOCK = true; /** - * @var UserIdentity|string|null + * @var BlockTarget|null * - * Target of the block - * - * This is null in case BlockUtils::parseBlockTarget failed to parse the target. - * Such case is detected in placeBlockUnsafe, by calling validateTarget from SpecialBlock. + * Target of the block. This is null in case BlockTargetFactory failed to + * parse the target. */ private $target; - /** - * @var int - * - * One of AbstractBlock::TYPE_* constants - * - * This will be -1 if BlockUtils::parseBlockTarget failed to parse the target. - */ - private $targetType; - /** @var DatabaseBlock|null */ private $blockToUpdate; @@ -90,7 +79,7 @@ class BlockUser { private ServiceOptions $options; private BlockRestrictionStore $blockRestrictionStore; private BlockPermissionChecker $blockPermissionChecker; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private BlockActionInfo $blockActionInfo; private HookRunner $hookRunner; private DatabaseBlockStore $blockStore; @@ -173,7 +162,7 @@ class BlockUser { * @param ServiceOptions $options * @param BlockRestrictionStore $blockRestrictionStore * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory * @param BlockActionInfo $blockActionInfo * @param HookContainer $hookContainer * @param DatabaseBlockStore $databaseBlockStore @@ -182,7 +171,7 @@ class BlockUser { * @param LoggerInterface $logger * @param TitleFactory $titleFactory * @param DatabaseBlock|null $blockToUpdate - * @param string|UserIdentity|null $target Target of the block + * @param BlockTarget|string|UserIdentity|null $target Target of the block * @param Authority $performer Performer of the block * @param string $expiry Expiry of the block (timestamp or 'infinity') * @param string $reason Reason of the block @@ -204,7 +193,7 @@ class BlockUser { ServiceOptions $options, BlockRestrictionStore $blockRestrictionStore, BlockPermissionCheckerFactory $blockPermissionCheckerFactory, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, BlockActionInfo $blockActionInfo, HookContainer $hookContainer, DatabaseBlockStore $databaseBlockStore, @@ -225,7 +214,7 @@ class BlockUser { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->blockRestrictionStore = $blockRestrictionStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->hookRunner = new HookRunner( $hookContainer ); $this->blockStore = $databaseBlockStore; $this->userFactory = $userFactory; @@ -237,16 +226,15 @@ class BlockUser { // Process block target if ( $blockToUpdate !== null ) { $this->blockToUpdate = $blockToUpdate; - $this->target = $blockToUpdate->getTargetUserIdentity() - ?? $blockToUpdate->getTargetName(); - $this->targetType = $blockToUpdate->getType() ?? -1; + $this->target = $blockToUpdate->getTarget(); + } elseif ( $target instanceof BlockTarget ) { + $this->target = $target; + } elseif ( $target === null ) { + throw new \InvalidArgumentException( + 'Either $target or $blockToUpdate must be specified' ); } else { - [ $this->target, $rawTargetType ] = $this->blockUtils->parseBlockTarget( $target ); - if ( $rawTargetType !== null ) { // Guard against invalid targets - $this->targetType = $rawTargetType; - } else { - $this->targetType = -1; - } + // TODO: deprecate + $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target ); } $this->blockPermissionChecker = $blockPermissionCheckerFactory @@ -302,10 +290,7 @@ class BlockUser { } - if ( - isset( $blockOptions['isHideUser'] ) && - $this->targetType === AbstractBlock::TYPE_USER - ) { + if ( isset( $blockOptions['isHideUser'] ) && $this->target instanceof UserBlockTarget ) { $this->isHideUser = $blockOptions['isHideUser']; } } @@ -403,7 +388,7 @@ class BlockUser { foreach ( $priorBlocks as $i => $block ) { // If we're blocking an IP, ignore any matching autoblocks (T287798) // TODO: put this in the query conditions - if ( $this->targetType !== Block::TYPE_AUTO + if ( $this->target->getType() !== Block::TYPE_AUTO && $block->getType() === Block::TYPE_AUTO ) { unset( $priorBlocks[$i] ); @@ -419,7 +404,7 @@ class BlockUser { * @return bool */ private function wasTargetHidden() { - if ( $this->targetType !== AbstractBlock::TYPE_USER ) { + if ( $this->target->getType() !== AbstractBlock::TYPE_USER ) { return false; } foreach ( $this->getPriorBlocksForTarget() as $block ) { @@ -515,7 +500,7 @@ class BlockUser { * Status is an instance of a newly placed block. */ public function placeBlockUnsafe( $conflictMode = self::CONFLICT_FAIL ): Status { - $status = $this->blockUtils->validateTarget( $this->target ); + $status = Status::wrap( $this->target->validateForCreation() ); if ( !$status->isOK() ) { $this->logger->debug( 'placeBlockUnsafe: invalid target' ); @@ -544,7 +529,8 @@ class BlockUser { return Status::newFatal( 'ipb_expiry_old' ); } - if ( $this->isHideUser ) { + $hideUserTarget = $this->getHideUserTarget(); + if ( $hideUserTarget ) { if ( $this->isPartial() ) { $this->logger->debug( 'placeBlockUnsafe: partial block cannot hide user' ); return Status::newFatal( 'ipb_hide_partial' ); @@ -558,7 +544,7 @@ class BlockUser { $hideUserContribLimit = $this->options->get( MainConfigNames::HideUserContribLimit ); if ( $hideUserContribLimit !== false && - $this->userEditTracker->getUserEditCount( $this->target ) > $hideUserContribLimit + $this->userEditTracker->getUserEditCount( $hideUserTarget ) > $hideUserContribLimit ) { $this->logger->debug( 'placeBlockUnsafe: hide user with too many contribs' ); return Status::newFatal( 'ipb_hide_invalid', Message::numParam( $hideUserContribLimit ) ); @@ -677,10 +663,9 @@ class BlockUser { $logEntry->addParameter( 'blockId', $block->getId() ); // Set *_deleted fields if requested - if ( $this->isHideUser ) { - // This should only be the case of $this->target is a user, so we can - // safely call ->getId() - RevisionDeleteUser::suppressUserName( $this->target->getName(), $this->target->getId() ); + $hideUserTarget = $this->getHideUserTarget(); + if ( $hideUserTarget ) { + RevisionDeleteUser::suppressUserName( $hideUserTarget->getName(), $hideUserTarget->getId() ); } DeferredUpdates::addCallableUpdate( function () use ( $block, $legacyUser, $priorBlock ) { @@ -698,6 +683,22 @@ class BlockUser { } /** + * If the operation is hiding a user, get the user being hidden + * + * @return UserIdentity|null + */ + private function getHideUserTarget(): ?UserIdentity { + if ( !$this->isHideUser ) { + return null; + } + if ( !( $this->target instanceof UserBlockTarget ) ) { + // Should be unreachable -- constructor checks this + throw new \LogicException( 'Wrong target type used with hide user option' ); + } + return $this->target->getUserIdentity(); + } + + /** * Build namespace restrictions array from $this->blockRestrictions * * Returns an array of namespace IDs. @@ -820,7 +821,7 @@ class BlockUser { private function blockLogFlags(): string { $flags = []; - if ( $this->targetType != AbstractBlock::TYPE_USER && !$this->isHardBlock ) { + if ( $this->target->getType() != AbstractBlock::TYPE_USER && !$this->isHardBlock ) { // For grepping: message block-log-flags-anononly $flags[] = 'anononly'; } @@ -830,7 +831,7 @@ class BlockUser { $flags[] = 'nocreate'; } - if ( $this->targetType == AbstractBlock::TYPE_USER && !$this->isAutoblocking ) { + if ( $this->target->getType() == AbstractBlock::TYPE_USER && !$this->isAutoblocking ) { // For grepping: message block-log-flags-noautoblock $flags[] = 'noautoblock'; } diff --git a/includes/block/BlockUserFactory.php b/includes/block/BlockUserFactory.php index 23e0422bd69c..5323d53e0982 100644 --- a/includes/block/BlockUserFactory.php +++ b/includes/block/BlockUserFactory.php @@ -31,7 +31,7 @@ interface BlockUserFactory { /** * Create BlockUser * - * @param string|UserIdentity $target Target of the block + * @param BlockTarget|string|UserIdentity $target Target of the block * @param Authority $performer Performer of the block * @param string $expiry Expiry of the block (timestamp or 'infinity') * @param string $reason Reason of the block diff --git a/includes/block/BlockUtils.php b/includes/block/BlockUtils.php index fd833becf380..a701c75df2dc 100644 --- a/includes/block/BlockUtils.php +++ b/includes/block/BlockUtils.php @@ -21,14 +21,8 @@ namespace MediaWiki\Block; -use MediaWiki\Config\ServiceOptions; -use MediaWiki\MainConfigNames; use MediaWiki\Status\Status; use MediaWiki\User\UserIdentity; -use MediaWiki\User\UserIdentityLookup; -use MediaWiki\User\UserIdentityValue; -use MediaWiki\User\UserNameUtils; -use Wikimedia\IPUtils; /** * Backend class for blocking utils @@ -42,34 +36,14 @@ use Wikimedia\IPUtils; * - block target validation * - parsing the target and type of a block in the database * + * @deprecated since 1.44 use BlockTargetFactory * @since 1.36 */ class BlockUtils { - private ServiceOptions $options; - private UserIdentityLookup $userIdentityLookup; - private UserNameUtils $userNameUtils; + private BlockTargetFactory $blockTargetFactory; - /** @var string|false */ - private $wikiId; - - /** - * @internal Only for use by ServiceWiring - */ - public const CONSTRUCTOR_OPTIONS = [ - MainConfigNames::BlockCIDRLimit, - ]; - - public function __construct( - ServiceOptions $options, - UserIdentityLookup $userIdentityLookup, - UserNameUtils $userNameUtils, - /* string|false */ $wikiId = Block::LOCAL - ) { - $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); - $this->options = $options; - $this->userIdentityLookup = $userIdentityLookup; - $this->userNameUtils = $userNameUtils; - $this->wikiId = $wikiId; + public function __construct( BlockTargetFactory $blockTargetFactory ) { + $this->blockTargetFactory = $blockTargetFactory; } /** @@ -91,55 +65,12 @@ class BlockUtils { * @return array [ UserIdentity|String|null, int|null ] */ public function parseBlockTarget( $target ): array { - // We may have been through this before - if ( $target instanceof UserIdentity ) { - if ( IPUtils::isValid( $target->getName() ) ) { - return [ $target, AbstractBlock::TYPE_IP ]; - } else { - return [ $target, AbstractBlock::TYPE_USER ]; - } - } elseif ( $target === null ) { + $targetObj = $this->blockTargetFactory->newFromLegacyUnion( $target ); + if ( $targetObj ) { + return $targetObj->getLegacyTuple(); + } else { return [ null, null ]; } - - $target = trim( $target ); - - if ( IPUtils::isValid( $target ) ) { - return [ - UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $target ), $this->wikiId ), - AbstractBlock::TYPE_IP - ]; - - } elseif ( IPUtils::isValidRange( $target ) ) { - // Can't create a UserIdentity from an IP range - return [ IPUtils::sanitizeRange( $target ), AbstractBlock::TYPE_RANGE ]; - } - - if ( preg_match( '/^#\d+$/', $target ) ) { - // Autoblock reference in the form "#12345" - return [ substr( $target, 1 ), AbstractBlock::TYPE_AUTO ]; - } - - $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $target ); - if ( $userFromDB instanceof UserIdentity ) { - // Note that since numbers are valid usernames, a $target of "12345" will be - // considered a UserIdentity. If you want to pass a block ID, prepend a hash "#12345", - // since hash characters are not valid in usernames or titles generally. - return [ $userFromDB, AbstractBlock::TYPE_USER ]; - } - - // Wrap the invalid user in a UserIdentityValue. - // This allows validateTarget() to return a "nosuchusershort" message, - // which is needed for Special:Block. - $canonicalName = $this->userNameUtils->getCanonical( $target ); - if ( $canonicalName !== false ) { - return [ - new UserIdentityValue( 0, $canonicalName ), - AbstractBlock::TYPE_USER - ]; - } - - return [ null, null ]; } /** @@ -151,25 +82,9 @@ class BlockUtils { * @return array [ UserIdentity|String|null, int|null ] */ public function parseBlockTargetRow( $row ) { - if ( $row->bt_auto ) { - return [ $row->bl_id, AbstractBlock::TYPE_AUTO ]; - } elseif ( isset( $row->bt_user ) ) { - if ( isset( $row->bt_user_text ) ) { - $user = new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId ); - } else { - $user = $this->userIdentityLookup->getUserIdentityByUserId( $row->bt_user ); - } - return [ $user, AbstractBlock::TYPE_USER ]; - } elseif ( $row->bt_address === null ) { - return [ null, null ]; - } elseif ( IPUtils::isValid( $row->bt_address ) ) { - return [ - UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $row->bt_address ), $this->wikiId ), - AbstractBlock::TYPE_IP - ]; - } elseif ( IPUtils::isValidRange( $row->bt_address ) ) { - // Can't create a UserIdentity from an IP range - return [ IPUtils::sanitizeRange( $row->bt_address ), AbstractBlock::TYPE_RANGE ]; + $target = $this->blockTargetFactory->newFromRowRedacted( $row ); + if ( $target ) { + return $target->getLegacyTuple(); } else { return [ null, null ]; } @@ -183,90 +98,12 @@ class BlockUtils { * @return Status */ public function validateTarget( $value ): Status { - [ $target, $type ] = $this->parseBlockTarget( $value ); - - $status = Status::newGood( $target ); - - switch ( $type ) { - case AbstractBlock::TYPE_USER: - if ( !$target->isRegistered() ) { - $status->fatal( - 'nosuchusershort', - wfEscapeWikiText( $target->getName() ) - ); - } - break; - - case AbstractBlock::TYPE_RANGE: - [ $ip, $range ] = explode( '/', $target, 2 ); - - if ( IPUtils::isIPv4( $ip ) ) { - $status->merge( $this->validateIPv4Range( (int)$range ) ); - } elseif ( IPUtils::isIPv6( $ip ) ) { - $status->merge( $this->validateIPv6Range( (int)$range ) ); - } else { - // Something is FUBAR - $status->fatal( 'badipaddress' ); - } - break; - - case AbstractBlock::TYPE_IP: - // All is well - break; - - default: - $status->fatal( 'badipaddress' ); - break; - } - - return $status; - } - - /** - * Validate an IPv4 range - * - * @param int $range - * - * @return Status - */ - private function validateIPv4Range( int $range ): Status { - $status = Status::newGood(); - $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit ); - - if ( $blockCIDRLimit['IPv4'] == 32 ) { - // Range block effectively disabled - $status->fatal( 'range_block_disabled' ); - } elseif ( $range > 32 ) { - // Such a range cannot exist - $status->fatal( 'ip_range_invalid' ); - } elseif ( $range < $blockCIDRLimit['IPv4'] ) { - $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv4'] ); + $target = $this->blockTargetFactory->newFromLegacyUnion( $value ); + if ( $target ) { + return Status::wrap( $target->validateForCreation() ); + } else { + return Status::newFatal( 'badipaddress' ); } - - return $status; } - /** - * Validate an IPv6 range - * - * @param int $range - * - * @return Status - */ - private function validateIPv6Range( int $range ): Status { - $status = Status::newGood(); - $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit ); - - if ( $blockCIDRLimit['IPv6'] == 128 ) { - // Range block effectively disabled - $status->fatal( 'range_block_disabled' ); - } elseif ( $range > 128 ) { - // Dodgy range - such a range cannot exist - $status->fatal( 'ip_range_invalid' ); - } elseif ( $range < $blockCIDRLimit['IPv6'] ) { - $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv6'] ); - } - - return $status; - } } diff --git a/includes/block/BlockUtilsFactory.php b/includes/block/BlockUtilsFactory.php index 2691b8041c07..76607f3c4fce 100644 --- a/includes/block/BlockUtilsFactory.php +++ b/includes/block/BlockUtilsFactory.php @@ -21,40 +21,22 @@ namespace MediaWiki\Block; -use MediaWiki\Config\ServiceOptions; -use MediaWiki\User\ActorStoreFactory; -use MediaWiki\User\UserNameUtils; -use Wikimedia\Rdbms\LBFactory; +use MediaWiki\WikiMap\WikiMap; /** + * @deprecated since 1.44 use CrossWikiBlockTargetFactory * @since 1.42 */ class BlockUtilsFactory { - /** - * @internal For use by ServiceWiring - */ - public const CONSTRUCTOR_OPTIONS = BlockUtils::CONSTRUCTOR_OPTIONS; - - private ServiceOptions $options; - private ActorStoreFactory $actorStoreFactory; - private UserNameUtils $userNameUtils; - private LBFactory $loadBalancerFactory; + private CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory; /** @var BlockUtils[] */ private array $storeCache = []; public function __construct( - ServiceOptions $options, - ActorStoreFactory $actorStoreFactory, - UserNameUtils $userNameUtils, - LBFactory $loadBalancerFactory + CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory ) { - $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); - - $this->options = $options; - $this->actorStoreFactory = $actorStoreFactory; - $this->userNameUtils = $userNameUtils; - $this->loadBalancerFactory = $loadBalancerFactory; + $this->crossWikiBlockTargetFactory = $crossWikiBlockTargetFactory; } /** @@ -62,17 +44,14 @@ class BlockUtilsFactory { * @return BlockUtils */ public function getBlockUtils( $wikiId = Block::LOCAL ): BlockUtils { - if ( is_string( $wikiId ) && $this->loadBalancerFactory->getLocalDomainID() === $wikiId ) { + if ( is_string( $wikiId ) && WikiMap::getCurrentWikiId() === $wikiId ) { $wikiId = Block::LOCAL; } $storeCacheKey = $wikiId === Block::LOCAL ? 'LOCAL' : 'crosswikistore-' . $wikiId; if ( !isset( $this->storeCache[$storeCacheKey] ) ) { $this->storeCache[$storeCacheKey] = new BlockUtils( - $this->options, - $this->actorStoreFactory->getUserIdentityLookup( $wikiId ), - $this->userNameUtils, - $wikiId + $this->crossWikiBlockTargetFactory->getFactory( $wikiId ) ); } return $this->storeCache[$storeCacheKey]; diff --git a/includes/block/CompositeBlock.php b/includes/block/CompositeBlock.php index c74481b98fbe..76707a46cdee 100644 --- a/includes/block/CompositeBlock.php +++ b/includes/block/CompositeBlock.php @@ -57,7 +57,7 @@ class CompositeBlock extends AbstractBlock { throw new InvalidArgumentException( 'No blocks given' ); } return new self( [ - 'address' => $originalBlocks[0]->target, + 'target' => $originalBlocks[0]->target, 'reason' => new Message( 'blockedtext-composite-reason' ), 'originalBlocks' => $originalBlocks, ] ); diff --git a/includes/block/CrossWikiBlockTargetFactory.php b/includes/block/CrossWikiBlockTargetFactory.php new file mode 100644 index 000000000000..f2560ed0a0e7 --- /dev/null +++ b/includes/block/CrossWikiBlockTargetFactory.php @@ -0,0 +1,60 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Config\ServiceOptions; +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\User\ActorStoreFactory; +use MediaWiki\User\UserNameUtils; +use MediaWiki\WikiMap\WikiMap; + +/** + * Factory for BlockTargetFactory objects. This is needed for cross-wiki block + * operations, since BlockTargetFactory needs a wiki ID passed to its constructor. + * + * @since 1.44 + */ +class CrossWikiBlockTargetFactory { + private ActorStoreFactory $actorStoreFactory; + private UserNameUtils $userNameUtils; + private ServiceOptions $options; + + /** @var BlockTargetFactory[] */ + private array $storeCache = []; + + /** + * @internal Only for use by ServiceWiring + */ + public const CONSTRUCTOR_OPTIONS = BlockTargetFactory::CONSTRUCTOR_OPTIONS; + + public function __construct( + ServiceOptions $options, + ActorStoreFactory $actorStoreFactory, + UserNameUtils $userNameUtils + ) { + $this->options = $options; + $this->actorStoreFactory = $actorStoreFactory; + $this->userNameUtils = $userNameUtils; + } + + /** + * @param string|false $wikiId + * @return BlockTargetFactory + */ + public function getFactory( $wikiId = WikiAwareEntity::LOCAL ) { + if ( is_string( $wikiId ) && WikiMap::getCurrentWikiId() === $wikiId ) { + $wikiId = WikiAwareEntity::LOCAL; + } + + $storeCacheKey = $wikiId === WikiAwareEntity::LOCAL ? 'LOCAL' : 'crosswikistore-' . $wikiId; + if ( !isset( $this->storeCache[$storeCacheKey] ) ) { + $this->storeCache[$storeCacheKey] = new BlockTargetFactory( + $this->options, + $this->actorStoreFactory->getUserIdentityLookup( $wikiId ), + $this->userNameUtils, + $wikiId + ); + } + return $this->storeCache[$storeCacheKey]; + } +} diff --git a/includes/block/DatabaseBlock.php b/includes/block/DatabaseBlock.php index c316ff360bb9..c478ed49bd5e 100644 --- a/includes/block/DatabaseBlock.php +++ b/includes/block/DatabaseBlock.php @@ -33,8 +33,6 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Title\Title; use MediaWiki\User\UserIdentity; use stdClass; -use UnexpectedValueException; -use Wikimedia\IPUtils; use Wikimedia\Rdbms\IDatabase; /** @@ -151,8 +149,7 @@ class DatabaseBlock extends AbstractBlock { */ public function equals( DatabaseBlock $block ) { return ( - (string)$this->target == (string)$block->target - && $this->type == $block->type + ( $this->target ? $this->target->equals( $block->target ) : $block->target === null ) && $this->auto == $block->auto && $this->isHardblock() == $block->isHardblock() && $this->isCreateAccountBlocked() == $block->isCreateAccountBlocked() @@ -290,38 +287,41 @@ class DatabaseBlock extends AbstractBlock { } /** - * Get the IP address at the start of the range in Hex form - * @return string IP in Hex form + * Get the IP address at the start of the range in hex form, or null if + * the target is not a range. + * + * @return string|null */ public function getRangeStart() { - switch ( $this->type ) { - case self::TYPE_USER: - return ''; - case self::TYPE_IP: - return IPUtils::toHex( $this->target ); - case self::TYPE_RANGE: - [ $start, /*...*/ ] = IPUtils::parseRange( $this->target ); - return $start; - default: - throw new UnexpectedValueException( "Block with invalid type" ); - } + return $this->target instanceof RangeBlockTarget + ? $this->target->getHexRangeStart() : null; } /** - * Get the IP address at the end of the range in Hex form - * @return string IP in Hex form + * Get the IP address at the end of the range in hex form, or null if + * the target is not a range. + * + * @return string|null */ public function getRangeEnd() { - switch ( $this->type ) { - case self::TYPE_USER: - return ''; - case self::TYPE_IP: - return IPUtils::toHex( $this->target ); - case self::TYPE_RANGE: - [ /*...*/, $end ] = IPUtils::parseRange( $this->target ); - return $end; - default: - throw new UnexpectedValueException( "Block with invalid type" ); + return $this->target instanceof RangeBlockTarget + ? $this->target->getHexRangeEnd() : null; + } + + /** + * Get the IP address of the single-IP block or the start of the range in + * hexadecimal form, or null if the target is not an IP. + * + * @since 1.44 + * @return string|null + */ + public function getIpHex() { + if ( $this->target instanceof RangeBlockTarget ) { + return $this->target->getHexRangeStart(); + } elseif ( $this->target instanceof AnonIpBlockTarget ) { + return $this->target->toHex(); + } else { + return null; } } @@ -501,7 +501,7 @@ class DatabaseBlock extends AbstractBlock { * @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. + * autoblock, we have to check the auto property instead. */ public function getType(): ?int { return $this->auto diff --git a/includes/block/DatabaseBlockStore.php b/includes/block/DatabaseBlockStore.php index bf53d04e166a..e3bcadcdf60b 100644 --- a/includes/block/DatabaseBlockStore.php +++ b/includes/block/DatabaseBlockStore.php @@ -35,7 +35,6 @@ use MediaWiki\User\ActorStoreFactory; use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; -use MediaWiki\User\UserIdentityValue; use Psr\Log\LoggerInterface; use RuntimeException; use stdClass; @@ -82,7 +81,7 @@ class DatabaseBlockStore { private ReadOnlyMode $readOnlyMode; private UserFactory $userFactory; private TempUserConfig $tempUserConfig; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private AutoblockExemptionList $autoblockExemptionList; public function __construct( @@ -96,7 +95,7 @@ class DatabaseBlockStore { ReadOnlyMode $readOnlyMode, UserFactory $userFactory, TempUserConfig $tempUserConfig, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, AutoblockExemptionList $autoblockExemptionList, /* string|false */ $wikiId = DatabaseBlock::LOCAL ) { @@ -114,7 +113,7 @@ class DatabaseBlockStore { $this->readOnlyMode = $readOnlyMode; $this->userFactory = $userFactory; $this->tempUserConfig = $tempUserConfig; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->autoblockExemptionList = $autoblockExemptionList; } @@ -191,16 +190,15 @@ class DatabaseBlockStore { * Load blocks from the database which target the specific target exactly, or which cover the * vague target. * - * @param UserIdentity|string|null $specificTarget - * @param int|null $specificType + * @param BlockTarget|null $specificTarget * @param bool $fromPrimary - * @param UserIdentity|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. + * @param BlockTarget|null $vagueTarget Also search for blocks affecting + * this target. Doesn't make any sense to use TYPE_AUTO here. Leave blank to + * skip IP lookups. * @return DatabaseBlock[] Any relevant blocks */ private function newLoad( $specificTarget, - $specificType, $fromPrimary, $vagueTarget = null ) { @@ -214,49 +212,37 @@ class DatabaseBlockStore { $userNames = []; $addresses = []; $ranges = []; - if ( $specificType === Block::TYPE_USER ) { - if ( $specificTarget instanceof UserIdentity ) { - $userId = $specificTarget->getId( $this->wikiId ); - if ( $userId ) { - $userIds[] = $specificTarget->getId( $this->wikiId ); - } else { - // A nonexistent user can have no blocks. - // This case is hit in testing, possibly production too. - // Ignoring the user is optimal for production performance. - } + if ( $specificTarget instanceof UserBlockTarget ) { + $userId = $specificTarget->getUserIdentity()->getId( $this->wikiId ); + if ( $userId ) { + $userIds[] = $userId; } else { - $userNames[] = (string)$specificTarget; + // A nonexistent user can have no blocks. + // This case is hit in testing, possibly production too. + // Ignoring the user is optimal for production performance. } - } elseif ( in_array( $specificType, [ Block::TYPE_IP, Block::TYPE_RANGE ], true ) ) { + } elseif ( $specificTarget instanceof AnonIpBlockTarget + || $specificTarget instanceof RangeBlockTarget + ) { $addresses[] = (string)$specificTarget; } // Be aware that the != '' check is explicit, since empty values will be // passed by some callers (T31116) - if ( $vagueTarget != '' ) { - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $vagueTarget ); - switch ( $type ) { - case Block::TYPE_USER: - // Slightly weird, but who are we to argue? - /** @var UserIdentity $vagueUser */ - $vagueUser = $target; - if ( $vagueUser->getId( $this->wikiId ) ) { - $userIds[] = $vagueUser->getId( $this->wikiId ); - } else { - $userNames[] = $vagueUser->getName(); - } - break; - - case Block::TYPE_IP: - $ranges[] = [ IPUtils::toHex( $target ), null ]; - break; - - case Block::TYPE_RANGE: - $ranges[] = IPUtils::parseRange( $target ); - break; - - default: - $this->logger->debug( "Ignoring invalid vague target" ); + if ( $vagueTarget !== null ) { + if ( $vagueTarget instanceof UserBlockTarget ) { + // Slightly weird, but who are we to argue? + $vagueUser = $vagueTarget->getUserIdentity(); + $userId = $vagueUser->getId( $this->wikiId ); + if ( $userId ) { + $userIds[] = $userId; + } else { + $userNames[] = $vagueUser->getName(); + } + } elseif ( $vagueTarget instanceof BlockTargetWithIp ) { + $ranges[] = $vagueTarget->toHexRange(); + } else { + $this->logger->debug( "Ignoring invalid vague target" ); } } @@ -302,9 +288,9 @@ class DatabaseBlockStore { // Don't use anon only blocks on users if ( - $specificType == Block::TYPE_USER && $specificTarget && + $specificTarget instanceof UserBlockTarget && !$block->isHardblock() && - !$this->tempUserConfig->isTempName( $specificTarget ) + !$this->tempUserConfig->isTempName( $specificTarget->toString() ) ) { continue; } @@ -348,21 +334,7 @@ class DatabaseBlockStore { // Lower will be better $bestBlockScore = 100; foreach ( $blocks as $block ) { - if ( $block->getType() == Block::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->getTargetName(); - $max = IPUtils::isIPv6( $target ) ? 128 : 32; - [ , $bits ] = IPUtils::parseCIDR( $target ); - $size = $max - $bits; - - // Rank a range block covering a single IP equally with a single-IP block - $score = Block::TYPE_RANGE - 1 + ( $size / $max ); - - } else { - $score = $block->getType(); - } - + $score = $block->getTarget()->getSpecificity(); if ( $score < $bestBlockScore ) { $bestBlockScore = $score; $bestBlock = $block; @@ -434,10 +406,8 @@ class DatabaseBlockStore { * @return DatabaseBlock */ public function newFromRow( IReadableDatabase $db, $row ) { - $address = $row->bt_address - ?? new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId ); return new DatabaseBlock( [ - 'address' => $address, + 'target' => $this->blockTargetFactory->newFromRowRaw( $row ), 'wiki' => $this->wikiId, 'timestamp' => $row->bl_timestamp, 'auto' => (bool)$row->bt_auto, @@ -465,7 +435,7 @@ class DatabaseBlockStore { * Given a target and the target's type, get an existing block object if possible. * * @since 1.42 - * @param string|UserIdentity|int|null $specificTarget A block target, which may be one of + * @param BlockTarget|string|UserIdentity|int|null $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 @@ -476,7 +446,7 @@ class DatabaseBlockStore { * 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|UserIdentity|int|null $vagueTarget As above, but we will search for *any* + * @param BlockTarget|string|UserIdentity|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 $fromPrimary Whether to use the DB_PRIMARY database @@ -497,8 +467,8 @@ class DatabaseBlockStore { * This is similar to DatabaseBlockStore::newFromTarget, but it returns all the relevant blocks. * * @since 1.42 - * @param string|UserIdentity|int|null $specificTarget - * @param string|UserIdentity|int|null $vagueTarget + * @param BlockTarget|string|UserIdentity|int|null $specificTarget + * @param BlockTarget|string|UserIdentity|int|null $vagueTarget * @param bool $fromPrimary * @return DatabaseBlock[] Any relevant blocks */ @@ -507,22 +477,21 @@ class DatabaseBlockStore { $vagueTarget = null, $fromPrimary = false ) { - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $specificTarget ); - if ( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ) { - $block = $this->newFromID( $target ); + if ( !( $specificTarget instanceof BlockTarget ) ) { + $specificTarget = $this->blockTargetFactory->newFromLegacyUnion( $specificTarget ); + } + if ( $vagueTarget !== null && !( $vagueTarget instanceof BlockTarget ) ) { + $vagueTarget = $this->blockTargetFactory->newFromLegacyUnion( $vagueTarget ); + } + if ( $specificTarget instanceof AutoBlockTarget ) { + $block = $this->newFromID( $specificTarget->getId() ); return $block ? [ $block ] : []; - } elseif ( $target === null && $vagueTarget == '' ) { + } elseif ( $specificTarget === null && $vagueTarget === null ) { // 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 []; - } elseif ( in_array( - $type, - [ Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ] ) - ) { - return $this->newLoad( $target, $type, $fromPrimary, $vagueTarget ); + } else { + return $this->newLoad( $specificTarget, $fromPrimary, $vagueTarget ); } - return []; } /** @@ -612,6 +581,32 @@ class DatabaseBlockStore { /** @name Database write methods */ /** + * Create a DatabaseBlock representing an unsaved block. Pass the returned + * object to insertBlock(). + * + * @since 1.44 + * + * @param array $options Options as documented in DatabaseBlock and + * AbstractBlock, and additionally: + * - targetUser: (UserIdentity) The UserIdentity to block + * - targetString (string) A string specifying the block target + * @return DatabaseBlock + */ + public function newUnsaved( array $options ): DatabaseBlock { + if ( isset( $options['targetUser'] ) ) { + $options['target'] = $this->blockTargetFactory + ->newUserBlockTarget( $options['targetUser'] ); + unset( $options['targetUser'] ); + } + if ( isset( $options['targetString'] ) ) { + $options['target'] = $this->blockTargetFactory + ->newFromString( $options['targetString'] ); + unset( $options['targetString'] ); + } + return new DatabaseBlock( $options ); + } + + /** * Delete expired blocks from the block table * * @internal only public for use in DatabaseBlock @@ -953,18 +948,19 @@ class DatabaseBlockStore { } /** - * Get conditions matching the block's block_target row + * Get conditions matching an existing block's block_target row * * @param DatabaseBlock $block * @return array */ private function getTargetConds( DatabaseBlock $block ) { - if ( $block->getType() === Block::TYPE_USER ) { + $target = $block->getTarget(); + if ( $target instanceof UserBlockTarget ) { return [ - 'bt_user' => $block->getTargetUserIdentity()->getId( $this->wikiId ) + 'bt_user' => $target->getUserIdentity()->getId( $this->wikiId ) ]; } else { - return [ 'bt_address' => $block->getTargetName() ]; + return [ 'bt_address' => $target->toString() ]; } } @@ -987,24 +983,24 @@ class DatabaseBlockStore { IDatabase $dbw, $expectedTargetCount ) { - $isUser = $block->getType() === Block::TYPE_USER; - $isRange = $block->getType() === Block::TYPE_RANGE; + $target = $block->getTarget(); + // Note: for new autoblocks, the target is an IpBlockTarget $isAuto = $block->getType() === Block::TYPE_AUTO; - $isSingle = !$isUser && !$isRange; - $targetAddress = $isUser ? null : $block->getTargetName(); - $targetUserName = $isUser ? $block->getTargetName() : null; - $targetUserId = $isUser - ? $block->getTargetUserIdentity()->getId( $this->wikiId ) : null; - - // Update bt_count field in existing target, if there is one - if ( $isUser ) { + if ( $target instanceof UserBlockTarget ) { + $targetAddress = null; + $targetUserName = (string)$target; + $targetUserId = $target->getUserIdentity()->getId( $this->wikiId ); $targetConds = [ 'bt_user' => $targetUserId ]; } else { + $targetAddress = (string)$target; + $targetUserName = null; + $targetUserId = null; $targetConds = [ 'bt_address' => $targetAddress, 'bt_auto' => $isAuto, ]; } + $condsWithCount = $targetConds; if ( $expectedTargetCount !== null ) { $condsWithCount['bt_count'] = $expectedTargetCount; @@ -1062,9 +1058,9 @@ class DatabaseBlockStore { 'bt_user' => $targetUserId, 'bt_user_text' => $targetUserName, 'bt_auto' => $isAuto, - 'bt_range_start' => $isRange ? $block->getRangeStart() : null, - 'bt_range_end' => $isRange ? $block->getRangeEnd() : null, - 'bt_ip_hex' => $isSingle || $isRange ? $block->getRangeStart() : null, + 'bt_range_start' => $block->getRangeStart(), + 'bt_range_end' => $block->getRangeEnd(), + 'bt_ip_hex' => $block->getIpHex(), 'bt_count' => 1 ]; $dbw->newInsertQueryBuilder() @@ -1167,7 +1163,7 @@ class DatabaseBlockStore { * * @since 1.42 * @param DatabaseBlock $block - * @param UserIdentity|string $newTarget + * @param BlockTarget|UserIdentity|string $newTarget * @return bool True if the update was successful, false if there was no * match for the block ID. */ @@ -1179,6 +1175,9 @@ class DatabaseBlockStore { __METHOD__ . " requires that a block id be set\n" ); } + if ( !( $newTarget instanceof BlockTarget ) ) { + $newTarget = $this->blockTargetFactory->newFromLegacyUnion( $newTarget ); + } $oldTargetConds = $this->getTargetConds( $block ); $block->setTarget( $newTarget ); @@ -1383,18 +1382,17 @@ class DatabaseBlockStore { return []; } - $type = $block->getType(); - if ( $type !== AbstractBlock::TYPE_USER ) { + $target = $block->getTarget(); + if ( !( $target instanceof UserBlockTarget ) ) { // Autoblocks only apply to users return []; } $dbr = $this->getReplicaDB(); - $targetUser = $block->getTargetUserIdentity(); - $actor = $targetUser ? $this->actorStoreFactory + $actor = $this->actorStoreFactory ->getActorNormalization( $this->wikiId ) - ->findActorId( $targetUser, $dbr ) : null; + ->findActorId( $target->getUserIdentity(), $dbr ); if ( !$actor ) { $this->logger->debug( 'No actor found to retroactively autoblock' ); @@ -1435,20 +1433,19 @@ class DatabaseBlockStore { } $parentBlock->assertWiki( $this->wikiId ); - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $autoblockIP ); - if ( $type != Block::TYPE_IP ) { - $this->logger->debug( "Autoblock not supported for ip ranges." ); + if ( !IPUtils::isValid( $autoblockIP ) ) { + $this->logger->debug( "Invalid autoblock IP" ); return false; } - $target = (string)$target; + $target = $this->blockTargetFactory->newAnonIpBlockTarget( $autoblockIP ); // Check if autoblock exempt. - if ( $this->autoblockExemptionList->isExempt( $target ) ) { + if ( $this->autoblockExemptionList->isExempt( $autoblockIP ) ) { return false; } // Allow hooks to cancel the autoblock. - if ( !$this->hookRunner->onAbortAutoblock( $target, $parentBlock ) ) { + if ( !$this->hookRunner->onAbortAutoblock( $autoblockIP, $parentBlock ) ) { $this->logger->debug( "Autoblock aborted by hook." ); return false; } @@ -1456,7 +1453,7 @@ class DatabaseBlockStore { // It's okay to autoblock. Go ahead and insert/update the block... // Do not add a *new* block if the IP is already blocked. - $blocks = $this->newLoad( $target, Block::TYPE_IP, false ); + $blocks = $this->newLoad( $target, false ); if ( $blocks ) { foreach ( $blocks as $ipblock ) { // Check if the block is an autoblock and would exceed the user block @@ -1480,7 +1477,7 @@ class DatabaseBlockStore { $expiry = $this->getAutoblockExpiry( $timestamp, $parentBlock->getExpiry() ); $autoblock = new DatabaseBlock( [ 'wiki' => $this->wikiId, - 'address' => UserIdentityValue::newAnonymous( $target, $this->wikiId ), + 'target' => $target, 'by' => $blocker, 'reason' => $this->getAutoblockReason( $parentBlock ), 'decodedTimestamp' => $timestamp, diff --git a/includes/block/DatabaseBlockStoreFactory.php b/includes/block/DatabaseBlockStoreFactory.php index bf77cda3d21e..915574c8355a 100644 --- a/includes/block/DatabaseBlockStoreFactory.php +++ b/includes/block/DatabaseBlockStoreFactory.php @@ -51,7 +51,7 @@ class DatabaseBlockStoreFactory { private ReadOnlyMode $readOnlyMode; private UserFactory $userFactory; private TempUserConfig $tempUserConfig; - private BlockUtilsFactory $blockUtilsFactory; + private CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory; private AutoblockExemptionList $autoblockExemptionList; /** @var DatabaseBlockStore[] */ @@ -68,7 +68,7 @@ class DatabaseBlockStoreFactory { ReadOnlyMode $readOnlyMode, UserFactory $userFactory, TempUserConfig $tempUserConfig, - BlockUtilsFactory $blockUtilsFactory, + CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory, AutoblockExemptionList $autoblockExemptionList ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); @@ -83,7 +83,7 @@ class DatabaseBlockStoreFactory { $this->readOnlyMode = $readOnlyMode; $this->userFactory = $userFactory; $this->tempUserConfig = $tempUserConfig; - $this->blockUtilsFactory = $blockUtilsFactory; + $this->crossWikiBlockTargetFactory = $crossWikiBlockTargetFactory; $this->autoblockExemptionList = $autoblockExemptionList; } @@ -109,7 +109,7 @@ class DatabaseBlockStoreFactory { $this->readOnlyMode, $this->userFactory, $this->tempUserConfig, - $this->blockUtilsFactory->getBlockUtils( $wikiId ), + $this->crossWikiBlockTargetFactory->getFactory( $wikiId ), $this->autoblockExemptionList, $wikiId ); diff --git a/includes/block/RangeBlockTarget.php b/includes/block/RangeBlockTarget.php new file mode 100644 index 000000000000..db9da098c7fe --- /dev/null +++ b/includes/block/RangeBlockTarget.php @@ -0,0 +1,116 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use StatusValue; +use Wikimedia\IPUtils; + +/** + * A block target for an IP address range + * + * @since 1.44 + */ +class RangeBlockTarget extends BlockTarget implements BlockTargetWithIp { + private string $cidr; + + /** + * @var array The minimum prefix lengths indexed by protocol (IPv4 or IPv6) + */ + private array $limits; + + /** + * @param string $cidr The range specification in CIDR notation + * @param array $limits The minimum prefix lengths indexed by protocol (IPv4 or IPv6) + * @param string|false $wikiId The wiki ID + */ + public function __construct( string $cidr, array $limits, $wikiId ) { + parent::__construct( $wikiId ); + $this->cidr = $cidr; + $this->limits = $limits; + } + + public function toString(): string { + return $this->cidr; + } + + public function getType(): int { + return Block::TYPE_RANGE; + } + + public function getLogPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->cidr, $this->wikiId ); + } + + public function getSpecificity() { + // This is the number of bits that are allowed to vary in the block, give + // or take some floating point errors + [ $ip, $bits ] = explode( '/', $this->cidr, 2 ); + $max = IPUtils::isIPv6( $ip ) ? 128 : 32; + $size = $max - (int)$bits; + + // Rank a range block covering a single IP equally with a single-IP block + return 2 + ( $size / $max ); + } + + public function validateForCreation(): StatusValue { + $status = StatusValue::newGood(); + [ $ip, $prefixLength ] = explode( '/', $this->cidr, 2 ); + + if ( IPUtils::isIPv4( $ip ) ) { + $minLength = $this->limits['IPv4']; + $totalLength = 32; + } elseif ( IPUtils::isIPv6( $ip ) ) { + $minLength = $this->limits['IPv6']; + $totalLength = 128; + } else { + // The factory should not have called the constructor with an invalid range + throw new \RuntimeException( 'invalid IP range' ); + } + + if ( $minLength == $totalLength ) { + // Range block effectively disabled + $status->fatal( 'range_block_disabled' ); + } elseif ( $prefixLength > $totalLength ) { + // Such a range cannot exist + $status->fatal( 'ip_range_invalid' ); + } elseif ( $prefixLength < $minLength ) { + $status->fatal( 'ip_range_toolarge', $minLength ); + } + + return $status; + } + + public function toHexRange() { + $range = IPUtils::parseRange( $this->cidr ); + if ( count( $range ) !== 2 || !is_string( $range[0] ) || !is_string( $range[1] ) ) { + throw new \RuntimeException( + 'Failed to parse range: constructor caller should have validated it' ); + } + return $range; + } + + /** + * Get the start of the range in hexadecimal form. + * + * @return string + */ + public function getHexRangeStart(): string { + return $this->toHexRange()[0]; + } + + /** + * Get the end of the range in hexadecimal form. + * + * @return string + */ + public function getHexRangeEnd(): string { + return $this->toHexRange()[1]; + } + + protected function getLegacyUnion() { + return $this->cidr; + } + +} diff --git a/includes/block/UnblockUser.php b/includes/block/UnblockUser.php index 82d2a6561270..5cb275c7cc60 100644 --- a/includes/block/UnblockUser.php +++ b/includes/block/UnblockUser.php @@ -28,7 +28,6 @@ use MediaWiki\HookContainer\HookRunner; use MediaWiki\Message\Message; use MediaWiki\Permissions\Authority; use MediaWiki\Status\Status; -use MediaWiki\Title\TitleValue; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use RevisionDeleteUser; @@ -41,16 +40,13 @@ use RevisionDeleteUser; class UnblockUser { private BlockPermissionChecker $blockPermissionChecker; private DatabaseBlockStore $blockStore; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private UserFactory $userFactory; private HookRunner $hookRunner; - /** @var UserIdentity|string */ + /** @var BlockTarget|null */ private $target; - /** @var int */ - private $targetType; - private ?DatabaseBlock $block; private ?DatabaseBlock $blockToRemove = null; @@ -67,7 +63,7 @@ class UnblockUser { /** * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory * @param DatabaseBlockStore $blockStore - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory * @param UserFactory $userFactory * @param HookContainer $hookContainer * @param DatabaseBlock|null $blockToRemove @@ -79,7 +75,7 @@ class UnblockUser { public function __construct( BlockPermissionCheckerFactory $blockPermissionCheckerFactory, DatabaseBlockStore $blockStore, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, UserFactory $userFactory, HookContainer $hookContainer, ?DatabaseBlock $blockToRemove, @@ -90,25 +86,19 @@ class UnblockUser { ) { // Process dependencies $this->blockStore = $blockStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->userFactory = $userFactory; $this->hookRunner = new HookRunner( $hookContainer ); // Process params if ( $blockToRemove !== null ) { $this->blockToRemove = $blockToRemove; - $this->target = $blockToRemove->getTargetUserIdentity() - ?? $blockToRemove->getTargetName(); - $this->targetType = $blockToRemove->getType() ?? -1; + $this->target = $blockToRemove->getTarget(); + } elseif ( $target instanceof BlockTarget ) { + $this->target = $target; } else { - [ $this->target, $this->targetType ] = $this->blockUtils->parseBlockTarget( $target ); - if ( - $this->targetType === AbstractBlock::TYPE_AUTO && - is_numeric( $this->target ) - ) { - // Needed, because BlockUtils::parseBlockTarget will strip the # from autoblocks. - $this->target = '#' . $this->target; - } + // TODO: deprecate (T382106) + $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target ); } $this->blockPermissionChecker = $blockPermissionCheckerFactory @@ -181,7 +171,7 @@ class UnblockUser { if ( $this->block->getType() === AbstractBlock::TYPE_RANGE && - $this->targetType === AbstractBlock::TYPE_IP + $this->target->getType() === AbstractBlock::TYPE_IP ) { return $status->fatal( 'ipb_blocked_as_range', $this->target, $this->block->getTargetName() ); } @@ -219,18 +209,10 @@ class UnblockUser { * Log the unblock to Special:Log/block */ private function log() { - // Redact IP for autoblocks - if ( $this->block->getType() === DatabaseBlock::TYPE_AUTO ) { - $page = TitleValue::tryNew( NS_USER, '#' . $this->block->getId() ); - } else { - $page = TitleValue::tryNew( NS_USER, $this->block->getTargetName() ); - } - + $page = $this->block->getRedactedTarget()->getLogPage(); $logEntry = new ManualLogEntry( 'block', 'unblock' ); - if ( $page !== null ) { - $logEntry->setTarget( $page ); - } + $logEntry->setTarget( $page ); $logEntry->setComment( $this->reason ); $logEntry->setPerformer( $this->performer->getUser() ); $logEntry->addTags( $this->tags ); diff --git a/includes/block/UnblockUserFactory.php b/includes/block/UnblockUserFactory.php index 520146adbd47..9c09c00baa28 100644 --- a/includes/block/UnblockUserFactory.php +++ b/includes/block/UnblockUserFactory.php @@ -29,7 +29,7 @@ use MediaWiki\User\UserIdentity; */ interface UnblockUserFactory { /** - * @param UserIdentity|string $target + * @param BlockTarget|UserIdentity|string $target * @param Authority $performer * @param string $reason * @param string[] $tags diff --git a/includes/block/UserBlockCommandFactory.php b/includes/block/UserBlockCommandFactory.php index 9981ee661300..64e4d80009be 100644 --- a/includes/block/UserBlockCommandFactory.php +++ b/includes/block/UserBlockCommandFactory.php @@ -32,7 +32,7 @@ use Psr\Log\LoggerInterface; class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { private BlockPermissionCheckerFactory $blockPermissionCheckerFactory; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private HookContainer $hookContainer; private BlockRestrictionStore $blockRestrictionStore; private ServiceOptions $options; @@ -52,7 +52,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { ServiceOptions $options, HookContainer $hookContainer, BlockPermissionCheckerFactory $blockPermissionCheckerFactory, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, DatabaseBlockStore $blockStore, BlockRestrictionStore $blockRestrictionStore, UserFactory $userFactory, @@ -66,7 +66,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { $this->options = $options; $this->hookContainer = $hookContainer; $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->blockStore = $blockStore; $this->blockRestrictionStore = $blockRestrictionStore; $this->userFactory = $userFactory; @@ -79,7 +79,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { /** * Create BlockUser * - * @param string|UserIdentity $target Target of the block + * @param BlockTarget|string|UserIdentity $target Target of the block * @param Authority $performer Performer of the block * @param string $expiry Expiry of the block (timestamp or 'infinity') * @param string $reason Reason of the block @@ -102,7 +102,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { $this->options, $this->blockRestrictionStore, $this->blockPermissionCheckerFactory, - $this->blockUtils, + $this->blockTargetFactory, $this->blockActionInfo, $this->hookContainer, $this->blockStore, @@ -148,7 +148,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { $this->options, $this->blockRestrictionStore, $this->blockPermissionCheckerFactory, - $this->blockUtils, + $this->blockTargetFactory, $this->blockActionInfo, $this->hookContainer, $this->blockStore, @@ -172,7 +172,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { * * @since 1.44 * - * @param UserIdentity|string $target + * @param BlockTarget|UserIdentity|string $target * @param Authority $performer * @param string $reason * @param string[] $tags @@ -188,7 +188,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { return new UnblockUser( $this->blockPermissionCheckerFactory, $this->blockStore, - $this->blockUtils, + $this->blockTargetFactory, $this->userFactory, $this->hookContainer, null, @@ -218,7 +218,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { return new UnblockUser( $this->blockPermissionCheckerFactory, $this->blockStore, - $this->blockUtils, + $this->blockTargetFactory, $this->userFactory, $this->hookContainer, $block, diff --git a/includes/block/UserBlockTarget.php b/includes/block/UserBlockTarget.php new file mode 100644 index 000000000000..9fb78503b4ad --- /dev/null +++ b/includes/block/UserBlockTarget.php @@ -0,0 +1,60 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use MediaWiki\User\UserIdentity; +use StatusValue; + +/** + * A block target for a registered user + * + * @since 1.44 + */ +class UserBlockTarget extends BlockTarget implements BlockTargetWithUserPage { + private UserIdentity $userIdentity; + + public function __construct( UserIdentity $userIdentity ) { + parent::__construct( $userIdentity->getWikiId() ); + $this->userIdentity = $userIdentity; + } + + public function toString(): string { + return $this->userIdentity->getName(); + } + + public function getType(): int { + return Block::TYPE_USER; + } + + public function getSpecificity() { + return 1; + } + + public function getLogPage(): PageReference { + return $this->getUserPage(); + } + + public function getUserPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->userIdentity->getName(), $this->wikiId ); + } + + public function getUserIdentity(): UserIdentity { + return $this->userIdentity; + } + + public function validateForCreation(): StatusValue { + if ( !$this->userIdentity->isRegistered() ) { + return StatusValue::newFatal( + 'nosuchusershort', + wfEscapeWikiText( $this->userIdentity->getName() ) + ); + } + return StatusValue::newGood(); + } + + protected function getLegacyUnion() { + return $this->getUserIdentity(); + } +} |