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