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