wikimedia/mediawiki-core

View on GitHub
includes/Rest/Handler/CompareHandler.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace MediaWiki\Rest\Handler;

use MediaWiki\Rest\Handler;
use MediaWiki\Rest\LocalizedHttpException;
use MediaWiki\Rest\StringStream;
use MediaWiki\Revision\RevisionAccessException;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Revision\SuppressedDataException;
use ParserFactory;
use TextContent;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;

class CompareHandler extends Handler {
    /** @var RevisionLookup */
    private $revisionLookup;

    /** @var ParserFactory */
    private $parserFactory;

    /** @var RevisionRecord[] */
    private $revisions = [];

    /** @var string[] */
    private $textCache = [];

    public function __construct(
        RevisionLookup $revisionLookup,
        ParserFactory $parserFactory
    ) {
        $this->revisionLookup = $revisionLookup;
        $this->parserFactory = $parserFactory;
    }

    public function execute() {
        $fromRev = $this->getRevisionOrThrow( 'from' );
        $toRev = $this->getRevisionOrThrow( 'to' );

        if ( $fromRev->getPageId() !== $toRev->getPageId() ) {
            throw new LocalizedHttpException(
                new MessageValue( 'rest-compare-page-mismatch' ), 400 );
        }

        if ( !$this->getAuthority()->authorizeRead( 'read', $toRev->getPage() ) ) {
            throw new LocalizedHttpException(
                new MessageValue( 'rest-compare-permission-denied' ), 403 );
        }

        $data = [
            'from' => [
                'id' => $fromRev->getId(),
                'slot_role' => $this->getRole(),
                'sections' => $this->getSectionInfo( 'from' )
            ],
            'to' => [
                'id' => $toRev->getId(),
                'slot_role' => $this->getRole(),
                'sections' => $this->getSectionInfo( 'to' )
            ],
            'diff' => [ 'PLACEHOLDER' => null ]
        ];
        $rf = $this->getResponseFactory();
        $wrapperJson = $rf->encodeJson( $data );
        $diff = $this->getJsonDiff();
        $response = $rf->create();
        $response->setHeader( 'Content-Type', 'application/json' );
        // A hack until getJsonDiff() is moved to SlotDiffRenderer and only nested inner diff is returned
        $innerDiff = substr( $diff, 1, -1 );
        $response->setBody( new StringStream(
            str_replace( '"diff":{"PLACEHOLDER":null}', $innerDiff, $wrapperJson ) ) );
        return $response;
    }

    /**
     * @param string $paramName
     * @return RevisionRecord|null
     */
    private function getRevision( $paramName ) {
        if ( !isset( $this->revisions[$paramName] ) ) {
            $this->revisions[$paramName] =
                $this->revisionLookup->getRevisionById( $this->getValidatedParams()[$paramName] );
        }
        return $this->revisions[$paramName];
    }

    /**
     * @param string $paramName
     * @return RevisionRecord
     * @throws LocalizedHttpException
     */
    private function getRevisionOrThrow( $paramName ) {
        $rev = $this->getRevision( $paramName );
        if ( !$rev ) {
            throw new LocalizedHttpException(
                new MessageValue( 'rest-compare-nonexistent', [ $paramName ] ), 404 );
        }

        if ( !$this->isAccessible( $rev ) ) {
            throw new LocalizedHttpException(
                new MessageValue( 'rest-compare-inaccessible', [ $paramName ] ), 403 );
        }
        return $rev;
    }

    /**
     * @param RevisionRecord $rev
     * @return bool
     */
    private function isAccessible( $rev ) {
        return $rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() );
    }

    private function getRole() {
        return SlotRecord::MAIN;
    }

    private function getRevisionText( $paramName ) {
        if ( !isset( $this->textCache[$paramName] ) ) {
            $revision = $this->getRevision( $paramName );
            try {
                $content = $revision
                    ->getSlot( $this->getRole(), RevisionRecord::FOR_THIS_USER, $this->getAuthority() )
                    ->getContent()
                    ->convert( CONTENT_MODEL_TEXT );
                if ( $content instanceof TextContent ) {
                    $this->textCache[$paramName] = $content->getText();
                } else {
                    throw new LocalizedHttpException(
                        new MessageValue(
                            'rest-compare-wrong-content',
                            [ $this->getRole(), $paramName ]
                        ),
                        400 );
                }
            } catch ( SuppressedDataException $e ) {
                throw new LocalizedHttpException(
                    new MessageValue( 'rest-compare-inaccessible', [ $paramName ] ), 403 );
            } catch ( RevisionAccessException $e ) {
                throw new LocalizedHttpException(
                    new MessageValue( 'rest-compare-nonexistent', [ $paramName ] ), 404 );
            }
        }
        return $this->textCache[$paramName];
    }

    /**
     * @return string
     */
    private function getJsonDiff() {
        // TODO: properly implement
        // This is a prototype only. SlotDiffRenderer should be extended to support this use case.
        $fromText = $this->getRevisionText( 'from' );
        $toText = $this->getRevisionText( 'to' );
        if ( !function_exists( 'wikidiff2_inline_json_diff' ) ) {
            throw new LocalizedHttpException(
                new MessageValue( 'rest-compare-wikidiff2' ), 500 );
        }
        return wikidiff2_inline_json_diff( $fromText, $toText, 2 );
    }

    /**
     * @param string $paramName
     * @return array
     */
    private function getSectionInfo( $paramName ) {
        $text = $this->getRevisionText( $paramName );
        $parserSections = $this->parserFactory->getInstance()->getFlatSectionInfo( $text );
        $sections = [];
        foreach ( $parserSections as $i => $parserSection ) {
            // Skip section zero, which comes before the first heading, since
            // its offset is always zero, so the client can assume its location.
            if ( $i !== 0 ) {
                $sections[] = [
                    'level' => $parserSection['level'],
                    'heading' => $parserSection['heading'],
                    'offset' => $parserSection['offset'],
                ];
            }
        }
        return $sections;
    }

    /**
     * @inheritDoc
     */
    public function needsWriteAccess() {
        return false;
    }

    public function getParamSettings() {
        return [
            'from' => [
                ParamValidator::PARAM_TYPE => 'integer',
                ParamValidator::PARAM_REQUIRED => true,
                Handler::PARAM_SOURCE => 'path',
            ],
            'to' => [
                ParamValidator::PARAM_TYPE => 'integer',
                ParamValidator::PARAM_REQUIRED => true,
                Handler::PARAM_SOURCE => 'path',
            ],
        ];
    }
}