Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompareHandler
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 11
552
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 getRevision
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRevisionOrThrow
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 isAccessible
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRole
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRevisionText
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 getJsonDiff
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getSectionInfo
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParamSettings
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use MediaWiki\Rest\Handler;
6use MediaWiki\Rest\LocalizedHttpException;
7use MediaWiki\Rest\StringStream;
8use MediaWiki\Revision\RevisionAccessException;
9use MediaWiki\Revision\RevisionLookup;
10use MediaWiki\Revision\RevisionRecord;
11use MediaWiki\Revision\SlotRecord;
12use MediaWiki\Revision\SuppressedDataException;
13use ParserFactory;
14use TextContent;
15use Wikimedia\Message\MessageValue;
16use Wikimedia\ParamValidator\ParamValidator;
17
18class CompareHandler extends Handler {
19    /** @var RevisionLookup */
20    private $revisionLookup;
21
22    /** @var ParserFactory */
23    private $parserFactory;
24
25    /** @var RevisionRecord[] */
26    private $revisions = [];
27
28    /** @var string[] */
29    private $textCache = [];
30
31    public function __construct(
32        RevisionLookup $revisionLookup,
33        ParserFactory $parserFactory
34    ) {
35        $this->revisionLookup = $revisionLookup;
36        $this->parserFactory = $parserFactory;
37    }
38
39    public function execute() {
40        $fromRev = $this->getRevisionOrThrow( 'from' );
41        $toRev = $this->getRevisionOrThrow( 'to' );
42
43        if ( $fromRev->getPageId() !== $toRev->getPageId() ) {
44            throw new LocalizedHttpException(
45                new MessageValue( 'rest-compare-page-mismatch' ), 400 );
46        }
47
48        if ( !$this->getAuthority()->authorizeRead( 'read', $toRev->getPage() ) ) {
49            throw new LocalizedHttpException(
50                new MessageValue( 'rest-compare-permission-denied' ), 403 );
51        }
52
53        $data = [
54            'from' => [
55                'id' => $fromRev->getId(),
56                'slot_role' => $this->getRole(),
57                'sections' => $this->getSectionInfo( 'from' )
58            ],
59            'to' => [
60                'id' => $toRev->getId(),
61                'slot_role' => $this->getRole(),
62                'sections' => $this->getSectionInfo( 'to' )
63            ],
64            'diff' => [ 'PLACEHOLDER' => null ]
65        ];
66        $rf = $this->getResponseFactory();
67        $wrapperJson = $rf->encodeJson( $data );
68        $diff = $this->getJsonDiff();
69        $response = $rf->create();
70        $response->setHeader( 'Content-Type', 'application/json' );
71        // A hack until getJsonDiff() is moved to SlotDiffRenderer and only nested inner diff is returned
72        $innerDiff = substr( $diff, 1, -1 );
73        $response->setBody( new StringStream(
74            str_replace( '"diff":{"PLACEHOLDER":null}', $innerDiff, $wrapperJson ) ) );
75        return $response;
76    }
77
78    /**
79     * @param string $paramName
80     * @return RevisionRecord|null
81     */
82    private function getRevision( $paramName ) {
83        if ( !isset( $this->revisions[$paramName] ) ) {
84            $this->revisions[$paramName] =
85                $this->revisionLookup->getRevisionById( $this->getValidatedParams()[$paramName] );
86        }
87        return $this->revisions[$paramName];
88    }
89
90    /**
91     * @param string $paramName
92     * @return RevisionRecord
93     * @throws LocalizedHttpException
94     */
95    private function getRevisionOrThrow( $paramName ) {
96        $rev = $this->getRevision( $paramName );
97        if ( !$rev ) {
98            throw new LocalizedHttpException(
99                new MessageValue( 'rest-compare-nonexistent', [ $paramName ] ), 404 );
100        }
101
102        if ( !$this->isAccessible( $rev ) ) {
103            throw new LocalizedHttpException(
104                new MessageValue( 'rest-compare-inaccessible', [ $paramName ] ), 403 );
105        }
106        return $rev;
107    }
108
109    /**
110     * @param RevisionRecord $rev
111     * @return bool
112     */
113    private function isAccessible( $rev ) {
114        return $rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() );
115    }
116
117    private function getRole() {
118        return SlotRecord::MAIN;
119    }
120
121    private function getRevisionText( $paramName ) {
122        if ( !isset( $this->textCache[$paramName] ) ) {
123            $revision = $this->getRevision( $paramName );
124            try {
125                $content = $revision
126                    ->getSlot( $this->getRole(), RevisionRecord::FOR_THIS_USER, $this->getAuthority() )
127                    ->getContent()
128                    ->convert( CONTENT_MODEL_TEXT );
129                if ( $content instanceof TextContent ) {
130                    $this->textCache[$paramName] = $content->getText();
131                } else {
132                    throw new LocalizedHttpException(
133                        new MessageValue(
134                            'rest-compare-wrong-content',
135                            [ $this->getRole(), $paramName ]
136                        ),
137                        400 );
138                }
139            } catch ( SuppressedDataException $e ) {
140                throw new LocalizedHttpException(
141                    new MessageValue( 'rest-compare-inaccessible', [ $paramName ] ), 403 );
142            } catch ( RevisionAccessException $e ) {
143                throw new LocalizedHttpException(
144                    new MessageValue( 'rest-compare-nonexistent', [ $paramName ] ), 404 );
145            }
146        }
147        return $this->textCache[$paramName];
148    }
149
150    /**
151     * @return string
152     */
153    private function getJsonDiff() {
154        // TODO: properly implement
155        // This is a prototype only. SlotDiffRenderer should be extended to support this use case.
156        $fromText = $this->getRevisionText( 'from' );
157        $toText = $this->getRevisionText( 'to' );
158        if ( !function_exists( 'wikidiff2_inline_json_diff' ) ) {
159            throw new LocalizedHttpException(
160                new MessageValue( 'rest-compare-wikidiff2' ), 500 );
161        }
162        return wikidiff2_inline_json_diff( $fromText, $toText, 2 );
163    }
164
165    /**
166     * @param string $paramName
167     * @return array
168     */
169    private function getSectionInfo( $paramName ) {
170        $text = $this->getRevisionText( $paramName );
171        $parserSections = $this->parserFactory->getInstance()->getFlatSectionInfo( $text );
172        $sections = [];
173        foreach ( $parserSections as $i => $parserSection ) {
174            // Skip section zero, which comes before the first heading, since
175            // its offset is always zero, so the client can assume its location.
176            if ( $i !== 0 ) {
177                $sections[] = [
178                    'level' => $parserSection['level'],
179                    'heading' => $parserSection['heading'],
180                    'offset' => $parserSection['offset'],
181                ];
182            }
183        }
184        return $sections;
185    }
186
187    /**
188     * @inheritDoc
189     */
190    public function needsWriteAccess() {
191        return false;
192    }
193
194    public function getParamSettings() {
195        return [
196            'from' => [
197                ParamValidator::PARAM_TYPE => 'integer',
198                ParamValidator::PARAM_REQUIRED => true,
199                Handler::PARAM_SOURCE => 'path',
200            ],
201            'to' => [
202                ParamValidator::PARAM_TYPE => 'integer',
203                ParamValidator::PARAM_REQUIRED => true,
204                Handler::PARAM_SOURCE => 'path',
205            ],
206        ];
207    }
208}