wikimedia/mediawiki-core

View on GitHub
includes/block/BlockErrorFormatter.php

Summary

Maintainability
A
2 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 Language;
use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Language\LocalizationContext;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Message\Message;
use MediaWiki\Page\PageReferenceValue;
use MediaWiki\Title\TitleFormatter;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityUtils;

/**
 * A service class for getting formatted information about a block.
 * To obtain an instance, use MediaWikiServices::getInstance()->getBlockErrorFormatter().
 *
 * @since 1.35
 */
class BlockErrorFormatter {

    private TitleFormatter $titleFormatter;
    private HookRunner $hookRunner;
    private UserIdentityUtils $userIdentityUtils;
    private LocalizationContext $uiContext;
    private LanguageFactory $languageFactory;

    public function __construct(
        TitleFormatter $titleFormatter,
        HookContainer $hookContainer,
        UserIdentityUtils $userIdentityUtils,
        LanguageFactory $languageFactory,
        LocalizationContext $uiContext
    ) {
        $this->titleFormatter = $titleFormatter;
        $this->hookRunner = new HookRunner( $hookContainer );
        $this->userIdentityUtils = $userIdentityUtils;

        $this->languageFactory = $languageFactory;
        $this->uiContext = $uiContext;
    }

    /**
     * @return Language
     */
    private function getLanguage(): Language {
        return $this->languageFactory->getLanguage( $this->uiContext->getLanguageCode() );
    }

    /**
     * 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.
     *
     * If passed a CompositeBlock, will get a generic message stating that there are
     * multiple blocks. To get all the block messages, use getMessages instead.
     *
     * @param Block $block
     * @param UserIdentity $user
     * @param mixed $language Unused since 1.42
     * @param string $ip
     * @return Message
     */
    public function getMessage(
        Block $block,
        UserIdentity $user,
        $language,
        string $ip
    ): Message {
        $key = $this->getBlockErrorMessageKey( $block, $user );
        $params = $this->getBlockErrorMessageParams( $block, $user, $ip );
        return $this->uiContext->msg( $key, $params );
    }

    /**
     * Get block error messages for all of the blocks that apply to a user.
     *
     * @since 1.42
     * @param Block $block
     * @param UserIdentity $user
     * @param string $ip
     * @return Message[]
     */
    public function getMessages(
        Block $block,
        UserIdentity $user,
        string $ip
    ): array {
        $messages = [];
        foreach ( $block->toArray() as $singleBlock ) {
            $messages[] = $this->getMessage( $singleBlock, $user, null, $ip );
        }

        return $messages;
    }

    /**
     * Get a standard set of block details for building a block error message.
     *
     * @param Block $block
     * @return mixed[]
     *  - identifier: Information for looking up the block
     *  - targetName: The target, as a string
     *  - blockerName: The blocker, as a string
     *  - reason: Reason for the block
     *  - expiry: Expiry time
     *  - timestamp: Time the block was created
     */
    private function getBlockErrorInfo( Block $block ) {
        $blocker = $block->getBlocker();
        return [
            'identifier' => $block->getIdentifier(),
            'targetName' => $block->getTargetName(),
            'blockerName' => $blocker ? $blocker->getName() : '',
            'reason' => $block->getReasonComment(),
            'expiry' => $block->getExpiry(),
            'timestamp' => $block->getTimestamp(),
        ];
    }

    /**
     * Get a standard set of block details for building a block error message,
     * formatted for a specified user and language.
     *
     * @since 1.35
     * @param Block $block
     * @param UserIdentity $user
     * @return mixed[] See getBlockErrorInfo
     */
    private function getFormattedBlockErrorInfo(
        Block $block,
        UserIdentity $user
    ) {
        $info = $this->getBlockErrorInfo( $block );

        $language = $this->getLanguage();

        $info['expiry'] = $language->formatExpiry( $info['expiry'], true, 'infinity', $user );
        $info['timestamp'] = $language->userTimeAndDate( $info['timestamp'], $user );
        $info['blockerName'] = $language->embedBidi( $info['blockerName'] );
        $info['targetName'] = $language->embedBidi( $info['targetName'] );

        $info['reason'] = $this->formatBlockReason( $info['reason'], $language );

        return $info;
    }

    /**
     * Format the block reason as plain wikitext in the specified language.
     *
     * @param CommentStoreComment $reason
     * @param Language $language
     * @return string
     */
    private function formatBlockReason( CommentStoreComment $reason, Language $language ) {
        if ( $reason->text === '' ) {
            $message = new Message( 'blockednoreason', [], $language );
            return $message->plain();
        }
        return $reason->message->inLanguage( $language )->plain();
    }

