Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.47% covered (warning)
89.47%
68 / 76
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionContentHelper
89.47% covered (warning)
89.47%
68 / 76
70.00% covered (warning)
70.00%
7 / 10
27.85
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.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
5.01
 getParamSettings
100.00% covered (success)
100.00%
8 / 8
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 Wikimedia\Message\MessageValue;
12use Wikimedia\ParamValidator\ParamValidator;
13
14/**
15 * @internal for use by core REST infrastructure
16 */
17class RevisionContentHelper extends PageContentHelper {
18    /**
19     * @return int|null The ID of the target revision
20     */
21    public function getRevisionId(): ?int {
22        return isset( $this->parameters['id'] ) ? (int)$this->parameters['id'] : null;
23    }
24
25    /**
26     * @return string|null title text or null if unable to retrieve title
27     */
28    public function getTitleText(): ?string {
29        $revision = $this->getTargetRevision();
30        return $revision
31            ? $this->titleFormatter->getPrefixedText( $revision->getPageAsLinkTarget() )
32            : null;
33    }
34
35    /**
36     * @return ExistingPageRecord|null
37     */
38    public function getPage(): ?ExistingPageRecord {
39        $revision = $this->getTargetRevision();
40        return $revision ? $this->pageLookup->getPageByReference( $revision->getPage() ) : null;
41    }
42
43    /**
44     * @return RevisionRecord|null latest revision or null if unable to retrieve revision
45     */
46    public function getTargetRevision(): ?RevisionRecord {
47        if ( $this->targetRevision === false ) {
48            $revId = $this->getRevisionId();
49            if ( $revId ) {
50                $this->targetRevision = $this->revisionLookup->getRevisionById( $revId );
51            } else {
52                $this->targetRevision = null;
53            }
54        }
55        return $this->targetRevision;
56    }
57
58    /**
59     * @return bool
60     */
61    public function isAccessible(): bool {
62        if ( !parent::isAccessible() ) {
63            return false;
64        }
65
66        $revision = $this->getTargetRevision();
67
68        // TODO: allow authorized users to see suppressed content. Set cache control accordingly.
69
70        if ( !$revision ||
71            !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC )
72        ) {
73            return false;
74        }
75
76        return true;
77    }
78
79    /**
80     * @return bool
81     */
82    public function hasContent(): bool {
83        return (bool)$this->getTargetRevision();
84    }
85
86    public function setCacheControl( ResponseInterface $response, ?int $expiry = null ) {
87        $revision = $this->getTargetRevision();
88
89        if ( $revision && $revision->getVisibility() !== 0 ) {
90            // The revision is not public, so it's not cacheable!
91            return;
92        }
93
94        parent::setCacheControl( $response, $expiry );
95    }
96
97    /**
98     * @return array
99     */
100    public function constructMetadata(): array {
101        $page = $this->getPage();
102        $revision = $this->getTargetRevision();
103
104        $metadata = [
105            'id' => $revision->getId(),
106            'size' => $revision->getSize(),
107            'minor' => $revision->isMinor(),
108            'timestamp' => wfTimestampOrNull( TS_ISO_8601, $revision->getTimestamp() ),
109            'content_model' => $revision->getMainContentModel(),
110            'page' => [
111                'id' => $page->getId(),
112                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
113                'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
114                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
115                'title' => $this->titleFormatter->getPrefixedText( $page ),
116            ],
117            'license' => [
118                'url' => $this->options->get( MainConfigNames::RightsUrl ),
119                'title' => $this->options->get( MainConfigNames::RightsText )
120            ],
121        ];
122
123        $revUser = $revision->getUser( RevisionRecord::FOR_THIS_USER, $this->authority );
124        if ( $revUser ) {
125            $metadata['user'] = [
126                'id' => $revUser->isRegistered() ? $revUser->getId() : null,
127                'name' => $revUser->getName()
128            ];
129        } else {
130            $metadata['user'] = null;
131        }
132
133        $comment = $revision->getComment( RevisionRecord::FOR_THIS_USER, $this->authority );
134        $metadata['comment'] = $comment ? $comment->text : null;
135
136        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
137        $parent = $this->revisionLookup->getPreviousRevision( $revision );
138        if ( $parent ) {
139            $metadata['delta'] = $revision->getSize() - $parent->getSize();
140        } else {
141            $metadata['delta'] = null;
142        }
143
144        // FIXME: test fall fields
145        return $metadata;
146    }
147
148    /**
149     * @return array[]
150     */
151    public function getParamSettings(): array {
152        return [
153            'id' => [
154                Handler::PARAM_SOURCE => 'path',
155                ParamValidator::PARAM_TYPE => 'integer',
156                ParamValidator::PARAM_REQUIRED => true,
157                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-revision-id' )
158            ],
159        ];
160    }
161
162    /**
163     * @throws LocalizedHttpException if the content is not accessible
164     */
165    public function checkAccess() {
166        $revId = $this->getRevisionId() ?? '';
167
168        if ( !$this->hasContent() ) {
169            throw new LocalizedHttpException(
170                MessageValue::new( 'rest-nonexistent-revision' )->plaintextParams( $revId ),
171                404
172            );
173        }
174
175        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Validated by hasContent
176        if ( !$this->isAccessible() || !$this->authority->authorizeRead( 'read', $this->getPageIdentity() ) ) {
177            throw new LocalizedHttpException(
178                MessageValue::new( 'rest-permission-denied-revision' )->plaintextParams( $revId ),
179                403
180            );
181        }
182    }
183
184}