Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.69% |
58 / 71 |
|
66.67% |
8 / 12 |
CRAP | |
0.00% |
0 / 1 |
PageSourceHandler | |
81.69% |
58 / 71 |
|
66.67% |
8 / 12 |
27.54 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getRedirectHelper | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
postValidationSetup | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
constructHtmlUrl | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
run | |
97.22% |
35 / 36 |
|
0.00% |
0 / 1 |
8 | |||
getETag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLastModified | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOutputMode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getParamSettings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasRepresentation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getResponseBodySchemaFileName | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Page\PageReference; |
7 | use MediaWiki\Rest\Handler\Helper\PageContentHelper; |
8 | use MediaWiki\Rest\Handler\Helper\PageRedirectHelper; |
9 | use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory; |
10 | use MediaWiki\Rest\LocalizedHttpException; |
11 | use MediaWiki\Rest\Response; |
12 | use MediaWiki\Rest\SimpleHandler; |
13 | use MediaWiki\Title\TitleFormatter; |
14 | use 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 | */ |
21 | class 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 | } |