wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Specials/SpecialMergeItems.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Repo\Specials;

use Exception;
use MediaWiki\Html\Html;
use MediaWiki\HTMLForm\HTMLForm;
use MediaWiki\Message\Message;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdParsingException;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Services\Lookup\UnresolvedEntityRedirectException;
use Wikibase\Lib\Store\EntityTitleLookup;
use Wikibase\Lib\UserInputException;
use Wikibase\Repo\AnonymousEditWarningBuilder;
use Wikibase\Repo\Interactors\ItemMergeException;
use Wikibase\Repo\Interactors\ItemMergeInteractor;
use Wikibase\Repo\Interactors\TokenCheckInteractor;
use Wikibase\Repo\Localizer\ExceptionLocalizer;

/**
 * Special page for merging one item to another.
 *
 * @license GPL-2.0-or-later
 * @author Bene* < benestar.wikimedia@gmail.com >
 * @author Daniel Kinzler
 */
class SpecialMergeItems extends SpecialWikibasePage {

    private AnonymousEditWarningBuilder $anonymousEditWarningBuilder;
    private EntityIdParser $idParser;
    private ExceptionLocalizer $exceptionLocalizer;
    private ItemMergeInteractor $interactor;
    private EntityTitleLookup $titleLookup;
    private TokenCheckInteractor $tokenCheck;
    private bool $isMobileView;

    public function __construct(
        AnonymousEditWarningBuilder $anonymousEditWarningBuilder,
        EntityIdParser $idParser,
        EntityTitleLookup $titleLookup,
        ExceptionLocalizer $exceptionLocalizer,
        ItemMergeInteractor $interactor,
        bool $isMobileView,
        TokenCheckInteractor $tokenCheck
    ) {
        parent::__construct( 'MergeItems', 'item-merge' );

        $this->anonymousEditWarningBuilder = $anonymousEditWarningBuilder;
        $this->idParser = $idParser;
        $this->exceptionLocalizer = $exceptionLocalizer;
        $this->interactor = $interactor;
        $this->titleLookup = $titleLookup;
        $this->tokenCheck = $tokenCheck;
        $this->isMobileView = $isMobileView;
    }

    public function doesWrites() {
        return true;
    }

    /**
     * @throws UserInputException
     */
    private function getItemIdParam( string $name ): ?ItemId {
        $rawId = $this->getTextParam( $name );

        if ( $rawId === '' ) {
            return null;
        }

        try {
            $id = $this->idParser->parse( $rawId );

            if ( !( $id instanceof ItemId ) ) {
                throw new UserInputException(
                    'wikibase-itemmerge-not-item',
                    [],
                    "$name \"$rawId\" does not refer to an Item"
                );
            }

            return $id;
        } catch ( EntityIdParsingException $ex ) {
            throw new UserInputException(
                'wikibase-wikibaserepopage-invalid-id',
                [ $rawId ],
                "$name \"$rawId\" is not valid"
            );
        }
    }

    private function getStringListParam( string $name ): array {
        $list = $this->getTextParam( $name );

        return $list === '' ? [] : explode( '|', $list );
    }

    private function getTextParam( string $name ): string {
        return trim( $this->getRequest()->getText( $name, '' ) );
    }

    /**
     * @see SpecialWikibasePage::execute
     *
     * @param string|null $subPage
     */
    public function execute( $subPage ) {
        parent::execute( $subPage );

        $this->checkPermissions();
        $this->checkBlocked();
        $this->checkReadOnly();

        $this->setHeaders();
        $this->outputHeader();

        try {
            $fromId = $this->getItemIdParam( 'fromid' );
            $toId = $this->getItemIdParam( 'toid' );

            $ignoreConflicts = $this->getStringListParam( 'ignoreconflicts' );
            $summary = $this->getTextParam( 'summary' );

            if ( $fromId && $toId ) {
                $success = $this->getStringListParam( 'success' );
                if ( count( $success ) === 2 && ctype_digit( $success[0] ) && ctype_digit( $success[1] ) ) {
                    // redirected back here after a successful edit + temp user, show success now
                    // (the success may be inaccurate if users created this URL manually, but that’s harmless)
                    $this->showSuccess( $fromId, $toId, (int)$success[0], (int)$success[1] );
                } else {
                    $this->mergeItems( $fromId, $toId, $ignoreConflicts, $summary );
                }
            }
        } catch ( ItemMergeException $ex ) {
            if ( $ex->getPrevious() instanceof UnresolvedEntityRedirectException ) {
                $this->showErrorHTML( $this->msg( 'wikibase-itemmerge-redirect' )->parse() );
            } else {
                $this->showExceptionMessage( $ex );
            }
        } catch ( Exception $ex ) {
            $this->showExceptionMessage( $ex );
        }

        $this->createForm();
    }

