Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.51% covered (success)
93.51%
72 / 77
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionHTMLHandler
93.51% covered (success)
93.51%
72 / 77
61.54% covered (warning)
61.54%
8 / 13
23.14
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
95.83% covered (success)
95.83%
23 / 24
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
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getResponseBodySchemaFileName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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%
9 / 9
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
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
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        $cacheExpiry = $this->htmlHelper->getHtml()->getCacheExpiry();
76
77        // This endpoint emits a full document from the page bundle
78        $parserOutputHtml = $this->htmlHelper->getPageBundle()->html;
79
80        $outputMode = $this->getOutputMode();
81        switch ( $outputMode ) {
82            case 'html':
83                $response = $this->getResponseFactory()->create();
84                // TODO: need to respect content-type returned by Parsoid.
85                $response->setHeader( ResponseHeaders::CONTENT_TYPE, 'text/html' );
86                $this->htmlHelper->putHeaders( $response, forHtml: true );
87                $this->contentHelper->setCacheControl( $response, $cacheExpiry );
88                $response->setBody( new StringStream( $parserOutputHtml ) );
89                break;
90            case 'with_html':
91                $body = $this->contentHelper->constructMetadata();
92                $body['html'] = $parserOutputHtml;
93                $response = $this->getResponseFactory()->createJson( $body );
94                // For JSON content, it doesn't make sense to set content language header
95                $this->htmlHelper->putHeaders( $response, forHtml: false );
96                $this->contentHelper->setCacheControl( $response, $cacheExpiry );
97                break;
98            default:
99                throw new LogicException( "Unknown HTML type $outputMode" );
100        }
101
102        return $response;
103    }
104
105    /**
106     * Returns an ETag representing a page's source. The ETag assumes a page's source has changed
107     * if the latest revision of a page has been made private, un-readable for another reason,
108     * or a newer revision exists.
109     * @return string|null
110     */
111    protected function getETag(): ?string {
112        if ( !$this->contentHelper->isAccessible() ) {
113            return null;
114        }
115
116        // Vary eTag based on output mode
117        return $this->htmlHelper->getETag( $this->getOutputMode() );
118    }
119
120    protected function getLastModified(): ?string {
121        if ( !$this->contentHelper->isAccessible() ) {
122            return null;
123        }
124
125        return $this->htmlHelper->getLastModified();
126    }
127
128    private function getOutputMode(): string {
129        return $this->getConfig()['format'];
130    }
131
132    public function needsWriteAccess(): bool {
133        return false;
134    }
135
136    protected function generateResponseSpec( string $method ): array {
137        $spec = parent::generateResponseSpec( $method );
138
139        // TODO: Consider if we prefer something like:
140        //    text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.8.0"
141        //  That would be more specific, but fragile when the profile version changes. It could
142        //  also be inaccurate if the page content was not in fact produced by Parsoid.
143        if ( $this->getOutputMode() == 'html' ) {
144            unset( $spec['200']['content']['application/json'] );
145            $spec['200']['content']['text/html']['schema']['type'] = 'string';
146            $spec['200']['content']['text/html']['example'] = '<h2 id="mwAA">Hello world</h2>';
147        }
148
149        return $spec;
150    }
151
152    public function getResponseBodySchemaFileName( string $method ): ?string {
153        return __DIR__ . '/Schema/ExistingRevisionHtml.json';
154    }
155
156    public function getParamSettings(): array {
157        return array_merge(
158            $this->contentHelper->getParamSettings(),
159            HtmlOutputRendererHelper::getParamSettings()
160        );
161    }
162
163    /**
164     * @inheritDoc
165     * @return array
166     */
167    public function getHeaderParamSettings(): array {
168        return [
169            'Accept-Language' => [
170                self::PARAM_SOURCE => 'header',
171                ParamValidator::PARAM_TYPE => 'string',
172                ParamValidator::PARAM_REQUIRED => false,
173                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-acceptlanguage' ),
174                Handler::PARAM_EXAMPLE => 'en',
175            ],
176        ];
177    }
178
179    /**
180     * @return bool
181     */
182    protected function hasRepresentation() {
183        return $this->contentHelper->hasContent();
184    }
185
186    /** @inheritDoc */
187    public function getResponseHeaderSettings(): array {
188        return array_merge(
189            parent::getResponseHeaderSettings(),
190            [
191                ResponseHeaders::CONTENT_TYPE => ResponseHeaders::RESPONSE_HEADER_DEFINITIONS[
192                    ResponseHeaders::CONTENT_TYPE
193                ]
194            ]
195        );
196    }
197}