Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
57 / 76
38.46% covered (danger)
38.46%
5 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionHTMLHandler
75.00% covered (warning)
75.00%
57 / 76
38.46% covered (danger)
38.46%
5 / 13
31.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 postValidationSetup
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 run
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
4
 getETag
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getLastModified
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getOutputMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generateResponseSpec
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getResponseBodySchemaFileName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParamSettings
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getHeaderParamSettings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 hasRepresentation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResponseHeaderSettings
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use LogicException;
6use MediaWiki\Rest\Handler;
7use MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper;
8use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory;
9use MediaWiki\Rest\Handler\Helper\RevisionContentHelper;
10use MediaWiki\Rest\LocalizedHttpException;
11use MediaWiki\Rest\Response;
12use MediaWiki\Rest\ResponseHeaders;
13use MediaWiki\Rest\SimpleHandler;
14use MediaWiki\Rest\StringStream;
15use Wikimedia\Assert\Assert;
16use Wikimedia\Message\MessageValue;
17use Wikimedia\ParamValidator\ParamValidator;
18
19/**
20 * A handler that returns Parsoid HTML for the following routes:
21 * - /revision/{revision}/html,
22 * - /revision/{revision}/with_html
23 */
24class RevisionHTMLHandler extends SimpleHandler {
25
26    private ?HtmlOutputRendererHelper $htmlHelper = null;
27    private PageRestHelperFactory $helperFactory;
28    private RevisionContentHelper $contentHelper;
29
30    public function __construct( PageRestHelperFactory $helperFactory ) {
31        $this->helperFactory = $helperFactory;
32        $this->contentHelper = $helperFactory->newRevisionContentHelper();
33    }
34
35    protected function postValidationSetup() {
36        $authority = $this->getAuthority();
37        $this->contentHelper->init( $authority, $this->getValidatedParams() );
38
39        $page = $this->contentHelper->getPage();
40        $revision = $this->contentHelper->getTargetRevision();
41
42        if ( $page && $revision ) {
43            $this->htmlHelper = $this->helperFactory->newHtmlOutputRendererHelper(
44                $page, $this->getValidatedParams(), $authority, $revision
45            );
46
47            $request = $this->getRequest();
48            $acceptLanguage = $request->getHeaderLine( 'Accept-Language' ) ?: null;
49            if ( $acceptLanguage ) {
50                $this->htmlHelper->setVariantConversionLanguage(
51                    $acceptLanguage
52                );
53            }
54        }
55    }
56
57    /**
58     * @return Response
59     * @throws LocalizedHttpException
60     */
61    public function run(): Response {
62        $this->contentHelper->checkAccess();
63
64        $page = $this->contentHelper->getPage();
65        $revisionRecord = $this->contentHelper->getTargetRevision();
66
67        // The call to $this->contentHelper->getPage() should not return null if
68        // $this->contentHelper->checkAccess() did not throw.
69        Assert::invariant( $page !== null, 'Page should be known' );
70
71        // The call to $this->contentHelper->getTargetRevision() should not return null if
72        // $this->contentHelper->checkAccess() did not throw.
73        Assert::invariant( $revisionRecord !== null, 'Revision should be known' );
74
75        $outputMode = $this->getOutputMode();
76        $setContentLanguageHeader = true;
77        switch ( $outputMode ) {
78            case 'html':
79                $parserOutput = $this->htmlHelper->getHtml();
80                $response = $this->getResponseFactory()->create();
81                // TODO: need to respect content-type returned by Parsoid.
82                $response->setHeader( ResponseHeaders::CONTENT_TYPE, 'text/html' );
83                $this->htmlHelper->putHeaders( $response, $setContentLanguageHeader );
84                $this->contentHelper->setCacheControl( $response, $parserOutput->getCacheExpiry() );
85                $response->setBody( new StringStream( $parserOutput->getRawText() ) );
86                break;
87            case 'with_html':
88                $parserOutput = $this->htmlHelper->getHtml();
89                $body = $this->contentHelper->constructMetadata();
90                $body['html'] = $parserOutput->getRawText();
91                $response = $this->getResponseFactory()->createJson( $body );
92                // For JSON content, it doesn't make sense to set content language header
93                $this->htmlHelper->putHeaders( $response, !$setContentLanguageHeader );
94                $this->contentHelper->setCacheControl( $response, $parserOutput->getCacheExpiry() );
95                break;
96            default:
97                throw new LogicException( "Unknown HTML type $outputMode" );
98        }
99
100        return $response;
101    }
102
103    /**
104     * Returns an ETag representing a page's source. The ETag assumes a page's source has changed
105     * if the latest revision of a page has been made private, un-readable for another reason,
106     * or a newer revision exists.
107     * @return string|null
108     */
109    protected function getETag(): ?string {
110        if ( !$this->contentHelper->isAccessible() ) {
111            return null;
112        }
113
114        // Vary eTag based on output mode
115        return $this->htmlHelper->getETag( $this->getOutputMode() );
116    }
117
118    protected function getLastModified(): ?string {
119        if ( !$this->contentHelper->isAccessible() ) {
120            return null;
121        }
122
123        return $this->htmlHelper->getLastModified();
124    }
125
126    private function getOutputMode(): string {
127        return $this->getConfig()['format'];
128    }
129
130    public function needsWriteAccess(): bool {
131        return false;
132    }
133
134    protected function generateResponseSpec( string $method ): array {
135        $spec = parent::generateResponseSpec( $method );
136
137        // TODO: Consider if we prefer something like:
138        //    text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.8.0"
139        //  That would be more specific, but fragile when the profile version changes. It could
140        //  also be inaccurate if the page content was not in fact produced by Parsoid.
141        if ( $this->getOutputMode() == 'html' ) {
142            unset( $spec['200']['content']['application/json'] );
143            $spec['200']['content']['text/html']['schema']['type'] = 'string';
144        }
145
146        return $spec;
147    }
148
149    public function getResponseBodySchemaFileName( string $method ): ?string {
150        return 'includes/Rest/Handler/Schema/ExistingRevisionHtml.json';
151    }
152
153    public function getParamSettings(): array {
154        return array_merge(
155            $this->contentHelper->getParamSettings(),
156            HtmlOutputRendererHelper::getParamSettings()
157        );
158    }
159
160    /**
161     * @inheritDoc
162     * @return array
163     */
164    public function getHeaderParamSettings(): array {
165        return [
166            'Accept-Language' => [
167                self::PARAM_SOURCE => 'header',
168                ParamValidator::PARAM_TYPE => 'string',
169                ParamValidator::PARAM_REQUIRED => false,
170                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-acceptlanguage' ),
171            ],
172        ];
173    }
174
175    /**
176     * @return bool
177     */
178    protected function hasRepresentation() {
179        return $this->contentHelper->hasContent();
180    }
181
182    /** @inheritDoc */
183    public function getResponseHeaderSettings(): array {
184        return array_merge(
185            parent::getResponseHeaderSettings(),
186            [
187                ResponseHeaders::CONTENT_TYPE => ResponseHeaders::RESPONSE_HEADER_DEFINITIONS[
188                    ResponseHeaders::CONTENT_TYPE
189                ]
190            ]
191        );
192    }
193}