Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
45.95% |
68 / 148 |
|
63.64% |
7 / 11 |
CRAP | |
0.00% |
0 / 1 |
RevisionContentHelper | |
45.95% |
68 / 148 |
|
63.64% |
7 / 11 |
151.82 | |
0.00% |
0 / 1 |
getRevisionId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getTitleText | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getPage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getTargetRevision | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
isAccessible | |
28.57% |
2 / 7 |
|
0.00% |
0 / 1 |
9.83 | |||
hasContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCacheControl | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
constructMetadata | |
93.94% |
31 / 33 |
|
0.00% |
0 / 1 |
5.01 | |||
getResponseBodySchema | |
0.00% |
0 / 72 |
|
0.00% |
0 / 1 |
2 | |||
getParamSettings | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
checkAccess | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler\Helper; |
4 | |
5 | use MediaWiki\MainConfigNames; |
6 | use MediaWiki\Page\ExistingPageRecord; |
7 | use MediaWiki\Rest\Handler; |
8 | use MediaWiki\Rest\LocalizedHttpException; |
9 | use MediaWiki\Rest\ResponseInterface; |
10 | use MediaWiki\Revision\RevisionRecord; |
11 | use MediaWiki\Revision\SlotRecord; |
12 | use Wikimedia\Message\MessageValue; |
13 | use Wikimedia\ParamValidator\ParamValidator; |
14 | |
15 | /** |
16 | * @internal for use by core REST infrastructure |
17 | */ |
18 | class RevisionContentHelper extends PageContentHelper { |
19 | |
20 | /** |
21 | * @return int|null The ID of the target revision |
22 | */ |
23 | public function getRevisionId(): ?int { |
24 | return isset( $this->parameters['id'] ) ? (int)$this->parameters['id'] : null; |
25 | } |
26 | |
27 | /** |
28 | * @return string|null title text or null if unable to retrieve title |
29 | */ |
30 | public function getTitleText(): ?string { |
31 | $revision = $this->getTargetRevision(); |
32 | return $revision |
33 | ? $this->titleFormatter->getPrefixedText( $revision->getPageAsLinkTarget() ) |
34 | : null; |
35 | } |
36 | |
37 | /** |
38 | * @return ExistingPageRecord|null |
39 | */ |
40 | public function getPage(): ?ExistingPageRecord { |
41 | $revision = $this->getTargetRevision(); |
42 | return $revision ? $this->pageLookup->getPageByReference( $revision->getPage() ) : null; |
43 | } |
44 | |
45 | /** |
46 | * @return RevisionRecord|null latest revision or null if unable to retrieve revision |
47 | */ |
48 | public function getTargetRevision(): ?RevisionRecord { |
49 | if ( $this->targetRevision === false ) { |
50 | $revId = $this->getRevisionId(); |
51 | if ( $revId ) { |
52 | $this->targetRevision = $this->revisionLookup->getRevisionById( $revId ); |
53 | } else { |
54 | $this->targetRevision = null; |
55 | } |
56 | } |
57 | return $this->targetRevision; |
58 | } |
59 | |
60 | /** |
61 | * @return bool |
62 | */ |
63 | public function isAccessible(): bool { |
64 | if ( !parent::isAccessible() ) { |
65 | return false; |
66 | } |
67 | |
68 | $revision = $this->getTargetRevision(); |
69 | |
70 | // TODO: allow authorized users to see suppressed content. Set cache control accordingly. |
71 | |
72 | if ( !$revision || |
73 | !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) |
74 | ) { |
75 | return false; |
76 | } |
77 | |
78 | return true; |
79 | } |
80 | |
81 | /** |
82 | * @return bool |
83 | */ |
84 | public function hasContent(): bool { |
85 | return (bool)$this->getTargetRevision(); |
86 | } |
87 | |
88 | public function setCacheControl( ResponseInterface $response, int $expiry = null ) { |
89 | $revision = $this->getTargetRevision(); |
90 | |
91 | if ( $revision && $revision->getVisibility() !== 0 ) { |
92 | // The revision is not public, so it's not cacheable! |
93 | return; |
94 | } |
95 | |
96 | parent::setCacheControl( $response, $expiry ); |
97 | } |
98 | |
99 | /** |
100 | * @return array |
101 | */ |
102 | public function constructMetadata(): array { |
103 | $page = $this->getPage(); |
104 | $revision = $this->getTargetRevision(); |
105 | |
106 | $mainSlot = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ); |
107 | |
108 | $metadata = [ |
109 | 'id' => $revision->getId(), |
110 | 'size' => $revision->getSize(), |
111 | 'minor' => $revision->isMinor(), |
112 | 'timestamp' => wfTimestampOrNull( TS_ISO_8601, $revision->getTimestamp() ), |
113 | 'content_model' => $mainSlot->getModel(), |
114 | 'page' => [ |
115 | 'id' => $page->getId(), |
116 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
117 | 'key' => $this->titleFormatter->getPrefixedDBkey( $page ), |
118 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
119 | 'title' => $this->titleFormatter->getPrefixedText( $page ), |
120 | ], |
121 | 'license' => [ |
122 | 'url' => $this->options->get( MainConfigNames::RightsUrl ), |
123 | 'title' => $this->options->get( MainConfigNames::RightsText ) |
124 | ], |
125 | ]; |
126 | |
127 | $revUser = $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->authority ); |
128 | if ( $revUser ) { |
129 | $metadata['user'] = [ |
130 | 'id' => $revUser->isRegistered() ? $revUser->getId() : null, |
131 | 'name' => $revUser->getName() |
132 | ]; |
133 | } else { |
134 | $metadata['user'] = null; |
135 | } |
136 | |
137 | $comment = $revision->getComment( RevisionRecord::FOR_THIS_USER, $this->authority ); |
138 | $metadata['comment'] = $comment ? $comment->text : null; |
139 | |
140 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
141 | $parent = $this->revisionLookup->getPreviousRevision( $revision ); |
142 | if ( $parent ) { |
143 | $metadata['delta'] = $revision->getSize() - $parent->getSize(); |
144 | } else { |
145 | $metadata['delta'] = null; |
146 | } |
147 | |
148 | // FIXME: test fall fields |
149 | return $metadata; |
150 | } |
151 | |
152 | /** |
153 | * Returns an OpenAPI schema object describing the structure of the response. |
154 | * @return array |
155 | */ |
156 | public function getResponseBodySchema() { |
157 | // TODO: we need to reference a re-usable "user" type. The license structure should also be re-usable. |
158 | return [ |
159 | 'description' => 'revision meta-data', |
160 | 'required' => [ |
161 | 'id', 'size', 'delta', 'comment', 'minor', 'timestamp', 'content_model', 'page', 'license' |
162 | ], |
163 | 'properties' => [ |
164 | 'id' => [ |
165 | 'type' => 'integer', |
166 | 'description' => 'Revision id', |
167 | ], |
168 | 'size' => [ |
169 | 'type' => 'integer', |
170 | 'description' => 'The size of the revision, in no particular measure.', |
171 | ], |
172 | 'delta' => [ |
173 | 'type' => 'integer', |
174 | 'nullable' => true, |
175 | 'description' => 'The difference in size compared to the previous revision.', |
176 | ], |
177 | 'comment' => [ |
178 | 'type' => 'string', |
179 | 'nullable' => true, |
180 | 'description' => 'The comment the author associated with the revision', |
181 | ], |
182 | 'minor' => [ |
183 | 'type' => 'boolean', |
184 | 'description' => 'Whether the author of the revision conidered it minor.', |
185 | ], |
186 | 'timestamp' => [ |
187 | 'type' => 'string', |
188 | 'format' => 'date-time', |
189 | ], |
190 | 'content_model' => [ |
191 | 'type' => 'string', |
192 | 'format' => 'mw-content-model', |
193 | ], |
194 | 'page' => [ |
195 | 'description' => 'the page the revision belongs to', |
196 | 'required' => [ 'id', 'key', 'title' ], |
197 | 'properties' => [ |
198 | 'id' => [ |
199 | 'type' => 'integer', |
200 | 'description' => 'the page ID', |
201 | ], |
202 | 'key' => [ |
203 | 'type' => 'string', |
204 | 'format' => 'mw-title', |
205 | 'description' => 'the page title in URL form (unencoded)', |
206 | ], |
207 | 'title' => [ |
208 | 'type' => 'string', |
209 | 'format' => 'mw-title', |
210 | 'description' => 'the page title in human readable form', |
211 | ], |
212 | ] |
213 | ], |
214 | 'license' => [ |
215 | 'description' => 'license information for the revision content', |
216 | 'required' => [ 'url', 'title' ], |
217 | 'properties' => [ |
218 | 'url' => [ |
219 | 'type' => 'string', |
220 | 'format' => 'url', |
221 | ], |
222 | 'title' => [ |
223 | 'type' => 'string', |
224 | 'description' => 'the name of the license', |
225 | ], |
226 | ] |
227 | ], |
228 | ] |
229 | ]; |
230 | } |
231 | |
232 | /** |
233 | * @return array[] |
234 | */ |
235 | public function getParamSettings(): array { |
236 | return [ |
237 | 'id' => [ |
238 | Handler::PARAM_SOURCE => 'path', |
239 | ParamValidator::PARAM_TYPE => 'integer', |
240 | ParamValidator::PARAM_REQUIRED => true, |
241 | ], |
242 | ]; |
243 | } |
244 | |
245 | /** |
246 | * @throws LocalizedHttpException if the content is not accessible |
247 | */ |
248 | public function checkAccess() { |
249 | $revId = $this->getRevisionId() ?? ''; |
250 | |
251 | if ( !$this->hasContent() ) { |
252 | throw new LocalizedHttpException( |
253 | MessageValue::new( 'rest-nonexistent-revision' )->plaintextParams( $revId ), |
254 | 404 |
255 | ); |
256 | } |
257 | |
258 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Validated by hasContent |
259 | if ( !$this->isAccessible() || !$this->authority->authorizeRead( 'read', $this->getPageIdentity() ) ) { |
260 | throw new LocalizedHttpException( |
261 | MessageValue::new( 'rest-permission-denied-revision' )->plaintextParams( $revId ), |
262 | 403 |
263 | ); |
264 | } |
265 | } |
266 | |
267 | } |