wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Api/MergeItems.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Repo\Api;

use Exception;
use InvalidArgumentException;
use LogicException;
use MediaWiki\Api\ApiBase;
use MediaWiki\Api\ApiCreateTempUserTrait;
use MediaWiki\Api\ApiMain;
use MediaWiki\Api\ApiUsageException;
use Wikibase\DataModel\Entity\EntityIdParsingException;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\Store\EntityRevision;
use Wikibase\Repo\ChangeOp\ChangeOpsMerge;
use Wikibase\Repo\Interactors\ItemMergeException;
use Wikibase\Repo\Interactors\ItemMergeInteractor;
use Wikibase\Repo\Interactors\RedirectCreationException;
use Wikimedia\ParamValidator\ParamValidator;

/**
 * @license GPL-2.0-or-later
 * @author Addshore
 * @author Daniel Kinzler
 * @author Lucie-Aimée Kaffee
 */
class MergeItems extends ApiBase {

    use ApiCreateTempUserTrait;

    private ApiErrorReporter $errorReporter;
    private ItemMergeInteractor $interactor;
    private ResultBuilder $resultBuilder;
    private array $sandboxEntityIds;

    public function __construct(
        ApiMain $mainModule,
        string $moduleName,
        ItemMergeInteractor $interactor,
        ApiErrorReporter $errorReporter,
        callable $resultBuilderInstantiator,
        array $sandboxEntityIds
    ) {
        parent::__construct( $mainModule, $moduleName );

        $this->interactor = $interactor;

        $this->errorReporter = $errorReporter;
        $this->resultBuilder = $resultBuilderInstantiator( $this );

        $this->sandboxEntityIds = $sandboxEntityIds;
    }

    public static function factory(
        ApiMain $mainModule,
        string $moduleName,
        ApiHelperFactory $apiHelperFactory,
        ItemMergeInteractor $interactor,
        SettingsArray $settings
    ): self {
        return new self(
            $mainModule,
            $moduleName,
            $interactor,
            $apiHelperFactory->getErrorReporter( $mainModule ),
            function ( $module ) use ( $apiHelperFactory ) {
                return $apiHelperFactory->getResultBuilder( $module );
            },
            $settings->getSetting( 'sandboxEntityIds' )
        );
    }

    /**
     * @param array $parameters
     * @param string $name
     *
     * @return ItemId
     * @throws ApiUsageException if the given parameter is not a valid ItemId
     * @throws LogicException
     */
    private function getItemIdParam( array $parameters, string $name ): ItemId {
        if ( !isset( $parameters[$name] ) ) {
            $this->errorReporter->dieWithError( [ 'param-missing', $name ], 'param-missing' );
        }

        $value = $parameters[$name];

        try {
            return new ItemId( $value );
        } catch ( InvalidArgumentException $ex ) {
            $this->errorReporter->dieError( $ex->getMessage(), 'invalid-entity-id' );

            // @phan-suppress-next-line PhanPluginUnreachableCode Wanted
            throw new LogicException( 'ApiErrorReporter::dieError did not throw an exception' );
        }
    }

