Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
45.95% covered (danger)
45.95%
68 / 148
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionContentHelper
45.95% covered (danger)
45.95%
68 / 148
63.64% covered (warning)
63.64%
7 / 11
151.82
0.00% covered (danger)
0.00%
0 / 1
 getRevisionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getTitleText
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getPage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getTargetRevision
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 isAccessible
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
9.83
 hasContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCacheControl
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 constructMetadata
93.94% covered (success)
93.94%
31 / 33
0.00% covered (danger)
0.00%
0 / 1
5.01
 getResponseBodySchema
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
2
 getParamSettings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 checkAccess
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace MediaWiki\Rest\Handler\Helper;
4
5use MediaWiki\MainConfigNames;
6use MediaWiki\Page\ExistingPageRecord;
7use MediaWiki\Rest\Handler;
8use MediaWiki\Rest\LocalizedHttpException;
9use MediaWiki\Rest\ResponseInterface;
10use MediaWiki\Revision\RevisionRecord;
11use MediaWiki\Revision\SlotRecord;
12use Wikimedia\Message\MessageValue;
13use Wikimedia\ParamValidator\ParamValidator;
14
15/**
16 * @internal for use by core REST infrastructure
17 */
18class 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}