wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Diff/SiteLinkDiffView.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Repo\Diff;

use Diff\DiffOp\AtomicDiffOp;
use Diff\DiffOp\Diff\Diff;
use Diff\DiffOp\DiffOp;
use Diff\DiffOp\DiffOpAdd;
use Diff\DiffOp\DiffOpChange;
use Diff\DiffOp\DiffOpRemove;
use InvalidArgumentException;
use MediaWiki\Html\Html;
use MediaWiki\Language\LanguageCode;
use MediaWiki\Site\Site;
use MediaWiki\Site\SiteLookup;
use MessageLocalizer;
use UnexpectedValueException;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
use Wikimedia\Diff\WordLevelDiff;

/**
 * Class for generating views of DiffOp objects
 * representing diffs of an Item’s site links (including badges).
 *
 * Diffing of other Item data is done by {@link BasicDiffView}.
 *
 * @license GPL-2.0-or-later
 */
class SiteLinkDiffView implements DiffView {

    /**
     * @var string[]
     */
    private $path;

    /**
     * @var Diff
     */
    private $diff;

    /**
     * @var SiteLookup
     */
    private $siteLookup;

    /**
     * @var EntityIdFormatter
     */
    private $entityIdFormatter;

    /**
     * @var MessageLocalizer
     */
    private $messageLocalizer;

    /**
     * @param string[] $path
     * @param Diff $diff
     * @param SiteLookup $siteLookup
     * @param EntityIdFormatter $entityIdFormatter that must return only HTML! otherwise injections might be possible
     * @param MessageLocalizer $messageLocalizer
     */
    public function __construct(
        array $path,
        Diff $diff,
        SiteLookup $siteLookup,
        EntityIdFormatter $entityIdFormatter,
        MessageLocalizer $messageLocalizer
    ) {
        $this->path = $path;
        $this->diff = $diff;
        $this->siteLookup = $siteLookup;
        $this->entityIdFormatter = $entityIdFormatter;
        $this->messageLocalizer = $messageLocalizer;
    }

    /**
     * Builds and returns the HTML to represent the Diff.
     */
    public function getHtml(): string {
        return $this->generateOpHtml( $this->path, $this->diff );
    }

    /**
     * Does the actual work.
     *
     * @param string[] $path
     * @param DiffOp $op
     *
     * @return string
     */
    protected function generateOpHtml( array $path, DiffOp $op ): string {
        if ( $op instanceof AtomicDiffOp ) {
            $localizedPath = $path;

            $translatedLinkSubPath = $this->messageLocalizer->msg(
                'wikibase-diffview-link-' . $path[2]
            );

            if ( !$translatedLinkSubPath->isDisabled() ) {
                $localizedPath[2] = $translatedLinkSubPath->text();
            }

            $html = $this->generateDiffHeaderHtml( implode( ' / ', $localizedPath ) );

            $html .= $this->generateDiffOpHtml( $path, $op );
        } else {
            $html = '';
            // @phan-suppress-next-line PhanTypeNoPropertiesForeach
            foreach ( $op as $key => $subOp ) {
                $html .= $this->generateOpHtml(
                    array_merge( $path, [ $key ] ),
                    $subOp
                );
            }
        }

        return $html;
    }

    private function generateDiffOpHtml( array $path, AtomicDiffOp $op ): string {
        if ( $path[2] === 'badges' ) {
            return $this->generateBadgeDiffOpHtml( $op );
        } else {
            return $this->generateLinkDiffOpHtml( $path[1], $op );
        }
    }

    private function generateBadgeDiffOpHtml( AtomicDiffOp $op ): string {
        $oldHtml = null;
        $newHtml = null;

        if ( $op instanceof DiffOpAdd ) {
            $newHtml = $this->getAddedLine( $this->getBadgeLinkElement( $op->getNewValue() ) );
        } elseif ( $op instanceof DiffOpRemove ) {
            $oldHtml = $this->getDeletedLine( $this->getBadgeLinkElement( $op->getOldValue() ) );
        } elseif ( $op instanceof DiffOpChange ) {
            $oldHtml = $this->getDeletedLine( $this->getBadgeLinkElement( $op->getOldValue() ) );
            $newHtml = $this->getAddedLine( $this->getBadgeLinkElement( $op->getNewValue() ) );
        } else {
            throw new UnexpectedValueException( 'Unknown DiffOp type' );
        }

        return $this->generateHtmlDiffTableRow( $oldHtml, $newHtml );
    }

