wikimedia/mediawiki-extensions-Wikibase

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

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Repo\Api;

use MediaWiki\Api\ApiMain;
use MediaWiki\Api\ApiUsageException;
use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Term\AliasesProvider;
use Wikibase\Lib\EntityFactory;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\Summary;
use Wikibase\Repo\ChangeOp\ChangeOp;
use Wikibase\Repo\ChangeOp\ChangeOpFactoryProvider;
use Wikibase\Repo\ChangeOp\ChangeOps;
use Wikibase\Repo\ChangeOp\FingerprintChangeOpFactory;
use Wikibase\Repo\WikibaseRepo;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\Stats\IBufferingStatsdDataFactory;

/**
 * API module to set the aliases for a Wikibase entity.
 * Requires API write mode to be enabled.
 *
 * @license GPL-2.0-or-later
 */
class SetAliases extends ModifyEntity {

    /**
     * @var FingerprintChangeOpFactory
     */
    private $termChangeOpFactory;

    /** @var IBufferingStatsdDataFactory */
    private $stats;

    /** @var EntityFactory */
    private $entityFactory;

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

    public function __construct(
        ApiMain $mainModule,
        string $moduleName,
        FingerprintChangeOpFactory $termChangeOpFactory,
        IBufferingStatsdDataFactory $stats,
        bool $federatedPropertiesEnabled,
        EntityFactory $entityFactory,
        array $sandboxEntityIds
    ) {
        parent::__construct( $mainModule, $moduleName, $federatedPropertiesEnabled );
        $this->termChangeOpFactory = $termChangeOpFactory;
        $this->stats = $stats;
        $this->entityFactory = $entityFactory;
        $this->sandboxEntityIds = $sandboxEntityIds;
    }

    public static function factory(
        ApiMain $mainModule,
        string $moduleName,
        IBufferingStatsdDataFactory $stats,
        ChangeOpFactoryProvider $changeOpFactoryProvider,
        EntityFactory $entityFactory,
        SettingsArray $repoSettings
    ): self {
        return new self(
            $mainModule,
            $moduleName,
            $changeOpFactoryProvider
                ->getFingerprintChangeOpFactory(),
            $stats,
            $repoSettings->getSetting( 'federatedPropertiesEnabled' ),
            $entityFactory,
            $repoSettings->getSetting( 'sandboxEntityIds' )
        );
    }

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

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

    /**
     * @see ModifyEntity::validateParameters
     *
     * @param array $params
     *
     * @throws ApiUsageException
     */
    protected function validateParameters( array $params ): void {
        parent::validateParameters( $params );

        if ( !( ( !empty( $params['add'] ) || !empty( $params['remove'] ) )
            xor isset( $params['set'] )
        ) ) {
            $this->errorReporter->dieError(
                "Parameters 'add' and 'remove' are not allowed to be set when parameter 'set' is provided",
                'invalid-list'
            );
        }
    }

    private function adjustSummary( Summary $summary, array $params, AliasesProvider $entity ): void {
        if ( !empty( $params['add'] ) && !empty( $params['remove'] ) ) {
            $language = $params['language'];

            $aliasGroups = $entity->getAliasGroups();

            $summary->setAction( 'update' );
            $summary->setLanguage( $language );

            // Get the full list of current aliases
            if ( $aliasGroups->hasGroupForLanguage( $language ) ) {
                $aliases = $aliasGroups->getByLanguage( $language )->getAliases();
                $summary->addAutoSummaryArgs( $aliases );
            }
        }
    }

    protected function modifyEntity( EntityDocument $entity, ChangeOp $changeOp, array $preparedParameters ): Summary {
        if ( !( $entity instanceof AliasesProvider ) ) {
            $this->errorReporter->dieError( 'The given entity cannot contain aliases', 'not-supported' );
        }

        $language = $preparedParameters['language'];

        // FIXME: if we have ADD and REMOVE operations in the same call,
        // we will also have two ChangeOps updating the same edit summary.
        // This will cause the edit summary to be overwritten by the last ChangeOp being applied.
        $this->stats->increment( 'wikibase.repo.api.wbsetaliases.total' );
        if ( !empty( $preparedParameters['add'] ) && !empty( $preparedParameters['remove'] ) ) {
            $this->stats->increment( 'wikibase.repo.api.wbsetaliases.addremove' );
        }

        $summary = $this->createSummary( $preparedParameters );

        $this->applyChangeOp( $changeOp, $entity, $summary );

        $this->adjustSummary( $summary, $preparedParameters, $entity );

        $aliasGroups = $entity->getAliasGroups();

        if ( $aliasGroups->hasGroupForLanguage( $language ) ) {
            $aliasGroupList = $aliasGroups->getWithLanguages( [ $language ] );
            $this->getResultBuilder()->addAliasGroupList( $aliasGroupList, 'entity' );
        }

        return $summary;
    }

