wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/ChangeOp/Deserialization/SiteLinksChangeOpDeserializer.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php

namespace Wikibase\Repo\ChangeOp\Deserialization;

use MediaWiki\Site\SiteList;
use OutOfBoundsException;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Services\Lookup\EntityLookup;
use Wikibase\Lib\StringNormalizer;
use Wikibase\Repo\ChangeOp\ChangeOp;
use Wikibase\Repo\ChangeOp\ChangeOpDeserializer;
use Wikibase\Repo\ChangeOp\ChangeOps;
use Wikibase\Repo\ChangeOp\SiteLinkChangeOpFactory;
use Wikibase\Repo\SiteLinkPageNormalizer;
use Wikibase\Repo\SiteLinkTargetProvider;

/**
 * Deserializer for site link change requests.
 *
 * @see @ref docs_topics_changeop-serializations for documentation on site link change request format.
 *
 * @license GPL-2.0-or-later
 */
class SiteLinksChangeOpDeserializer implements ChangeOpDeserializer {

    /**
     * @var SiteLinkBadgeChangeOpSerializationValidator
     */
    private $badgeChangeOpSerializationValidator;

    /**
     * @var SiteLinkChangeOpFactory
     */
    private $siteLinkChangeOpFactory;

    /** @var SiteLinkPageNormalizer */
    private $siteLinkPageNormalizer;

    /**
     * @var SiteLinkTargetProvider
     */
    private $siteLinkTargetProvider;

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

    /**
     * @var EntityIdParser
     */
    private $entityIdParser;

    /**
     * @var EntityLookup
     */
    private $entityLookup;

    /**
     * @var StringNormalizer
     */
    private $stringNormalizer;

    /**
     * @param SiteLinkBadgeChangeOpSerializationValidator $badgeChangeOpSerializationValidator
     * @param SiteLinkChangeOpFactory $siteLinkChangeOpFactory
     * @param SiteLinkPageNormalizer $siteLinkPageNormalizer
     * @param SiteLinkTargetProvider $siteLinkTargetProvider
     * @param EntityIdParser $entityIdParser
     * @param EntityLookup $entityLookup
     * @param StringNormalizer $stringNormalizer
     * @param string[] $siteLinkGroups
     */
    public function __construct(
        SiteLinkBadgeChangeOpSerializationValidator $badgeChangeOpSerializationValidator,
        SiteLinkChangeOpFactory $siteLinkChangeOpFactory,
        SiteLinkPageNormalizer $siteLinkPageNormalizer,
        SiteLinkTargetProvider $siteLinkTargetProvider,
        EntityIdParser $entityIdParser,
        EntityLookup $entityLookup,
        StringNormalizer $stringNormalizer,
        array $siteLinkGroups
    ) {
        $this->badgeChangeOpSerializationValidator = $badgeChangeOpSerializationValidator;
        $this->siteLinkChangeOpFactory = $siteLinkChangeOpFactory;
        $this->siteLinkPageNormalizer = $siteLinkPageNormalizer;
        $this->siteLinkTargetProvider = $siteLinkTargetProvider;
        $this->entityIdParser = $entityIdParser;
        $this->entityLookup = $entityLookup;
        $this->stringNormalizer = $stringNormalizer;
        $this->siteLinkGroups = $siteLinkGroups;
    }

    /**
     * @see ChangeOpDeserializer::createEntityChangeOp
     *
     * @param array $changeRequest
     *
     * @return ChangeOp
     *
     * @throws ChangeOpDeserializationException
     */
    public function createEntityChangeOp( array $changeRequest ) {
        $this->assertArray( $changeRequest['sitelinks'], 'List of sitelinks must be an array' );

        $siteLinksChangeOps = new ChangeOps();
        $sites = $this->siteLinkTargetProvider->getSiteList( $this->siteLinkGroups );

        foreach ( $changeRequest['sitelinks'] as $siteId => $serialization ) {
            '@phan-var array $serialization';
            $this->validateSiteLinkSerialization( $serialization, $siteId, $sites );
            $globalSiteId = $serialization['site'];

            $linkSite = $sites->getSite( $globalSiteId );
            $shouldRemove = array_key_exists( 'remove', $serialization )
                || ( !isset( $serialization['title'] ) && !isset( $serialization['badges'] ) )
                || ( isset( $serialization['title'] ) && $serialization['title'] === '' );

            if ( $shouldRemove ) {
                $siteLinksChangeOps->add( $this->siteLinkChangeOpFactory->newRemoveSiteLinkOp( $globalSiteId ) );
            } else {
                $badges = ( isset( $serialization['badges'] ) )
                    ? $this->getBadgeItemIds( $serialization['badges'] )
                    : null;

                $effectiveLinkTitle = isset( $serialization['title'] )
                    ? $this->stringNormalizer->trimWhitespace( $serialization['title'] )
                    : $this->getLinkTitleFromExistingSiteLink( $changeRequest['id'], $globalSiteId );

                $linkPage = $this->siteLinkPageNormalizer->normalize(
                    $linkSite,
                    $effectiveLinkTitle,
                    $serialization['badges'] ?? []
                );

                if ( $linkPage === false ) {
                    throw new ChangeOpDeserializationException(
                        'A page "' . $serialization['title'] . '" could not be found on "' . $globalSiteId . '"',
                        'no-external-page',
                        [ $globalSiteId, $serialization['title'] ]
                    );
                }

                $siteLinksChangeOps->add( $this->siteLinkChangeOpFactory->newSetSiteLinkOp( $globalSiteId, $linkPage, $badges ) );
            }
        }

        return $siteLinksChangeOps;
    }