    /**
     * Create a link to the blocker's user page. This must be done here rather than in
     * the message translation, because the blocker may not be a local user, in which
     * case their page cannot be linked.
     *
     * @param ?UserIdentity $blocker
     * @return string Link to the blocker's page; blocker's name if not a local user
     */
    private function formatBlockerLink( ?UserIdentity $blocker ) {
        if ( !$blocker ) {
            // TODO should we say something? This is just matching the code before
            // the refactoring in late July 2021
            return '';
        }

        $language = $this->getLanguage();

        if ( $blocker->getId() === 0 ) {
            // Foreign user
            // TODO what about blocks placed by IPs? Shouldn't we check based on
            // $blocker's wiki instead? This is just matching the code before the
            // refactoring in late July 2021.
            return $language->embedBidi( $blocker->getName() );
        }

        $blockerUserpage = PageReferenceValue::localReference( NS_USER, $blocker->getName() );
        $blockerText = $language->embedBidi(
            $this->titleFormatter->getText( $blockerUserpage )
        );
        $prefixedText = $this->titleFormatter->getPrefixedText( $blockerUserpage );
        return "[[{$prefixedText}|{$blockerText}]]";
    }

    /**
     * Determine the block error message key by examining the block.
     *
     * @param Block $block
     * @param UserIdentity $user
     * @return string Message key
     */
    private function getBlockErrorMessageKey( Block $block, UserIdentity $user ) {
        $isTempUser = $this->userIdentityUtils->isTemp( $user );
        $key = $isTempUser ? 'blockedtext-tempuser' : 'blockedtext';
        if ( $block instanceof DatabaseBlock ) {
            if ( $block->getType() === Block::TYPE_AUTO ) {
                $key = $isTempUser ? 'autoblockedtext-tempuser' : 'autoblockedtext';
            } elseif ( !$block->isSitewide() ) {
                $key = 'blockedtext-partial';
            }
        } elseif ( $block instanceof SystemBlock ) {
            $key = 'systemblockedtext';
        } elseif ( $block instanceof CompositeBlock ) {
            $key = 'blockedtext-composite';
        }

        // Allow extensions to modify the block error message
        $this->hookRunner->onGetBlockErrorMessageKey( $block, $key );

        return $key;
    }

    /**
     * Get the formatted parameters needed to build the block error messages handled by
     * getBlockErrorMessageKey.
     *
     * @param Block $block
     * @param UserIdentity $user
     * @param string $ip
     * @return mixed[] Params used by standard block error messages, in order:
     *  - blockerLink: Link to the blocker's user page, if any; otherwise same as blockerName
     *  - reason: Reason for the block
     *  - ip: IP address of the user attempting to perform an action
     *  - blockerName: The blocker, as a bidi-embedded string
     *  - identifier: Information for looking up the block
     *  - expiry: Expiry time, in the specified language
     *  - targetName: The target, as a bidi-embedded string
     *  - timestamp: Time the block was created, in the specified language
     */
    private function getBlockErrorMessageParams(
        Block $block,
        UserIdentity $user,
        string $ip
    ) {
        $info = $this->getFormattedBlockErrorInfo( $block, $user );

        // Add params that are specific to the standard block errors
        $info['ip'] = $ip;
        $info['blockerLink'] = $this->formatBlockerLink( $block->getBlocker() );

        // Display the CompositeBlock identifier as a message containing relevant block IDs
        if ( $block instanceof CompositeBlock ) {
            $ids = $this->getLanguage()->commaList( array_map(
                static function ( $id ) {
                    return '#' . $id;
                },
                array_filter( $info['identifier'], 'is_int' )
            ) );
            if ( $ids === '' ) {
                $idsMsg = $this->uiContext->msg( 'blockedtext-composite-no-ids', [] );
            } else {
                $idsMsg = $this->uiContext->msg( 'blockedtext-composite-ids', [ $ids ] );
            }
            $info['identifier'] = $idsMsg->plain();
        }

        // Messages expect the params in this order
        $order = [
            'blockerLink',
            'reason',
            'ip',
            'blockerName',
            'identifier',
            'expiry',
            'targetName',
            'timestamp',
        ];

        $params = [];
        foreach ( $order as $item ) {
            $params[] = $info[$item];
        }

        return $params;
    }

}