wikimedia/mediawiki-extensions-Wikibase

View on GitHub
client/includes/RecentChanges/ChangeLineFormatter.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Client\RecentChanges;

use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\Language\Language;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Permissions\Authority;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserNameUtils;
use Wikibase\Client\RepoLinker;
use Wikibase\DataModel\Entity\EntityId;

/**
 * Formats a changes line for including changes from the Wikibase repo in
 * the client's recent changes, watchlist and related changes special pages.
 *
 * @license GPL-2.0-or-later
 * @author Katie Filbert < aude.wiki@gmail.com >
 * @author Daniel Kinzler
 */
class ChangeLineFormatter {

    /**
     * @var RepoLinker
     */
    private $repoLinker;

    /**
     * @var UserNameUtils
     */
    private $userNameUtils;

    /**
     * @var LinkRenderer
     */
    private $linkRenderer;

    /**
     * @var CommentFormatter
     */
    private $commentFormatter;

    public function __construct(
        RepoLinker $repoLinker,
        UserNameUtils $userNameUtils,
        LinkRenderer $linkRenderer,
        CommentFormatter $commentFormatter
    ) {
        $this->repoLinker = $repoLinker;
        $this->userNameUtils = $userNameUtils;
        $this->linkRenderer = $linkRenderer;
        $this->commentFormatter = $commentFormatter;
    }

    public function format(
        ExternalChange $externalChange,
        Title $title,
        int $count,
        string $flag,
        Language $lang,
        User $user
    ): string {
        $changeType = $externalChange->getChangeType();
        $rev = $externalChange->getRev();
        $entityId = $externalChange->getEntityId();

        $formattedHTMLLine = ( $changeType === 'restore' || $changeType === 'remove' )
            ? $this->formatDeletionLogLinkHTML()
            : $this->formatDiffHistHTML( $entityId, $rev, $count, $lang );

        $formattedHTMLLine .= $this->getChangeSeparatorHTML();
        $formattedHTMLLine .= $flag . ' ';
        $link = $this->linkRenderer->makeKnownLink( $title );

        $formattedHTMLLine .= "<span class=\"mw-title\">$link</span>";

        if ( $changeType !== 'remove' ) {
            $formattedHTMLLine .= $this->formatEntityLinkHTML( $entityId );
        }

        $formattedHTMLLine .= $this->formatTimestampHTML( $rev->getTimestamp(), $lang, $user );
        $formattedHTMLLine .= implode( $this->formatUserLinksHTML( $rev->getUserName(), $rev->getVisibility(), $lang, $user ) );

        $formattedHTMLLine .= $this->getFormattedCommentHTML( $rev, $title, $externalChange->getSiteId(), $user );

        return $formattedHTMLLine;
    }

    private function formatCommonDataForEnhancedLine(
        array &$data,
        ExternalChange $externalChange,
        Title $title,
        Language $lang,
        User $user
    ): void {
        $entityId = $externalChange->getEntityId();
        $rev = $externalChange->getRev();

        $data['recentChangesFlags']['wikibase-edit'] = true;
        $data['timestampLink'] = $this->buildPermanentLinkHTML( $entityId, $rev, $lang, $user );

        $userLinks = $this->formatUserLinksHTML( $rev->getUserName(), $rev->getVisibility(), $lang, $user );
        if ( count( $userLinks ) > 1 ) {
            [ $data['userLink'], $data['userTalkLink'] ] = $userLinks;
        } else {
            $data['userLink'] = array_pop( $userLinks );
        }

        $data['comment'] = $this->getFormattedCommentHTML( $rev, $title, $externalChange->getSiteId(), $user );
    }

    public function formatDataForEnhancedLine(
        array &$data,
        ExternalChange $externalChange,
        Title $title,
        int $count,
        Language $lang,
        User $user
    ): void {
        $this->formatCommonDataForEnhancedLine( $data, $externalChange, $title, $lang, $user );

        $entityId = $externalChange->getEntityId();
        $rev = $externalChange->getRev();
        $changeType = $externalChange->getChangeType();

        $wordSeparator = wfMessage( 'word-separator' )->escaped();
        $data['currentAndLastLinks'] =
            $wordSeparator . $this->repoLinker->buildEntityLink( $entityId ) . $wordSeparator;
        $data['currentAndLastLinks'] .= ( $changeType === 'restore' || $changeType === 'remove' )
            ? $this->formatDeletionLogLinkHTML()
            : $this->formatDiffHistHTML( $entityId, $rev, $count, $lang );

        $data['separatorAfterCurrentAndLastLinks'] = $this->getChangeSeparatorHTML();

        unset( $data['characterDiff'] );
        // @fixme: this has different case than in formatDataForEnhancedBlockLine
        unset( $data['separatorAfterCharacterDiff'] );
    }