    protected function showExceptionMessage( Exception $ex ): void {
        $msg = $this->exceptionLocalizer->getExceptionMessage( $ex );

        $this->showErrorHTML( $msg->parse() );

        // Report chained exceptions recursively
        $previousEx = $ex->getPrevious();
        if ( $previousEx ) {
            $this->showExceptionMessage( $previousEx );
        }
    }

    /**
     * @param ItemId $fromId
     * @param ItemId $toId
     * @param string[] $ignoreConflicts
     * @param string $summary
     */
    private function mergeItems( ItemId $fromId, ItemId $toId, array $ignoreConflicts, $summary ): void {
        $this->tokenCheck->checkRequestToken( $this->getContext(), 'wpEditToken' );

        $status = $this->interactor->mergeItems( $fromId, $toId, $this->getContext(), $ignoreConflicts, $summary );
        $newRevisionFromId = $status->getFromEntityRevision()->getRevisionId();
        $newRevisionToId = $status->getToEntityRevision()->getRevisionId();
        $savedTempUser = $status->getSavedTempUser();
        if ( $savedTempUser !== null ) {
            $redirectUrl = '';
            $this->getHookRunner()->onTempUserCreatedRedirect(
                $this->getRequest()->getSession(),
                $savedTempUser,
                $this->getPageTitle()->getPrefixedDBkey(),
                "fromid={$fromId->getSerialization()}&toid={$toId->getSerialization()}" .
                    "&success=$newRevisionFromId|$newRevisionToId",
                '',
                $redirectUrl
            );
            if ( $redirectUrl ) {
                $this->getOutput()->redirect( $redirectUrl );
                return; // success will be shown when returning here from redirect
            }
        }

        $this->showSuccess( $fromId, $toId, $newRevisionFromId, $newRevisionToId );
    }

    private function showSuccess( ItemId $fromId, ItemId $toId, int $newRevisionFromId, int $newRevisionToId ): void {
        $linkRenderer = $this->getLinkRenderer();
        $fromTitle = $this->titleLookup->getTitleForId( $fromId );
        $toTitle = $this->titleLookup->getTitleForId( $toId );

        $this->getOutput()->addWikiMsg(
            'wikibase-mergeitems-success',
            Message::rawParam(
                $linkRenderer->makePreloadedLink(
                    $fromTitle,
                    $fromId->getSerialization(),
                    'mw-redirect',
                    [],
                    [ 'redirect' => 'no' ]
                )
            ),
            $newRevisionFromId,
            Message::rawParam(
                $linkRenderer->makeKnownLink(
                    $toTitle,
                    $toId->getSerialization()
                )
            ),
            $newRevisionToId
        );
    }

    /**
     * Creates the HTML form for merging two items.
     */
    protected function createForm(): void {
        // T324991
        if ( !$this->isMobileView ) {
            $this->getOutput()->addModules( 'wikibase.special.mergeItems' );
        }

        $pre = '';
        if ( !$this->getUser()->isRegistered() ) {
            $pre = Html::rawElement(
                'p',
                [ 'class' => 'warning' ],
                $this->anonymousEditWarningBuilder->buildAnonymousEditWarningHTML( $this->getFullTitle()->getPrefixedText() )
            );
        }

        HTMLForm::factory( 'ooui', $this->getFormElements(), $this->getContext() )
            ->setId( 'wb-mergeitems-form1' )
            ->setPreHtml( $pre )
            ->setHeaderHtml( $this->msg( 'wikibase-mergeitems-intro' )->parse() )
            ->setSubmitID( 'wb-mergeitems-submit' )
            ->setSubmitName( 'wikibase-mergeitems-submit' )
            ->setSubmitTextMsg( 'wikibase-mergeitems-submit' )
            ->setWrapperLegendMsg( 'special-mergeitems' )
            ->setSubmitCallback( function () {// no-op
            } )->show();
    }

    /**
     * @return array[]
     */
    protected function getFormElements(): array {
        return [
            'fromid' => [
                'name' => 'fromid',
                'default' => $this->getRequest()->getVal( 'fromid' ),
                'type' => 'text',
                'id' => 'wb-mergeitems-fromid',
                'label-message' => 'wikibase-mergeitems-fromid',
            ],
            'toid' => [
                'name' => 'toid',
                'default' => $this->getRequest()->getVal( 'toid' ),
                'type' => 'text',
                'id' => 'wb-mergeitems-toid',
                'label-message' => 'wikibase-mergeitems-toid',
            ],
        ];
        // TODO: Selector for ignoreconflicts
    }

}