wikimedia/mediawiki-extensions-Wikibase

View on GitHub
client/includes/RecentChanges/ExternalChangeFactory.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

namespace Wikibase\Client\RecentChanges;

use MediaWiki\Language\Language;
use MediaWiki\User\ExternalUserNames;
use RecentChange;
use UnexpectedValueException;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdParsingException;

/**
 * @license GPL-2.0-or-later
 * @author Katie Filbert < aude.wiki@gmail.com >
 * @author Daniel Kinzler
 */
class ExternalChangeFactory {

    /**
     * @var string
     */
    private $repoSiteId;

    /**
     * @var Language
     */
    private $summaryLanguage;

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

    /**
     * @param string $repoSiteId
     * @param Language $summaryLanguage Language to use when generating edit summaries
     * @param EntityIdParser $idParser
     */
    public function __construct(
        $repoSiteId,
        Language $summaryLanguage,
        EntityIdParser $idParser
    ) {
        $this->repoSiteId = $repoSiteId;
        $this->summaryLanguage = $summaryLanguage;
        $this->idParser = $idParser;
    }

    /**
     * @param RecentChange $recentChange
     *
     * @throws UnexpectedValueException
     * @return ExternalChange
     */
    public function newFromRecentChange( RecentChange $recentChange ) {
        $rc_params = $recentChange->parseParams();

        if ( !is_array( $rc_params ) || !array_key_exists( 'wikibase-repo-change', $rc_params ) ) {
            throw new UnexpectedValueException( 'Not a Wikibase change' );
        }

        $changeParams = $this->extractChangeData( $rc_params );

        // If a pre-formatted comment exists, pass it on.
        $changeHtml = $rc_params['comment-html'] ?? null;

        $entityId = $this->extractEntityId( $changeParams['object_id'] );
        $changeType = $this->extractChangeType( $changeParams['type'] );
        $rev = $this->newRevisionData( $recentChange, $changeParams, $changeHtml );

        return new ExternalChange( $entityId, $rev, $changeType );
    }

    /**
     * @param RecentChange $recentChange
     * @param array $changeParams
     * @param string|null $commentHtml Pre-formatted comment HTML
     *
     * @return RevisionData
     */
    private function newRevisionData( RecentChange $recentChange, array $changeParams, $commentHtml = null ) {
        $repoId = $changeParams['site_id'] ?? $this->repoSiteId;

        $comment = $recentChange->getAttribute( 'rc_comment' );

        if ( $comment === '' || $comment === null ) {
            $comment = $this->generateComment( $changeParams );
        }

        return new RevisionData(
            ExternalUserNames::getLocal( $recentChange->getAttribute( 'rc_user_text' ) ),
            $recentChange->getAttribute( 'rc_timestamp' ),
            $comment,
            $commentHtml,
            $repoId,
            (int)$recentChange->getAttribute( 'rc_deleted' ),
            $changeParams
        );
    }

    /**
     * @param array $rc_params
     *
     * @throws UnexpectedValueException
     * @return array
     */
    private function extractChangeData( array $rc_params ) {
        $changeParams = $rc_params['wikibase-repo-change'];

        $this->validateChangeData( $changeParams );

        return $changeParams;
    }

    /**
     * @param mixed $changeParams
     *
     * @throws UnexpectedValueException
     * @return bool
     */
    private function validateChangeData( $changeParams ) {
        if ( !is_array( $changeParams ) ) {
            throw new UnexpectedValueException( 'Invalid Wikibase change' );
        }

        // TODO: Add central_user_id after
        // Ie7b9c482cf6a0dd7215b34841efd86fb51be651a has been deployed long
        // enough that there are no rows without this.
        // Bug: T51315
        $keys = [ 'type', 'page_id', 'rev_id', 'parent_id', 'object_id' ];

        foreach ( $keys as $key ) {
            if ( !array_key_exists( $key, $changeParams ) ) {
                throw new UnexpectedValueException( "$key key missing in change data" );
            }
        }

        return true;
    }

