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