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