    private function getLinkTitleFromExistingSiteLink( string $id, string $linkSite ): string {
        $entityId = $this->entityIdParser->parse( $id );
        $entity = $this->entityLookup->getEntity( $entityId );
        if ( $entity === null ) {
            throw new ChangeOpDeserializationException( 'The given entity does not exist', 'not-existing' );
        }
        if ( !( $entity instanceof Item ) ) {
            throw new ChangeOpDeserializationException( 'The given entity is not an item', 'not-item' );
        }
        try {
            $siteLink = $entity->getSiteLinkList()->getBySiteId( $linkSite );
        } catch ( OutOfBoundsException $e ) {
            throw new ChangeOpDeserializationException(
                "The given entity does not have a sitelink to $linkSite",
                'no-such-sitelink',
                [ $linkSite ]
            );
        }
        return $siteLink->getPageName();
    }

    /**
     * @param array $serialization Site link serialization array
     * @param string $siteCode
     * @param SiteList|null $sites Valid sites. Null for skipping site validity check.
     *
     * @throws ChangeOpDeserializationException
     */
    private function validateSiteLinkSerialization( $serialization, $siteCode, SiteList $sites = null ) {
        $this->assertArray( $serialization, 'An array was expected, but not found' );

        if ( !array_key_exists( 'site', $serialization ) ) {
            throw new ChangeOpDeserializationException( 'Site must be provided', 'no-site' );
        }
        $this->assertString( $serialization['site'], 'A string was expected, but not found' );

        if ( !is_numeric( $siteCode ) ) {
            if ( $siteCode !== $serialization['site'] ) {
                throw new ChangeOpDeserializationException(
                    "inconsistent site: $siteCode is not equal to {$serialization['site']}",
                    'inconsistent-site'
                );
            }
        }

        if ( $sites !== null && !$sites->hasSite( $serialization['site'] ) ) {
            throw new ChangeOpDeserializationException( 'Unknown site: ' . $serialization['site'], 'not-recognized-site' );
        }

        if ( isset( $serialization['title'] ) ) {
            $this->assertString( $serialization['title'], 'A string was expected, but not found' );
        }

        if ( isset( $serialization['badges'] ) ) {
            $this->assertArray( $serialization['badges'], 'Badges: an array was expected, but not found' );
            $this->badgeChangeOpSerializationValidator->validateBadgeSerialization( $serialization['badges'] );
        }
    }

    /**
     * @param string[] $badgeSerialization
     *
     * @return ItemId[]
     */
    private function getBadgeItemIds( array $badgeSerialization ) {
        $badgeIds = [];

        foreach ( $badgeSerialization as $badge ) {
            $badgeIds[] = $this->entityIdParser->parse( $badge );
        }

        return $badgeIds;
    }

    /**
     * @param string $message
     * @param string $errorCode
     * @return never
     *
     * @throws ChangeOpDeserializationException
     */
    private function throwException( $message, $errorCode ) {
        throw new ChangeOpDeserializationException( $message, $errorCode );
    }

    /**
     * @param mixed $value
     * @param string $message
     * @phan-assert array $value
     */
    private function assertArray( $value, $message ) {
        $this->assertType( 'array', $value, $message );
    }

    /**
     * @param mixed $value
     * @param string $message
     * @phan-assert string $value
     */
    private function assertString( $value, $message ) {
        $this->assertType( 'string', $value, $message );
    }

    /**
     * @param string $type
     * @param mixed $value
     * @param string $message
     */
    private function assertType( $type, $value, $message ) {
        if ( gettype( $value ) !== $type ) {
            $this->throwException( $message, 'not-recognized-' . $type );
        }
    }

}