    /**
     * @see EntityChange::getAction
     *
     * @param string $type
     *
     * @throws UnexpectedValueException
     * @return string
     */
    private function extractChangeType( $type ) {
        if ( !is_string( $type ) ) {
            throw new UnexpectedValueException( '$type must be a string.' );
        }

        [ , $changeType ] = explode( '~', $type, 2 );

        return $changeType;
    }

    /**
     * @param string $prefixedId
     *
     * @throws UnexpectedValueException
     * @return EntityId
     */
    private function extractEntityId( $prefixedId ) {
        try {
            return $this->idParser->parse( $prefixedId );
        } catch ( EntityIdParsingException $ex ) {
            throw new UnexpectedValueException( 'Invalid $entityId found for change.' );
        }
    }

    /**
     * This method transforms the comments field into rc_params into an appropriate
     * comment value for ExternalChange.
     *
     * $comment can be a string or an array with some additional data.
     *
     * String comments are either 'wikibase-comment-update' (legacy) or have
     * comments from the repo, such as '/ wbsetclaim-update:2||1 / [[Property:P213]]: [[Q850]]'.
     *
     * We don't yet parse repo comments in the client, so for now, we use the
     * generic 'wikibase-comment-update' for these.
     *
     * Comment arrays may contain a message key that provide autocomments for stuff
     * like log actions (item deletion) or edits that have no meaningful summary
     * to use in the client.
     *
     *  - 'wikibase-comment-unlinked' (when the sitelink to the given page is removed on the repo)
     *  - 'wikibase-comment-add' (when the item is created, with sitelink to the given page)
     *  - 'wikibase-comment-remove' (when the item is deleted, the page becomes unconnected)
     *  - 'wikibase-comment-restore' (when the item is undeleted and reconnected to the page)
     *  - 'wikibase-comment-sitelink-add' (and other sitelink messages, unused)
     *  - 'wikibase-comment-update' (legacy, generic, item updated commment)
     *
     * @param array|string $comment
     * @param string $type
     *
     * @return array
     */
    private function parseAutoComment( $comment, $type ) {
        $newComment = [
            'key' => 'wikibase-comment-update',
        ];

        if ( is_array( $comment ) ) {
            if ( $type === 'wikibase-item~add' ) {
                // @todo: provide a link to the entity
                $newComment['key'] = 'wikibase-comment-linked';
            } elseif ( array_key_exists( 'sitelink', $comment ) ) {
                // @fixme site link change message
                $newComment['key'] = 'wikibase-comment-update';
            } else {
                $newComment['key'] = $comment['message'];
            }
        }

        return $newComment;
    }

    /**
     * @param array $comment
     *
     * @return string
     */
    private function formatComment( array $comment ) {
        $commentMsg = wfMessage( $comment['key'] )->inLanguage( $this->summaryLanguage );

        if ( isset( $comment['numparams'] ) ) {
            $commentMsg->numParams( $comment['numparams'] );
        }

        return $commentMsg->text();
    }

    /**
     * @param array $changeParams
     *
     * @return string
     */
    private function generateComment( array $changeParams ) {
        // NOTE: We want to get rid of the comment and composite-comment fields in $changeParams
        // in the future, see https://phabricator.wikimedia.org/T101836#1414639 part 3.
        if ( array_key_exists( 'composite-comment', $changeParams ) ) {
            $comment = [
                'key' => 'wikibase-comment-multi',
                'numparams' => $this->countCompositeComments( $changeParams['composite-comment'] ),
            ];

            return $this->formatComment( $comment );
        } elseif ( array_key_exists( 'comment', $changeParams ) ) {
            $comment = $this->parseAutoComment( $changeParams['comment'], $changeParams['type'] );

            return $this->formatComment( $comment );
        } else {
            // no override
            return '';
        }
    }

    /**
     * normalizes for extra empty comment in rc_params (see bug T47812)
     *
     * @param array $comments
     *
     * @return int
     */
    private function countCompositeComments( array $comments ) {
        $compositeComments = array_filter( $comments );

        return count( $compositeComments );
    }

}