    /**
     * @inheritDoc
     */
    public function execute(): void {
        $params = $this->extractRequestParams();

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

            $ignoreConflicts = $params['ignoreconflicts'];
            $summary = $params['summary'];

            if ( $ignoreConflicts === null ) {
                $ignoreConflicts = [];
            }

            $this->mergeItems( $fromId, $toId, $ignoreConflicts, $summary, $params['bot'], $params['tags'] ?: [], $params );
        } catch ( EntityIdParsingException $ex ) {
            $this->errorReporter->dieException( $ex, 'invalid-entity-id' );
        } catch ( ItemMergeException | RedirectCreationException $ex ) {
            $this->handleException( $ex, $ex->getErrorCode() );
        }
    }

    /**
     * @param ItemId $fromId
     * @param ItemId $toId
     * @param string[] $ignoreConflicts
     * @param string|null $summary
     * @param bool $bot
     * @param string[] $tags Already permission checked via ParamValidator::PARAM_TYPE => 'tags'
     * @param array $params Any other params (for ApiCreateTempUserTrait).
     * @throws ItemMergeException
     * @throws RedirectCreationException
     */
    private function mergeItems(
        ItemId $fromId,
        ItemId $toId,
        array $ignoreConflicts,
        ?string $summary,
        bool $bot,
        array $tags,
        array $params
    ): void {
        $status = $this->interactor->mergeItems( $fromId, $toId, $this->getContext(), $ignoreConflicts, $summary, $bot, $tags );

        $this->resultBuilder->setValue( null, 'success', 1 );
        $this->resultBuilder->setValue( null, 'redirected', (int)$status->getRedirected() );

        $this->addEntityToOutput( $status->getFromEntityRevision(), 'from' );
        $this->addEntityToOutput( $status->getToEntityRevision(), 'to' );
        $this->resultBuilder->addTempUser( $status, fn( $user ) => $this->getTempUserRedirectUrl( $params, $user ) );
    }

    /**
     * @param Exception $ex
     * @param string $errorCode
     * @param string[] $extraData
     *
     * @throws ApiUsageException always
     */
    private function handleException( Exception $ex, string $errorCode, array $extraData = [] ): void {
        $cause = $ex->getPrevious();

        if ( $cause ) {
            $extraData[] = $ex->getMessage();
            $this->handleException( $cause, $errorCode, $extraData );
        } else {
            $this->errorReporter->dieException( $ex, $errorCode, 0, [ 'extradata' => $extraData ] );
        }
    }

    private function addEntityToOutput( EntityRevision $entityRevision, string $name ): void {
        $entityId = $entityRevision->getEntity()->getId();
        $revisionId = $entityRevision->getRevisionId();

        $this->resultBuilder->addBasicEntityInformation( $entityId, $name );

        $this->resultBuilder->setValue(
            $name,
            'lastrevid',
            (int)$revisionId
        );
    }

    /**
     * @see ApiBase::needsToken
     *
     * @return string
     */
    public function needsToken(): string {
        return 'csrf';
    }

    /**
     * @inheritDoc
     */
    protected function getAllowedParams(): array {
        return array_merge( [
            'fromid' => [
                ParamValidator::PARAM_TYPE => 'string',
                ParamValidator::PARAM_REQUIRED => true,
            ],
            'toid' => [
                ParamValidator::PARAM_TYPE => 'string',
                ParamValidator::PARAM_REQUIRED => true,
            ],
            'ignoreconflicts' => [
                ParamValidator::PARAM_ISMULTI => true,
                ParamValidator::PARAM_TYPE => ChangeOpsMerge::CONFLICT_TYPES,
                ParamValidator::PARAM_REQUIRED => false,
            ],
            'summary' => [
                ParamValidator::PARAM_TYPE => 'string',
            ],
            'tags' => [
                ParamValidator::PARAM_TYPE => 'tags',
                ParamValidator::PARAM_ISMULTI => true,
            ],
            'bot' => [
                ParamValidator::PARAM_TYPE => 'boolean',
                ParamValidator::PARAM_DEFAULT => false,
            ],
            'token' => [
                ParamValidator::PARAM_TYPE => 'string',
                ParamValidator::PARAM_REQUIRED => true,
            ],
        ], $this->getCreateTempUserParams() );
    }

    /**
     * @inheritDoc
     */
    protected function getExamplesMessages(): array {
        $from = $this->sandboxEntityIds[ 'mainItem' ];
        $to = $this->sandboxEntityIds[ 'auxItem' ];

        return [
            'action=wbmergeitems&fromid=' . $from . '&toid=' . $to =>
                [ 'apihelp-wbmergeitems-example-1', $from, $to ],
            'action=wbmergeitems&fromid=' . $from . '&toid=' . $to . '&ignoreconflicts=sitelink' =>
                [ 'apihelp-wbmergeitems-example-3', $from, $to ],
            'action=wbmergeitems&fromid=' . $from . '&toid=' . $to . '&ignoreconflicts=sitelink|description' =>
                [ 'apihelp-wbmergeitems-example-4', $from, $to ],
        ];
    }

    /**
     * @see ApiBase::isWriteMode
     *
     * @return bool Always true.
     */
    public function isWriteMode(): bool {
        return true;
    }

}