Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.30% covered (success)
97.30%
72 / 74
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageHTMLHandler
97.30% covered (success)
97.30%
72 / 74
77.78% covered (warning)
77.78%
7 / 9
23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
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%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 run
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
7
 getETag
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getLastModified
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 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
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use LogicException;
6use MediaWiki\Rest\Handler\Helper\HtmlOutputHelper;
7use MediaWiki\Rest\Handler\Helper\PageContentHelper;
8use MediaWiki\Rest\Handler\Helper\PageRedirectHelper;
9use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory;
10use MediaWiki\Rest\LocalizedHttpException;
11use MediaWiki\Rest\Response;
12use MediaWiki\Rest\SimpleHandler;
13use MediaWiki\Rest\StringStream;
14use Wikimedia\Assert\Assert;
15
16/**
17 * A handler that returns Parsoid HTML for the following routes:
18 * - /page/{title}/html,
19 * - /page/{title}/with_html
20 *
21 * @package MediaWiki\Rest\Handler
22 */
23class PageHTMLHandler extends SimpleHandler {
24
25    private HtmlOutputHelper $htmlHelper;
26    private PageContentHelper $contentHelper;
27    private PageRestHelperFactory $helperFactory;
28
29    public function __construct(
30        PageRestHelperFactory $helperFactory
31    ) {
32        $this->contentHelper = $helperFactory->newPageContentHelper();
33        $this->helperFactory = $helperFactory;
34        $this->htmlHelper = $helperFactory->newHtmlOutputRendererHelper();
35    }
36
37    private function getRedirectHelper(): PageRedirectHelper {
38        return $this->helperFactory->newPageRedirectHelper(
39            $this->getResponseFactory(),
40            $this->getRouter(),
41            $this->getPath(),
42            $this->getRequest()
43        );
44    }
45
46    protected function postValidationSetup() {
47        $authority = $this->getAuthority();
48        $this->contentHelper->init( $authority, $this->getValidatedParams() );
49
50        $page = $this->contentHelper->getPageIdentity();
51        $isSystemMessage = $this->contentHelper->useDefaultSystemMessage();
52
53        if ( $page ) {
54            if ( $isSystemMessage ) {
55                $this->htmlHelper = $this->helperFactory->newHtmlMessageOutputHelper();
56                $this->htmlHelper->init( $page );
57            } else {
58                $revision = $this->contentHelper->getTargetRevision();
59                // NOTE: We know that $this->htmlHelper is an instance of HtmlOutputRendererHelper
60                //       because we set it in the constructor.
61                $this->htmlHelper->init( $page, $this->getValidatedParams(), $authority, $revision );
62
63                $request = $this->getRequest();
64                $acceptLanguage = $request->getHeaderLine( 'Accept-Language' ) ?: null;
65                if ( $acceptLanguage ) {
66                    $this->htmlHelper->setVariantConversionLanguage(
67                        $acceptLanguage
68                    );
69                }
70            }
71        }
72    }
73
74    /**
75     * @return Response
76     * @throws LocalizedHttpException
77     */
78    public function run(): Response {
79        $this->contentHelper->checkAccessPermission();
80        $page = $this->contentHelper->getPageIdentity();
81        $params = $this->getRequest()->getQueryParams();
82
83        if ( array_key_exists( 'redirect', $params ) ) {
84            $followWikiRedirects = $params['redirect'] !== 'no';
85        } else {
86            $followWikiRedirects = true;
87        }
88
89        // The call to $this->contentHelper->getPage() should not return null if
90        // $this->contentHelper->checkAccess() did not throw.
91        Assert::invariant( $page !== null, 'Page should be known' );
92
93        $redirectHelper = $this->getRedirectHelper();
94        $redirectHelper->setFollowWikiRedirects( $followWikiRedirects );
95        // Should treat variant redirects a special case as wiki redirects
96        // if ?redirect=no language variant should do nothing and fall into the 404 path
97        $redirectResponse = $redirectHelper->createRedirectResponseIfNeeded(
98            $page,
99            $this->contentHelper->getTitleText()
100        );
101
102        if ( $redirectResponse !== null ) {
103            return $redirectResponse;
104        }
105
106        // We could have a missing page at this point, check and return 404 if that's the case
107        $this->contentHelper->checkHasContent();
108
109        $parserOutput = $this->htmlHelper->getHtml();
110        $parserOutputHtml = $parserOutput->getRawText();
111
112        $outputMode = $this->getOutputMode();
113        switch ( $outputMode ) {
114            case 'html':
115                $response = $this->getResponseFactory()->create();
116                $this->contentHelper->setCacheControl( $response, $parserOutput->getCacheExpiry() );
117                $response->setBody( new StringStream( $parserOutputHtml ) );
118                break;
119            case 'with_html':
120                $body = $this->contentHelper->constructMetadata();
121                $body['html'] = $parserOutputHtml;
122
123                $redirectTargetUrl = $redirectHelper->getWikiRedirectTargetUrl( $page );
124
125                if ( $redirectTargetUrl ) {
126                    $body['redirect_target'] = $redirectTargetUrl;
127                }
128
129                $response = $this->getResponseFactory()->createJson( $body );
130                $this->contentHelper->setCacheControl( $response, $parserOutput->getCacheExpiry() );
131                break;
132            default:
133                throw new LogicException( "Unknown HTML type $outputMode" );
134        }
135
136        $setContentLanguageHeader = ( $outputMode === 'html' );
137        $this->htmlHelper->putHeaders( $response, $setContentLanguageHeader );
138
139        return $response;
140    }
141
142    /**
143     * Returns an ETag representing a page's source. The ETag assumes a page's source has changed
144     * if the latest revision of a page has been made private, un-readable for another reason,
145     * or a newer revision exists.
146     * @return string|null
147     */
148    protected function getETag(): ?string {
149        if ( !$this->contentHelper->isAccessible() || !$this->contentHelper->hasContent() ) {
150            return null;
151        }
152
153        // Vary eTag based on output mode
154        return $this->htmlHelper->getETag( $this->getOutputMode() );
155    }
156
157    /**
158     * @return string|null
159     */
160    protected function getLastModified(): ?string {
161        if ( !$this->contentHelper->isAccessible() || !$this->contentHelper->hasContent() ) {
162            return null;
163        }
164
165        return $this->htmlHelper->getLastModified();
166    }
167
168    private function getOutputMode(): string {
169        return $this->getConfig()['format'];
170    }
171
172    public function needsWriteAccess(): bool {
173        return false;
174    }
175
176    public function getParamSettings(): array {
177        return array_merge(
178            $this->contentHelper->getParamSettings(),
179            $this->htmlHelper->getParamSettings()
180        );
181    }
182}