wikimedia/mediawiki-core

View on GitHub
includes/block/AbstractBlock.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */

namespace MediaWiki\Block;

use InvalidArgumentException;
use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\DAO\WikiAwareEntityTrait;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Message\Message;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;

/**
 * @note Extensions should not subclass this, as MediaWiki currently does not
 *   support custom block types.
 * @since 1.34 Factored out from DatabaseBlock (previously Block).
 */
abstract class AbstractBlock implements Block {
    use WikiAwareEntityTrait;

    /** @var CommentStoreComment */
    protected $reason;

    /** @var string */
    protected $timestamp = '';

    /** @var string */
    protected $expiry = '';

    /** @var bool */
    protected $blockEmail = false;

    /** @var bool */
    protected $allowUsertalk = false;

    /** @var bool */
    protected $blockCreateAccount = false;

    /** @var bool */
    protected $hideName = false;

    /** @var bool */
    protected $isHardblock;

    /** @var UserIdentity|string|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;

    /** @var string|false */
    protected $wikiId;

    /**
     * Create a new block with specified parameters on a user, IP or IP range.
     *
     * @param array $options Parameters of the block, with supported options:
     *  - address: (string|UserIdentity) Target user name, user identity object,
     *    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
     *  - timestamp: (string) The time at which the block comes into effect,
     *    in any format supported by wfTimestamp()
     *  - decodedTimestamp: (string) The timestamp in MW 14-character format
     *  - hideName: (bool) Hide the target user name
     *  - anonOnly: (bool) Used if the target is an IP address. The block only
     *    applies to anon and temporary users using this IP address, and not to
     *    logged-in users.
     */
    public function __construct( array $options = [] ) {
        $defaults = [
            'address'         => '',
            'wiki'            => self::LOCAL,
            'reason'          => '',
            'timestamp'       => '',
            'hideName'        => false,
            'anonOnly'        => false,
        ];

        $options += $defaults;

        $this->wikiId = $options['wiki'];
        $this->setTarget( $options['address'] );
        $this->setReason( $options['reason'] );
        if ( isset( $options['decodedTimestamp'] ) ) {
            $this->setTimestamp( $options['decodedTimestamp'] );
        } else {
            $this->setTimestamp( wfTimestamp( TS_MW, $options['timestamp'] ) );
        }
        $this->setHideName( (bool)$options['hideName'] );
        $this->isHardblock( !$options['anonOnly'] );
    }

    /**
     * Get the user id of the blocking sysop
     *
     * @param string|false $wikiId (since 1.38)
     * @return int (0 for foreign users)
     */
    abstract public function getBy( $wikiId = self::LOCAL ): int;

    /**
     * Get the username of the blocking sysop
     *
     * @return string
     */
    abstract public function getByName();

    /**
     * @inheritDoc
     */
    public function getId( $wikiId = self::LOCAL ): ?int {
        $this->assertWiki( $wikiId );
        return null;
    }

    /**
     * Get the reason for creating the block.
     *
     * @since 1.35
     * @return CommentStoreComment
     */
    public function getReasonComment(): CommentStoreComment {
        return $this->reason;
    }

    /**
     * Set the reason for creating the block.
     *
     * @since 1.33
     * @param string|Message|CommentStoreComment $reason
     */
    public function setReason( $reason ) {
        $this->reason = CommentStoreComment::newUnsavedComment( $reason );
    }

    /**
     * Get whether the block hides the target's username
     *
     * @since 1.33
     * @return bool The block hides the username
     */
    public function getHideName() {
        return $this->hideName;
    }

    /**
     * Set whether the block hides the target's username
     *
     * @since 1.33
     * @param bool $hideName The block hides the username
     */
    public function setHideName( $hideName ) {
        $this->hideName = $hideName;
    }

    /**
     * Indicates that the block is a sitewide block. This means the user is
     * prohibited from editing any page on the site (other than their own talk
     * page).
     *
     * @since 1.33
     * @param null|bool $x
     * @return bool
     */
    public function isSitewide( $x = null ): bool {
        return wfSetVar( $this->isSitewide, $x );
    }

    /**
     * Get or set the flag indicating whether this block blocks the target from
     * creating an account. (Note that the flag may be overridden depending on
     * global configs.)
     *
     * @since 1.33
     * @param null|bool $x Value to set (if null, just get the property value)
     * @return bool Value of the property
     */
    public function isCreateAccountBlocked( $x = null ): bool {
        return wfSetVar( $this->blockCreateAccount, $x );
    }