    private function generateLinkDiffOpHtml( string $siteId, AtomicDiffOp $op ): string {
        $oldHtml = null;
        $newHtml = null;

        if ( $op instanceof DiffOpAdd ) {
            $newHtml = $this->getAddedLine( $this->getSiteLinkElement( $siteId, $op->getNewValue() ) );
        } elseif ( $op instanceof DiffOpRemove ) {
            $oldHtml = $this->getDeletedLine( $this->getSiteLinkElement( $siteId, $op->getOldValue() ) );
        } elseif ( $op instanceof DiffOpChange ) {
            $wordLevelDiff = new WordLevelDiff(
                [ $op->getOldValue() ],
                [ $op->getNewValue() ]
            );
            $oldHtml = $this->getSiteLinkElement( $siteId, $op->getOldValue(), $wordLevelDiff->orig()[0] );
            $newHtml = $this->getSiteLinkElement( $siteId, $op->getNewValue(), $wordLevelDiff->closing()[0] );
        } else {
            throw new UnexpectedValueException( 'Unknown DiffOp type' );
        }

        return $this->generateHtmlDiffTableRow( $oldHtml, $newHtml );
    }

    /**
     * Generates an HTML table row for a change diffOp
     * given HTML snippets representing old and new
     * sides of the Diff
     */
    protected function generateHtmlDiffTableRow( ?string $oldHtml, ?string $newHtml ): string {
        $html = Html::openElement( 'tr' );
        if ( $oldHtml !== null ) {
            $html .= Html::element( 'td', [ 'class' => 'diff-marker', 'data-marker' => '−' ] );
            $html .= Html::rawElement( 'td', [ 'class' => 'diff-deletedline' ],
                Html::rawElement( 'div', [], $oldHtml ) );
        }
        if ( $newHtml !== null ) {
            if ( $oldHtml === null ) {
                $html .= Html::element( 'td', [ 'colspan' => '2' ], "\u{00A0}" );
            }
            $html .= Html::rawElement( 'td', [ 'class' => 'diff-marker', 'data-marker' => '+' ] );
            $html .= Html::rawElement( 'td', [ 'class' => 'diff-addedline' ],
                Html::rawElement( 'div', [], $newHtml ) );
        }
        $html .= Html::closeElement( 'tr' );

        return $html;
    }

    private function getDeletedLine( string $html ): string {
        return $this->getChangedLine( 'del', $html );
    }

    private function getAddedLine( string $html ): string {
        return $this->getChangedLine( 'ins', $html );
    }

    private function getChangedLine( string $tag, string $html ): string {
        return Html::rawElement( $tag, [ 'class' => 'diffchange diffchange-inline' ], $html );
    }

    /**
     * @param string $siteId
     * @param string $pageName
     * @param string|null $html Defaults to $pageName (HTML-escaped)
     *
     * @return string
     */
    private function getSiteLinkElement( string $siteId, string $pageName, string $html = null ): string {
        $site = $this->siteLookup->getSite( $siteId );

        $tagName = 'span';
        $attrs = [
            'dir' => 'auto',
        ];
        if ( $html === null ) {
            $html = htmlspecialchars( $pageName );
        }

        if ( $site instanceof Site ) {
            // Otherwise it may have been deleted from the sites table
            $tagName = 'a';
            $attrs['href'] = $site->getPageUrl( $pageName );
            $attrs['hreflang'] = LanguageCode::bcp47( $site->getLanguageCode() );
        }

        return Html::rawElement( $tagName, $attrs, $html );
    }

    /**
     * @param string $idString
     *
     * @return string HTML
     */
    private function getBadgeLinkElement( string $idString ): string {
        try {
            $itemId = new ItemId( $idString );
        } catch ( InvalidArgumentException $ex ) {
            return htmlspecialchars( $idString );
        }

        return $this->entityIdFormatter->formatEntityId( $itemId );
    }

    /**
     * Generates HTML for the header of the diff operation
     */
    protected function generateDiffHeaderHtml( string $name ): string {
        $html = Html::openElement( 'tr' );
        $html .= Html::element( 'td', [ 'colspan' => '2', 'class' => 'diff-lineno' ], $name );
        // @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
        $html .= Html::element( 'td', [ 'colspan' => '2', 'class' => 'diff-lineno' ], $name );
        $html .= Html::closeElement( 'tr' );

        return $html;
    }

}