aboutsummaryrefslogtreecommitdiffstats
path: root/includes/block
diff options
context:
space:
mode:
Diffstat (limited to 'includes/block')
-rw-r--r--includes/block/AbstractBlock.php81
-rw-r--r--includes/block/AnonIpBlockTarget.php79
-rw-r--r--includes/block/AutoBlockTarget.php60
-rw-r--r--includes/block/Block.php25
-rw-r--r--includes/block/BlockErrorFormatter.php15
-rw-r--r--includes/block/BlockPermissionChecker.php38
-rw-r--r--includes/block/BlockPermissionCheckerFactory.php8
-rw-r--r--includes/block/BlockTarget.php117
-rw-r--r--includes/block/BlockTargetFactory.php280
-rw-r--r--includes/block/BlockTargetWithIp.php17
-rw-r--r--includes/block/BlockTargetWithUserIdentity.php19
-rw-r--r--includes/block/BlockTargetWithUserPage.php22
-rw-r--r--includes/block/BlockUser.php87
-rw-r--r--includes/block/BlockUserFactory.php2
-rw-r--r--includes/block/BlockUtils.php195
-rw-r--r--includes/block/BlockUtilsFactory.php35
-rw-r--r--includes/block/CompositeBlock.php2
-rw-r--r--includes/block/CrossWikiBlockTargetFactory.php60
-rw-r--r--includes/block/DatabaseBlock.php60
-rw-r--r--includes/block/DatabaseBlockStore.php221
-rw-r--r--includes/block/DatabaseBlockStoreFactory.php8
-rw-r--r--includes/block/RangeBlockTarget.php116
-rw-r--r--includes/block/UnblockUser.php44
-rw-r--r--includes/block/UnblockUserFactory.php2
-rw-r--r--includes/block/UserBlockCommandFactory.php18
-rw-r--r--includes/block/UserBlockTarget.php60
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();
+ }
+}