    /**
     * Get or set the flag indicating whether this block blocks the target from
     * sending emails. (Note that the flag may be overridden depending on
     * global configs.)
     *
     * @since 1.33
     * @param null|bool $x Value to set (if null, just get the property value)
     * @return bool Value of the property
     */
    public function isEmailBlocked( $x = null ) {
        return wfSetVar( $this->blockEmail, $x );
    }

    /**
     * Get or set the flag indicating whether this block blocks the target from
     * editing their own user talk page. (Note that the flag may be overridden
     * depending on global configs.)
     *
     * @since 1.33
     * @param null|bool $x Value to set (if null, just get the property value)
     * @return bool Value of the property
     */
    public function isUsertalkEditAllowed( $x = null ) {
        return wfSetVar( $this->allowUsertalk, $x );
    }

    /**
     * Get/set whether the block is a hard block (affects logged-in users on a
     * given IP/range).
     *
     * Note that temporary users are not considered logged-in here - they are
     * always blocked by IP-address blocks.
     *
     * Note that user blocks are always hard blocks, since the target is logged
     * in by definition.
     *
     * @since 1.36 Moved up from DatabaseBlock
     * @param bool|null $x
     * @return bool
     */
    public function isHardblock( $x = null ): bool {
        wfSetVar( $this->isHardblock, $x );

        return $this->getType() == self::TYPE_USER
            ? true
            : $this->isHardblock;
    }

    /**
     * Determine whether the block prevents a given right. A right may be
     * allowed or disallowed by default, or determined from a property on the
     * block object. For certain rights, the property may be overridden
     * according to global configs.
     *
     * @since 1.33
     * @param string $right
     * @return bool|null The block applies to the right, or null if
     *  unsure (e.g. unrecognized right or unset property)
     */
    public function appliesToRight( $right ) {
        $blockDisablesLogin = MediaWikiServices::getInstance()->getMainConfig()
            ->get( MainConfigNames::BlockDisablesLogin );

        $res = null;
        switch ( $right ) {
            case 'autocreateaccount':
            case 'createaccount':
                $res = $this->isCreateAccountBlocked();
                break;
            case 'sendemail':
                $res = $this->isEmailBlocked();
                break;
            case 'upload':
                // Sitewide blocks always block upload. This may be overridden in a subclass.
                $res = $this->isSitewide();
                break;
            case 'read':
                $res = false;
                break;
        }
        if ( !$res && $blockDisablesLogin ) {
            // If a block would disable login, then it should
            // prevent any right that all users cannot do
            $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
            $anon = MediaWikiServices::getInstance()->getUserFactory()->newAnonymous();
            $res = $permissionManager->userHasRight( $anon, $right ) ? $res : true;
        }

        return $res;
    }

    /**
     * Get the type of target for this particular block.
     * @return int|null AbstractBlock::TYPE_ constant, will never be TYPE_ID
     */
    public function getType(): ?int {
        return $this->type;
    }

    /**
     * @since 1.37
     * @return ?UserIdentity
     */
    public function getTargetUserIdentity(): ?UserIdentity {
        return $this->target instanceof UserIdentity ? $this->target : null;
    }

    /**
     * @since 1.37
     * @return string
     */
    public function getTargetName(): string {
        return $this->target instanceof UserIdentity
            ? $this->target->getName()
            : (string)$this->target;
    }

    /**
     * @param UserIdentity|string $target
     *
     * @return bool
     * @since 1.37
     */
    public function isBlocking( $target ): bool {
        $targetName = $target instanceof UserIdentity
            ? $target->getName()
            : (string)$target;

        return $targetName === $this->getTargetName();
    }

    /**
     * Get the block expiry time
     *
     * @since 1.19
     * @return string
     */
    public function getExpiry(): string {
        return $this->expiry;
    }

    /**
     * Set the block expiry time
     *
     * @since 1.33
     * @param string $expiry
     */
    public function setExpiry( $expiry ) {
        // Force string so getExpiry() return typehint doesn't break things
        $this->expiry = (string)$expiry;
    }

    /**
     * Get the timestamp indicating when the block was created
     *
     * @since 1.33
     * @return string
     */
    public function getTimestamp(): string {
        return $this->timestamp;
    }

    /**
     * Set the timestamp indicating when the block was created
     *
     * @since 1.33
     * @param string $timestamp
     */
    public function setTimestamp( $timestamp ) {
        // Force string so getTimestamp() return typehint doesn't break things
        $this->timestamp = (string)$timestamp;
    }

