Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.37% covered (warning)
88.37%
76 / 86
50.00% covered (danger)
50.00%
6 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageHTMLHandler
88.37% covered (warning)
88.37%
76 / 86
50.00% covered (danger)
50.00%
6 / 12
27.06
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
 getRedirectHelper
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 postValidationSetup
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 run
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
6
 getETag
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 getLastModified
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 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
 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
 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
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use LogicException;
6use MediaWiki\Rest\Handler;
7use MediaWiki\Rest\Handler\Helper\HtmlOutputHelper;
8use MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper;
9use MediaWiki\Rest\Handler\Helper\PageContentHelper;
10use MediaWiki\Rest\Handler\Helper\PageRedirectHelper;
11use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory;
12use MediaWiki\Rest\LocalizedHttpException;
13use MediaWiki\Rest\Response;
14use MediaWiki\Rest\ResponseHeaders;
15use MediaWiki\Rest\SimpleHandler;
16use MediaWiki\Rest\StringStream;
17use Wikimedia\Assert\Assert;
18use Wikimedia\Message\MessageValue;
19use Wikimedia\ParamValidator\ParamValidator;
20
21/**
22 * A handler that returns Parsoid HTML for the following routes:
23 * - /page/{title}/html,
24 * - /page/{title}/with_html
25 *
26 * @package MediaWiki\Rest\Handler
27 */
28class PageHTMLHandler extends SimpleHandler {
29
30    private HtmlOutputHelper $htmlHelper;
31    private PageContentHelper $contentHelper;
32    private PageRestHelperFactory $helperFactory;
33
34    public function __construct(
35        PageRestHelperFactory $helperFactory
36    ) {
37        $this->contentHelper = $helperFactory->newPageContentHelper();
38        $this->helperFactory = $helperFactory;
39    }
40
41    private function getRedirectHelper(): PageRedirectHelper {
42        return $this->helperFactory->newPageRedirectHelper(
43            $this->getResponseFactory(),
44            $this->getRouter(),
45            $this->getPath(),
46            $this->getRequest()
47        );
48    }
49
50    protected function postValidationSetup() {
51        $authority = $this->getAuthority();
52        $this->contentHelper->init( $authority, $this->getValidatedParams() );
53
54        $page = $this->contentHelper->getPageIdentity();
55        $isSystemMessage = $this->contentHelper->useDefaultSystemMessage();
56
57        if ( $page ) {
58            if ( $isSystemMessage ) {
59                $this->htmlHelper = $this->helperFactory->newHtmlMessageOutputHelper( $page );
60            } else {
61                $revision = $this->contentHelper->getTargetRevision();
62                $this->htmlHelper = $this->helperFactory->newHtmlOutputRendererHelper(
63                    $page, $this->getValidatedParams(), $authority, $revision
64                );
65
66                $request = $this->getRequest();
67                $acceptLanguage = $request->getHeaderLine( 'Accept-Language' ) ?: null;
68                if ( $acceptLanguage ) {
69                    $this->htmlHelper->setVariantConversionLanguage(
70                        $acceptLanguage
71                    );
72                }
73            }
74        }
75    }
76
77    /**
78     * @return Response
79     * @throws LocalizedHttpException
80     */
81    public function run(): Response {
82        $this->contentHelper->checkAccessPermission();
83        $page = $this->contentHelper->getPageIdentity();
84
85        $followWikiRedirects = $this->contentHelper->getRedirectsAllowed();
86
87        // The call to $this->contentHelper->getPage() should not return null if
88        // $this->contentHelper->checkAccess() did not throw.
89        Assert::invariant( $page !== null, 'Page should be known' );
90
91        $redirectHelper = $this->getRedirectHelper();
92        $redirectHelper->setFollowWikiRedirects( $followWikiRedirects );
93        // Should treat variant redirects a special case as wiki redirects
94        // if ?redirect=no language variant should do nothing and fall into the 404 path
95        $redirectResponse = $redirectHelper->createRedirectResponseIfNeeded(
96            $page,
97            $this->contentHelper->getTitleText()
98        );
99
100        if ( $redirectResponse !== null ) {
101            $redirectResponse->setHeader( ResponseHeaders::CACHE_CONTROL, 'max-age=60' );
102            return $redirectResponse;
103        }
104
105        // We could have a missing page at this point, check and return 404 if that's the case
106        $this->contentHelper->checkHasContent();
107
108        $parserOutput = $this->htmlHelper->getHtml();
109        $parserOutputHtml = $parserOutput->getRawText();
110
111        $outputMode = $this->getOutputMode();
112        switch ( $outputMode ) {
113            case 'html':
114                $response = $this->getResponseFactory()->create();
115                $this->contentHelper->setCacheControl( $response, $parserOutput->getCacheExpiry() );
116                $response->setBody( new StringStream( $parserOutputHtml ) );
117                break;
118            case 'with_html':
119                $body = $this->contentHelper->constructMetadata();
120                $body['html'] = $parserOutputHtml;
121
122                $redirectTargetUrl = $redirectHelper->getWikiRedirectTargetUrl( $page );
123
124                if ( $redirectTargetUrl ) {
125                    $body['redirect_target'] = $redirectTargetUrl;
126                }
127
128                $response = $this->getResponseFactory()->createJson( $body );
129                $this->contentHelper->setCacheControl( $response, $parserOutput->getCacheExpiry() );
130                break;
131            default:
132                throw new LogicException( "Unknown HTML type $outputMode" );
133        }
134
135        $setContentLanguageHeader = ( $outputMode === 'html' );
136        $this->htmlHelper->putHeaders( $response, $setContentLanguageHeader );
137
138        return $response;
139    }
140
141    /**
142     * Returns an ETag representing a page's source. The ETag assumes a page's source has changed
143     * if the latest revision of a page has been made private, un-readable for another reason,
144     * or a newer revision exists.
145     * @return string|null
146     */
147    protected function getETag(): ?string {
148        if ( !$this->contentHelper->isAccessible() || !$this->contentHelper->hasContent() ) {
149            return null;
150        }
151
152        // Vary eTag based on output mode
153        return $this->htmlHelper->getETag( $this->getOutputMode() );
154    }
155
156    protected function getLastModified(): ?string {
157        if ( !$this->contentHelper->isAccessible() || !$this->contentHelper->hasContent() ) {
158            return null;
159        }
160
161        return $this->htmlHelper->getLastModified();
162    }
163
164    private function getOutputMode(): string {
165        return $this->getConfig()['format'];
166    }
167
168    public function needsWriteAccess(): bool {
169        return false;
170    }
171
172    public function getParamSettings(): array {
173        return array_merge(
174            $this->contentHelper->getParamSettings(),
175            // Note that postValidation we might end up using
176            // a HtmlMessageOutputHelper, but the param settings
177            // for that are a subset of those for HtmlOutputRendererHelper
178            HtmlOutputRendererHelper::getParamSettings()
179        );
180    }
181
182    public function getHeaderParamSettings(): array {
183        return [
184            'Accept-Language' => [
185                self::PARAM_SOURCE => 'header',
186                ParamValidator::PARAM_TYPE => 'string',
187                ParamValidator::PARAM_REQUIRED => false,
188                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-acceptlanguage' ),
189            ],
190        ];
191    }
192
193    protected function generateResponseSpec( string $method ): array {
194        $spec = parent::generateResponseSpec( $method );
195
196        // TODO: Consider if we prefer something like:
197        //    text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.8.0"
198        //  That would be more specific, but fragile when the profile version changes. It could
199        //  also be inaccurate if the page content was not in fact produced by Parsoid.
200        if ( $this->getOutputMode() == 'html' ) {
201            unset( $spec['200']['content']['application/json'] );
202            $spec['200']['content']['text/html']['schema']['type'] = 'string';
203        }
204
205        return $spec;
206    }
207
208    public function getResponseBodySchemaFileName( string $method ): ?string {
209        return __DIR__ . '/Schema/ExistingPageHtml.json';
210    }
211}