    public function formatDataForEnhancedBlockLine(
        array &$data,
        ExternalChange $externalChange,
        Title $title,
        int $count,
        Language $lang,
        User $user
    ): void {
        $this->formatCommonDataForEnhancedLine( $data, $externalChange, $title, $lang, $user );

        $entityId = $externalChange->getEntityId();
        $rev = $externalChange->getRev();
        $changeType = $externalChange->getChangeType();

        if ( $changeType === 'restore' || $changeType === 'remove' ) {
            $data['articleLink'] = ''
                . $this->formatDeletionLogLinkHTML()
                . $data['articleLink']
                . $this->formatEntityLinkHTML( $entityId );
            unset( $data['historyLink'] );
        } else {
            $data['articleLink'] .= $this->formatEntityLinkHTML( $entityId );
            $data['historyLink'] = ''
                . wfMessage( 'word-separator' )->escaped()
                . $this->formatDiffHistHTML( $entityId, $rev, $count, $lang );
        }

        $data['separatorAfterLinks'] = $this->getChangeSeparatorHTML();

        unset( $data['characterDiff'] );
        // @fixme: this has different case than in formatDataForEnhancedLine
        unset( $data['separatorAftercharacterDiff'] );
    }

    private function getFormattedCommentHTML( RevisionData $rev, Title $title, string $siteId, Authority $user ): string {
        if ( !RevisionRecord::userCanBitfield(
            $rev->getVisibility(),
            RevisionRecord::DELETED_COMMENT,
            $user )
        ) {
            return ' <span class="history-deleted comment">' .
                wfMessage( 'rev-deleted-comment' )->escaped() . '</span>';
        }

        $commentHtml = $rev->getCommentHtml();
        if ( $commentHtml === null || $commentHtml === '' ) {
            $commentHtml = $this->commentFormatter->format( $rev->getComment(), $title, false, $siteId );
        }
        return $this->wrapCommentBlockHTML( $commentHtml );
    }

    private function wrapCommentBlockHTML( string $commentHtml ): string {
        //NOTE: keep in sync with MediaWiki\CommentFormatter\CommentFormatter::formatBlock
        $formatted = wfMessage( 'parentheses' )->rawParams( $commentHtml )->escaped();
        return " <span class=\"comment\">$formatted</span>";
    }

    private function getChangeSeparatorHTML(): string {
        return ' <span class="mw-changeslist-separator">. .</span> ';
    }

    private function formatTimestampHTML( string $timestamp, Language $lang, UserIdentity $user ): string {
        return wfMessage( 'semicolon-separator' )->text()
            . '<span class="mw-changeslist-date">'
            . $lang->userTime( $timestamp, $user )
            . '</span> <span class="mw-changeslist-separator">. .</span> ';
    }

    /**
     * @param string $userName
     * @param int $visibility
     * @param Language $lang
     * @param Authority $user
     * @return string[] array of HTML
     */
    private function formatUserLinksHTML( string $userName, int $visibility, Language $lang, Authority $user ): array {
        if ( !RevisionRecord::userCanBitfield(
            $visibility,
            RevisionRecord::DELETED_USER,
            $user )
        ) {
            return [
                ' <span class="history-deleted">' .
                wfMessage( 'rev-deleted-user' )->escaped() . '</span>',
            ];
        }

        $links = $this->buildUserLinksHTML( $userName );

        if ( $this->userNameUtils->isIP( $userName ) ) {
            return $this->formatIpUserLinksHTML( $links );
        } else {
            return $this->formatRegisteredUserLinksHTML( $links, $lang );
        }
    }

    /**
     * @fixme use shared bits of formatting from ChangesList and OldChangesList
     *
     * @param string[] $links
     *
     * @return string[] array of HTML
     */
    private function formatIpUserLinksHTML( array $links ): array {
        $ret = [];

        $ret[] = $links['contribs'];
        $ret[] = wfMessage( 'word-separator' )->plain()
            . wfMessage( 'parentheses' )->rawParams(
                $links['usertalk']
            )->text();

        return $ret;
    }

    /**
     * @fixme use shared bits of formatting from ChangesList and OldChangesList
     * @param string[] $links
     * @param Language $lang
     * @return string[] array of HTML
     */
    private function formatRegisteredUserLinksHTML( array $links, Language $lang ): array {
        $ret = [];

        $ret[] = $links['user'];

        $usertools = [
            $links['usertalk'],
            $links['contribs'],
        ];

        $ret[] = wfMessage( 'word-separator' )->plain()
            . '<span class="mw-usertoollinks">'
            . wfMessage( 'parentheses' )->rawParams(
                $lang->pipeList( $usertools )
            )->text()
            . '</span>';

        return $ret;
    }

