<?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\Context\IContextSource;
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;
	}

	/**
	 * Get the key and parameters for the corresponding error message.
	 *
	 * @deprecated since 1.35 Use BlockErrorFormatter::getMessage instead, and
	 *  build the array using Message::getKey and Message::getParams. Hard
	 *  deprecated since 1.40.
	 * @since 1.22
	 * @param IContextSource $context
	 * @return array A message array: either a list of strings, the first of which
	 *  is the message key and the remaining ones the parameters, or an array with
	 *  a single MessageSpecifier object.
	 * @phan-return non-empty-array
	 */
	public function getPermissionsError( IContextSource $context ) {
		wfDeprecated( __METHOD__, '1.35' );
		$message = MediaWikiServices::getInstance()
			->getFormatterFactory()->getBlockErrorFormatter( $context )
			->getMessage(
				$this,
				$context->getUser(),
				null,
				$context->getRequest()->getIP()
			);
		return [ $message->getKey(), ...$message->getParams() ];
	}

	/**
	 * 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 ];
	}

}
