Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.69% covered (warning)
81.69%
58 / 71
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageSourceHandler
81.69% covered (warning)
81.69%
58 / 71
66.67% covered (warning)
66.67%
8 / 12
27.54
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
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResponseBodySchemaFileName
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
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    /**
50     * @param PageReference $page
51     * @return string
52     */
53    private function constructHtmlUrl( PageReference $page ): string {
54        // TODO: once legacy "v1" routes are removed, just use the path prefix from the module.
55        $pathPrefix = $this->getModule()->getPathPrefix();
56        if ( strlen( $pathPrefix ) == 0 ) {
57            $pathPrefix = 'v1';
58        }
59
60        return $this->getRouter()->getRouteUrl(
61            '/' . $pathPrefix . '/page/{title}/html',
62            [ 'title' => $this->titleFormatter->getPrefixedText( $page ) ]
63        );
64    }
65
66    /**
67     * @return Response
68     * @throws LocalizedHttpException
69     */
70    public function run(): Response {
71        $this->contentHelper->checkAccess();
72        $page = $this->contentHelper->getPageIdentity();
73
74        if ( !$page->exists() ) {
75            // We may get here for "known" but non-existing pages, such as
76            // message pages. Since there is no page, we should still return
77            // a 404. See T349677 for discussion.
78            $titleText = $this->contentHelper->getTitleText() ?? '(unknown)';
79            throw new LocalizedHttpException(
80                MessageValue::new( 'rest-nonexistent-title' )
81                    ->plaintextParams( $titleText ),
82                404
83            );
84        }
85
86        $redirectHelper = $this->getRedirectHelper();
87
88        '@phan-var \MediaWiki\Page\ExistingPageRecord $page';
89        $redirectResponse = $redirectHelper->createNormalizationRedirectResponseIfNeeded(
90            $page,
91            $this->contentHelper->getTitleText()
92        );
93
94        if ( $redirectResponse !== null ) {
95            return $redirectResponse;
96        }
97
98        $outputMode = $this->getOutputMode();
99        switch ( $outputMode ) {
100            case 'restbase': // compatibility for restbase migration
101                $body = [ 'items' => [ $this->contentHelper->constructRestbaseCompatibleMetadata() ] ];
102                break;
103            case 'bare':
104                $body = $this->contentHelper->constructMetadata();
105                $body['html_url'] = $this->constructHtmlUrl( $page );
106                break;
107            case 'source':
108                $content = $this->contentHelper->getContent();
109                $body = $this->contentHelper->constructMetadata();
110                $body['source'] = $content->getText();
111                break;
112            default:
113                throw new LogicException( "Unknown HTML type $outputMode" );
114        }
115
116        // If param redirect=no is present, that means this page can be a redirect
117        // check for a redirectTargetUrl and send it to the body as `redirect_target`
118        '@phan-var \MediaWiki\Page\ExistingPageRecord $page';
119        $redirectTargetUrl = $redirectHelper->getWikiRedirectTargetUrl( $page );
120
121        if ( $redirectTargetUrl ) {
122            $body['redirect_target'] = $redirectTargetUrl;
123        }
124
125        $response = $this->getResponseFactory()->createJson( $body );
126        $this->contentHelper->setCacheControl( $response );
127
128        return $response;
129    }
130
131    /**
132     * Returns an ETag representing a page's source. The ETag assumes a page's source has changed
133     * if the latest revision of a page has been made private, un-readable for another reason,
134     * or a newer revision exists.
135     * @return string|null
136     */
137    protected function getETag(): ?string {
138        return $this->contentHelper->getETag();
139    }
140
141    /**
142     * @return string|null
143     */
144    protected function getLastModified(): ?string {
145        return $this->contentHelper->getLastModified();
146    }
147
148    private function getOutputMode(): string {
149        if ( $this->getRouter()->isRestbaseCompatEnabled( $this->getRequest() ) ) {
150            return 'restbase';
151        }
152        return $this->getConfig()['format'];
153    }
154
155    public function needsWriteAccess(): bool {
156        return false;
157    }
158
159    public function getParamSettings(): array {
160        return $this->contentHelper->getParamSettings();
161    }
162
163    /**
164     * @return bool
165     */
166    protected function hasRepresentation() {
167        return $this->contentHelper->hasContent();
168    }
169
170    public function getResponseBodySchemaFileName( string $method ): ?string {
171        // This does not include restbase compatibility mode, which is triggered by request
172        // headers. Presumably, such callers will look at the RESTBase spec instead.
173        switch ( $this->getConfig()['format'] ) {
174            case 'bare':
175                $schema = 'includes/Rest/Handler/Schema/ExistingPageBare.json';
176                break;
177            case 'source':
178                $schema = 'includes/Rest/Handler/Schema/ExistingPageSource.json';
179                break;
180            default:
181                $schema = null;
182                break;
183        }
184
185        return $schema;
186    }
187}