    /**
     * @param string[] $aliases
     *
     * @return string[]
     */
    private function normalizeAliases( array $aliases ): array {
        $stringNormalizer = $this->stringNormalizer;

        $aliases = array_map(
            function( $str ) use ( $stringNormalizer ) {
                return $stringNormalizer->trimToNFC( $str );
            },
            $aliases
        );

        $aliases = array_filter(
            $aliases,
            function( $str ) {
                return $str !== '';
            }
        );

        return $aliases;
    }

    protected function getChangeOp( array $preparedParameters, EntityDocument $entity ): ChangeOp {
        $changeOps = [];
        $language = $preparedParameters['language'];

        // Set the list of aliases to a user given one OR add/ remove certain entries
        if ( isset( $preparedParameters['set'] ) ) {
            $changeOps[] =
                $this->termChangeOpFactory->newSetAliasesOp(
                    $language,
                    $this->normalizeAliases( $preparedParameters['set'] )
                );
        } else {
            // FIXME: if we have ADD and REMOVE operations in the same call,
            // we will also have two ChangeOps updating the same edit summary.
            // This will cause the edit summary to be overwritten by the last ChangeOp beeing applied.
            if ( !empty( $preparedParameters['add'] ) ) {
                $changeOps[] =
                    $this->termChangeOpFactory->newAddAliasesOp(
                        $language,
                        $this->normalizeAliases( $preparedParameters['add'] )
                    );
            }

            if ( !empty( $preparedParameters['remove'] ) ) {
                $changeOps[] =
                    $this->termChangeOpFactory->newRemoveAliasesOp(
                        $language,
                        $this->normalizeAliases( $preparedParameters['remove'] )
                    );
            }
        }

        return $this->termChangeOpFactory->newFingerprintChangeOp( new ChangeOps( $changeOps ) );
    }

    /**
     * @inheritDoc
     */
    protected function getAllowedParams(): array {
        return array_merge(
            parent::getAllowedParams(),
            [
                'add' => [
                    ParamValidator::PARAM_TYPE => 'string',
                    ParamValidator::PARAM_ISMULTI => true,
                ],
                'remove' => [
                    ParamValidator::PARAM_TYPE => 'string',
                    ParamValidator::PARAM_ISMULTI => true,
                ],
                'set' => [
                    ParamValidator::PARAM_TYPE => 'string',
                    ParamValidator::PARAM_ISMULTI => true,
                ],
                'language' => [
                    // TODO inject TermsLanguages as a service
                    ParamValidator::PARAM_TYPE => WikibaseRepo::getTermsLanguages()->getLanguages(),
                    ParamValidator::PARAM_REQUIRED => true,
                ],
                'new' => [
                    ParamValidator::PARAM_TYPE => $this->getEntityTypesWithAliases(),
                ],
            ]
        );
    }

    protected function getEntityTypesWithAliases(): array {
        $supportedEntityTypes = [];
        foreach ( $this->enabledEntityTypes as $entityType ) {
            $testEntity = $this->entityFactory->newEmpty( $entityType );
            if ( $testEntity instanceof AliasesProvider ) {
                $supportedEntityTypes[] = $entityType;
            }
        }
        return $supportedEntityTypes;
    }

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

        return [
            'action=wbsetaliases&language=en&id=' . $id . '&set=Foo|Bar'
                => [ 'apihelp-wbsetaliases-example-1', $id ],

            'action=wbsetaliases&language=en&id=' . $id . '&add=Foo|Bar'
                => [ 'apihelp-wbsetaliases-example-2', $id ],

            'action=wbsetaliases&language=en&id=' . $id . '&remove=Foo|Bar'
                => [ 'apihelp-wbsetaliases-example-3', $id ],

            'action=wbsetaliases&language=en&id=' . $id . '&remove=Foo&add=Bar'
                => [ 'apihelp-wbsetaliases-example-4', $id ],
        ];
    }

}