Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.36% covered (warning)
86.36%
57 / 66
58.33% covered (warning)
58.33%
7 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageSourceHandler
86.36% covered (warning)
86.36%
57 / 66
58.33% covered (warning)
58.33%
7 / 12
22.12
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%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 constructHtmlUrl
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 run
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
8
 getETag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLastModified
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutputMode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 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%
1 / 1
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
 getResponseBodySchemaFileName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use LogicException;
6use MediaWiki\Page\PageReference;
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\Title\TitleFormatter;
14use Wikimedia\Message\MessageValue;
15
16/**
17 * Handler class for Core REST API Page Source endpoint with the following routes:
18 * - /page/{title}
19 * - /page/{title}/bare
20 */
21class PageSourceHandler extends SimpleHandler {
22
23    private TitleFormatter $titleFormatter;
24    private PageRestHelperFactory $helperFactory;
25    private PageContentHelper $contentHelper;
26
27    public function __construct(
28        TitleFormatter $titleFormatter,
29        PageRestHelperFactory $helperFactory
30    ) {
31        $this->titleFormatter = $titleFormatter;
32        $this->contentHelper = $helperFactory->newPageContentHelper();
33        $this->helperFactory = $helperFactory;
34    }
35
36    private function getRedirectHelper(): PageRedirectHelper {
37        return $this->helperFactory->newPageRedirectHelper(
38            $this->getResponseFactory(),
39            $this->getRouter(),
40            $this->getPath(),
41            $this->getRequest()
42        );
43    }
44
45    protected function postValidationSetup() {
46        $this->contentHelper->init( $this->getAuthority(), $this->getValidatedParams() );
47    }
48
49    private function constructHtmlUrl( PageReference $page ): string {
50        // TODO: once legacy "v1" routes are removed, just use the path prefix from the module.
51        $pathPrefix = $this->getModule()->getPathPrefix();
52        if ( $pathPrefix === '' ) {
53            $pathPrefix = 'v1';
54        }
55
56        return $this->getRouter()->getRouteUrl(
57            '/' . $pathPrefix . '/page/{title}/html',
58            [ 'title' => $this->titleFormatter->getPrefixedText( $page ) ]
59        );
60    }
61
62    /**
63     * @return Response
64     * @throws LocalizedHttpException
65     */
66    public function run(): Response {
67        $this->contentHelper->checkAccess();
68        $page = $this->contentHelper->getPageIdentity();
69
70        if ( !$page->exists() ) {
71            // We may get here for "known" but non-existing pages, such as
72            // message pages. Since there is no page, we should still return
73            // a 404. See T349677 for discussion.
74            $titleText = $this->contentHelper->getTitleText() ?? '(unknown)';
75            throw new LocalizedHttpException(
76                MessageValue::new( 'rest-nonexistent-title' )
77                    ->plaintextParams( $titleText ),
78                404
79            );
80        }
81
82        $redirectHelper = $this->getRedirectHelper();
83
84        '@phan-var \MediaWiki\Page\ExistingPageRecord $page';
85        $redirectResponse = $redirectHelper->createNormalizationRedirectResponseIfNeeded(
86            $page,
87            $this->contentHelper->getTitleText()
88        );
89
90        if ( $redirectResponse !== null ) {
91            return $redirectResponse;
92        }
93
94        $outputMode = $this->getOutputMode();
95        switch ( $outputMode ) {
96            case 'restbase': // compatibility for restbase migration
97                $body = [ 'items' => [ $this->contentHelper->constructRestbaseCompatibleMetadata() ] ];
98                break;
99            case 'bare':
100                $body = $this->contentHelper->constructMetadata();
101                $body['html_url'] = $this->constructHtmlUrl( $page );
102                break;
103            case 'source':
104                $content = $this->contentHelper->getContent();
105                $body = $this->contentHelper->constructMetadata();
106                $body['source'] = $content->getText();
107                break;
108            default:
109                throw new LogicException( "Unknown HTML type $outputMode" );
110        }
111
112        // If param redirect=no is present, that means this page can be a redirect
113        // check for a redirectTargetUrl and send it to the body as `redirect_target`
114        '@phan-var \MediaWiki\Page\ExistingPageRecord $page';
115        $redirectTargetUrl = $redirectHelper->getWikiRedirectTargetUrl( $page );
116
117        if ( $redirectTargetUrl ) {
118            $body['redirect_target'] = $redirectTargetUrl;
119        }
120
121        $response = $this->getResponseFactory()->createJson( $body );
122        $this->contentHelper->setCacheControl( $response );
123
124        return $response;
125    }
126
127    /**
128     * Returns an ETag representing a page's source. The ETag assumes a page's source has changed
129     * if the latest revision of a page has been made private, un-readable for another reason,
130     * or a newer revision exists.
131     * @return string|null
132     */
133    protected function getETag(): ?string {
134        return $this->contentHelper->getETag();
135    }
136
137    protected function getLastModified(): ?string {
138        return $this->contentHelper->getLastModified();
139    }
140
141    private function getOutputMode(): string {
142        if ( $this->getRouter()->isRestbaseCompatEnabled( $this->getRequest() ) ) {
143            return 'restbase';
144        }
145        return $this->getConfig()['format'];
146    }
147
148    public function needsWriteAccess(): bool {
149        return false;
150    }
151
152    public function getParamSettings(): array {
153        return $this->contentHelper->getParamSettings();
154    }
155
156    /**
157     * @return bool
158     */
159    protected function hasRepresentation() {
160        return $this->contentHelper->hasContent();
161    }
162
163    public function getResponseBodySchemaFileName( string $method ): ?string {
164        return match ( $this->getConfig()['format'] ) {
165            'bare' => 'includes/Rest/Handler/Schema/ExistingPageBare.json',
166            'source' => 'includes/Rest/Handler/Schema/ExistingPageSource.json',
167            default => null
168        };
169    }
170}