    private function formatEntityLinkHTML( EntityId $entityId ): string {
        $entityLink = $this->repoLinker->buildEntityLink( $entityId );

        return wfMessage( 'word-separator' )->plain()
            . wfMessage( 'parentheses' )->rawParams( $entityLink )->text();
    }

    private function formatDeletionLogLinkHTML(): string {
        $logLink = $this->repoLinker->formatLink(
            $this->repoLinker->getPageUrl( 'Special:Log/delete' ),
            wfMessage( 'dellogpage' )->text()
        );

        return wfMessage( 'parentheses' )->rawParams( $logLink )->text();
    }

    private function formatDiffHistHTML( EntityId $entityId, RevisionData $rev, int $count, Language $lang ): string {
        $diffLink = $this->buildDiffLinkHTML( $entityId, $rev, $count );
        $historyLink = $this->buildHistoryLinkHTML( $entityId, $rev );

        return wfMessage( 'parentheses' )->rawParams(
            $lang->pipeList( [ $diffLink, $historyLink ] )
        )->text();
    }

    private function buildPermanentLinkHTML( EntityId $entityId, RevisionData $rev, Language $lang, UserIdentity $user ): string {
        $params = [
            'title' => $this->repoLinker->getEntityTitle( $entityId ),
            'curid' => $rev->getPageId(),
            'oldid' => $rev->getRevId(),
        ];

        $url = $this->repoLinker->addQueryParams( $this->repoLinker->getIndexUrl(), $params );

        return $this->repoLinker->formatLink(
            $url,
            $lang->userTime( $rev->getTimestamp(), $user )
        );
    }

    private function buildDiffLinkHTML( EntityId $entityId, RevisionData $rev, $count ): string {
        $params = [
            'title' => $this->repoLinker->getEntityTitle( $entityId ),
            'curid' => $rev->getPageId(),
            'diff' => $rev->getRevId(),
            'oldid' => $rev->getParentId(),
        ];

        $url = $this->repoLinker->addQueryParams( $this->repoLinker->getIndexUrl(), $params );

        return $this->repoLinker->formatLink(
            $url,
            wfMessage( 'diff' )->text(),
            [
                'tabindex' => $count,
            ]
        );
    }

    private function buildHistoryLinkHTML( EntityId $entityId, RevisionData $rev ): string {
        $titleText = $this->repoLinker->getEntityTitle( $entityId );

        $params = [
            'title' => $titleText,
            'curid' => $rev->getPageId(),
            'action' => 'history',
        ];

        $url = $this->repoLinker->addQueryParams( $this->repoLinker->getIndexUrl(), $params );

        return $this->repoLinker->formatLink(
            $url,
            wfMessage( 'hist' )->text()
        );
    }

    private function buildUserLinkHTML( string $userName ): string {
        return $this->repoLinker->formatLink(
            // @todo: localise this once namespaces are localised on the repo
            $this->repoLinker->getPageUrl( "User:$userName" ),
            $userName,
            [
                'class' => 'mw-userlink',
            ]
        );
    }

    private function buildUserContribsLinkHTML( string $userName, ?string $text = null ): string {
        // @todo: know how the repo is localised. it's english now
        // for namespaces and special pages
        $link = $this->repoLinker->getPageUrl( "Special:Contributions/$userName" );

        if ( $text === null ) {
            $text = wfMessage( 'contribslink' )->text();
        }

        return $this->repoLinker->formatLink( $link, $text );
    }

    private function buildUserTalkLinkHTML( string $userName ): string {
        // @todo: localize this once we can localize namespaces on the repo
        $link = $this->repoLinker->getPageUrl( "User_talk:$userName" );
        $text = wfMessage( 'talkpagelinktext' )->text();

        return $this->repoLinker->formatLink( $link, $text );
    }

    /**
     * @param string $userName
     *
     * @return string[] List of HTML links
     */
    private function buildUserLinksHTML( string $userName ): array {
        $links = [];

        $links['usertalk'] = $this->buildUserTalkLinkHTML( $userName );

        if ( $this->userNameUtils->isIP( $userName ) ) {
            $links['contribs'] = $this->buildUserContribsLinkHTML( $userName, $userName );
        } else {
            $links['user'] = $this->buildUserLinkHTML( $userName );
            $links['contribs'] = $this->buildUserContribsLinkHTML( $userName );
        }

        return $links;
    }

}