Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 97 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
CompareHandler | |
0.00% |
0 / 97 |
|
0.00% |
0 / 11 |
552 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
getRevision | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getRevisionOrThrow | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
isAccessible | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRole | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRevisionText | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
getJsonDiff | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getSectionInfo | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getParamSettings | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler; |
4 | |
5 | use MediaWiki\Rest\Handler; |
6 | use MediaWiki\Rest\LocalizedHttpException; |
7 | use MediaWiki\Rest\StringStream; |
8 | use MediaWiki\Revision\RevisionAccessException; |
9 | use MediaWiki\Revision\RevisionLookup; |
10 | use MediaWiki\Revision\RevisionRecord; |
11 | use MediaWiki\Revision\SlotRecord; |
12 | use MediaWiki\Revision\SuppressedDataException; |
13 | use ParserFactory; |
14 | use TextContent; |
15 | use Wikimedia\Message\MessageValue; |
16 | use Wikimedia\ParamValidator\ParamValidator; |
17 | |
18 | class 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 | } |