    /**
     * Set the target for this block, and update $this->type accordingly
     * @param string|UserIdentity|null $target
     */
    public function setTarget( $target ) {
        // Small optimization to make this code testable, this is what would happen anyway
        if ( $target === '' ) {
            $this->target = null;
            $this->type = null;
        } else {
            [ $parsedTarget, $this->type ] = MediaWikiServices::getInstance()
                ->getBlockUtilsFactory()
                ->getBlockUtils( $this->wikiId )
                ->parseBlockTarget( $target );
            if ( $parsedTarget !== null ) {
                $this->assertWiki( is_string( $parsedTarget ) ? self::LOCAL : $parsedTarget->getWikiId() );
            }
            $this->target = $parsedTarget;
        }
    }

    /**
     * @since 1.38
     * @return string|false
     */
    public function getWikiId() {
        return $this->wikiId;
    }

    /**
     * Determine whether the block allows the user to edit their own
     * user talk page. This is done separately from
     * AbstractBlock::appliesToRight because there is no right for
     * editing one's own user talk page and because the user's talk
     * page needs to be passed into the block object, which is unaware
     * of the user.
     *
     * The bl_allow_usertalk flag (which corresponds to the property
     * allowUsertalk) is used on sitewide blocks and partial blocks
     * that contain a namespace restriction on the user talk namespace,
     * but do not contain a page restriction on the user's talk page.
     * For all other (i.e. most) partial blocks, the flag is ignored,
     * and the user can always edit their user talk page unless there
     * is a page restriction on their user talk page, in which case
     * they can never edit it. (Ideally the flag would be stored as
     * null in these cases, but the database field isn't nullable.)
     *
     * This method does not validate that the passed in talk page belongs to the
     * block target since the target (an IP) might not be the same as the user's
     * talk page (if they are logged in).
     *
     * @since 1.33
     * @param Title|null $usertalk The user's user talk page. If null,
     *  and if the target is a User, the target's userpage is used
     * @return bool The user can edit their talk page
     */
    public function appliesToUsertalk( Title $usertalk = null ) {
        if ( !$usertalk ) {
            if ( $this->target instanceof UserIdentity ) {
                $usertalk = Title::makeTitle(
                    NS_USER_TALK,
                    $this->target->getName()
                );
            } else {
                throw new InvalidArgumentException(
                    '$usertalk must be provided if block target is not a user/IP'
                );
            }
        }

        if ( $usertalk->getNamespace() !== NS_USER_TALK ) {
            throw new InvalidArgumentException(
                '$usertalk must be a user talk page'
            );
        }

        if ( !$this->isSitewide() ) {
            if ( $this->appliesToPage( $usertalk->getArticleID() ) ) {
                return true;
            }
            if ( !$this->appliesToNamespace( NS_USER_TALK ) ) {
                return false;
            }
        }

        // This is a type of block which uses the bl_allow_usertalk
        // flag. The flag can still be overridden by global configs.
        if ( !MediaWikiServices::getInstance()->getMainConfig()
            ->get( MainConfigNames::BlockAllowsUTEdit )
        ) {
            return true;
        }
        return !$this->isUsertalkEditAllowed();
    }

    /**
     * Checks if a block applies to a particular title
     *
     * This check does not consider whether `$this->isUsertalkEditAllowed`
     * returns false, as the identity of the user making the hypothetical edit
     * isn't known here (particularly in the case of IP hard blocks, range
     * blocks, and auto-blocks).
     *
     * @param Title $title
     * @return bool
     */
    public function appliesToTitle( Title $title ) {
        return $this->isSitewide();
    }

    /**
     * Checks if a block applies to a particular namespace
     *
     * @since 1.33
     *
     * @param int $ns
     * @return bool
     */
    public function appliesToNamespace( $ns ) {
        return $this->isSitewide();
    }

    /**
     * Checks if a block applies to a particular page
     *
     * This check does not consider whether `$this->isUsertalkEditAllowed`
     * returns false, as the identity of the user making the hypothetical edit
     * isn't known here (particularly in the case of IP hard blocks, range
     * blocks, and auto-blocks).
     *
     * @since 1.33
     *
     * @param int $pageId
     * @return bool
     */
    public function appliesToPage( $pageId ) {
        return $this->isSitewide();
    }

    /**
     * Check if the block prevents a user from resetting their password
     *
     * @since 1.33
     * @return bool The block blocks password reset
     */
    public function appliesToPasswordReset() {
        return $this->isCreateAccountBlocked();
    }

    /**
     * @return AbstractBlock[]
     */
    public function toArray(): array {
        return [ $this ];
    }

}