Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.74% |
72 / 76 |
|
66.67% |
8 / 12 |
CRAP | |
0.00% |
0 / 1 |
PageRedirectHelper | |
94.74% |
72 / 76 |
|
66.67% |
8 / 12 |
29.12 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setUseRelativeRedirects | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFollowWikiRedirects | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createNormalizationRedirectResponseIfNeeded | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
createWikiRedirectResponseIfNeeded | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getWikiRedirectTargetUrl | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
createVariantRedirectResponseIfNeeded | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
getVariantRedirectTargetUrl | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
getTargetUrl | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
createRedirectResponseIfNeeded | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
hasVariants | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
findVariantPage | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler\Helper; |
4 | |
5 | use MediaWiki\Languages\LanguageConverterFactory; |
6 | use MediaWiki\Linker\LinkTarget; |
7 | use MediaWiki\Page\PageIdentity; |
8 | use MediaWiki\Page\PageReference; |
9 | use MediaWiki\Page\RedirectStore; |
10 | use MediaWiki\Rest\RequestInterface; |
11 | use MediaWiki\Rest\Response; |
12 | use MediaWiki\Rest\ResponseFactory; |
13 | use MediaWiki\Rest\Router; |
14 | use MediaWiki\Title\TitleFormatter; |
15 | use MediaWiki\Title\TitleValue; |
16 | |
17 | /** |
18 | * Helper class for handling page redirects, for use with REST Handlers that provide access |
19 | * to resources bound to MediaWiki pages. |
20 | * |
21 | * @since 1.41 |
22 | */ |
23 | class PageRedirectHelper { |
24 | private RedirectStore $redirectStore; |
25 | private TitleFormatter $titleFormatter; |
26 | private ResponseFactory $responseFactory; |
27 | private Router $router; |
28 | private string $path; |
29 | private RequestInterface $request; |
30 | private LanguageConverterFactory $languageConverterFactory; |
31 | private bool $followWikiRedirects = false; |
32 | private string $titleParamName = 'title'; |
33 | private bool $useRelativeRedirects = true; |
34 | |
35 | public function __construct( |
36 | RedirectStore $redirectStore, |
37 | TitleFormatter $titleFormatter, |
38 | ResponseFactory $responseFactory, |
39 | Router $router, |
40 | string $path, |
41 | RequestInterface $request, |
42 | LanguageConverterFactory $languageConverterFactory |
43 | ) { |
44 | $this->redirectStore = $redirectStore; |
45 | $this->titleFormatter = $titleFormatter; |
46 | $this->responseFactory = $responseFactory; |
47 | $this->router = $router; |
48 | $this->path = $path; |
49 | $this->request = $request; |
50 | $this->languageConverterFactory = $languageConverterFactory; |
51 | } |
52 | |
53 | /** |
54 | * @param bool $useRelativeRedirects |
55 | */ |
56 | public function setUseRelativeRedirects( bool $useRelativeRedirects ): void { |
57 | $this->useRelativeRedirects = $useRelativeRedirects; |
58 | } |
59 | |
60 | /** |
61 | * @param bool $followWikiRedirects |
62 | */ |
63 | public function setFollowWikiRedirects( bool $followWikiRedirects ): void { |
64 | $this->followWikiRedirects = $followWikiRedirects; |
65 | } |
66 | |
67 | /** |
68 | * Check for Page Normalization Redirects and create a Permanent Redirect Response |
69 | * @param PageIdentity $page |
70 | * @param ?string $titleAsRequested |
71 | * @return Response|null |
72 | */ |
73 | public function createNormalizationRedirectResponseIfNeeded( |
74 | PageIdentity $page, |
75 | ?string $titleAsRequested |
76 | ): ?Response { |
77 | if ( $titleAsRequested === null ) { |
78 | return null; |
79 | } |
80 | |
81 | $normalizedTitle = $this->titleFormatter->getPrefixedDBkey( $page ); |
82 | |
83 | // Check for normalization redirects |
84 | if ( $titleAsRequested !== $normalizedTitle ) { |
85 | $redirectTargetUrl = $this->getTargetUrl( $normalizedTitle ); |
86 | return $this->responseFactory->createPermanentRedirect( $redirectTargetUrl ); |
87 | } |
88 | |
89 | return null; |
90 | } |
91 | |
92 | /** |
93 | * Check for Page Wiki Redirects and create a Temporary Redirect Response |
94 | * @param PageIdentity $page |
95 | * @return Response|null |
96 | */ |
97 | public function createWikiRedirectResponseIfNeeded( PageIdentity $page ): ?Response { |
98 | $redirectTargetUrl = $this->getWikiRedirectTargetUrl( $page ); |
99 | |
100 | if ( $redirectTargetUrl === null ) { |
101 | return null; |
102 | } |
103 | |
104 | return $this->responseFactory->createTemporaryRedirect( $redirectTargetUrl ); |
105 | } |
106 | |
107 | /** |
108 | * @param PageIdentity $page |
109 | * @return string|null |
110 | */ |
111 | public function getWikiRedirectTargetUrl( PageIdentity $page ): ?string { |
112 | $redirectTarget = $this->redirectStore->getRedirectTarget( $page ); |
113 | |
114 | if ( $redirectTarget === null ) { |
115 | return null; |
116 | } |
117 | |
118 | if ( $redirectTarget->isSameLinkAs( TitleValue::newFromPage( $page ) ) ) { |
119 | // This can happen if the current page is virtual file description |
120 | // page backed by a remote file repo (T353688). |
121 | return null; |
122 | } |
123 | |
124 | return $this->getTargetUrl( $redirectTarget ); |
125 | } |
126 | |
127 | /** |
128 | * Check if a page with a variant title exists and create a Temporary Redirect Response |
129 | * if needed. |
130 | * |
131 | * @param PageIdentity $page |
132 | * @param string|null $titleAsRequested |
133 | * |
134 | * @return Response|null |
135 | */ |
136 | private function createVariantRedirectResponseIfNeeded( |
137 | PageIdentity $page, ?string $titleAsRequested |
138 | ): ?Response { |
139 | if ( $page->exists() ) { |
140 | // If the page exists, there is no need to generate a redirect. |
141 | return null; |
142 | } |
143 | |
144 | $redirectTargetUrl = $this->getVariantRedirectTargetUrl( |
145 | $page, |
146 | $titleAsRequested |
147 | ); |
148 | |
149 | if ( $redirectTargetUrl === null ) { |
150 | return null; |
151 | } |
152 | |
153 | return $this->responseFactory->createTemporaryRedirect( $redirectTargetUrl ); |
154 | } |
155 | |
156 | /** |
157 | * @param PageIdentity $page |
158 | * @param string $titleAsRequested |
159 | * |
160 | * @return string|null |
161 | */ |
162 | private function getVariantRedirectTargetUrl( |
163 | PageIdentity $page, string $titleAsRequested |
164 | ): ?string { |
165 | $variantPage = null; |
166 | if ( $this->hasVariants() ) { |
167 | $variantPage = $this->findVariantPage( $titleAsRequested, $page ); |
168 | } |
169 | |
170 | if ( !$variantPage ) { |
171 | return null; |
172 | } |
173 | |
174 | return $this->getTargetUrl( $variantPage ); |
175 | } |
176 | |
177 | /** |
178 | * @param string|LinkTarget|PageReference $title |
179 | * @return string The target to use in the Location header. Will be relative, |
180 | * unless setUseRelativeRedirects( false ) was called. |
181 | */ |
182 | public function getTargetUrl( $title ): string { |
183 | if ( !is_string( $title ) ) { |
184 | $title = $this->titleFormatter->getPrefixedDBkey( $title ); |
185 | } |
186 | |
187 | $pathParams = [ $this->titleParamName => $title ]; |
188 | |
189 | if ( $this->useRelativeRedirects ) { |
190 | return $this->router->getRoutePath( |
191 | $this->path, |
192 | $pathParams, |
193 | $this->request->getQueryParams() |
194 | ); |
195 | } else { |
196 | return $this->router->getRouteUrl( |
197 | $this->path, |
198 | $pathParams, |
199 | $this->request->getQueryParams() |
200 | ); |
201 | } |
202 | } |
203 | |
204 | /** |
205 | * Use this function for endpoints that check for both |
206 | * normalizations and wiki redirects. |
207 | * |
208 | * @param PageIdentity $page |
209 | * @param string|null $titleAsRequested |
210 | * @return Response|null |
211 | */ |
212 | public function createRedirectResponseIfNeeded( |
213 | PageIdentity $page, |
214 | ?string $titleAsRequested |
215 | ): ?Response { |
216 | if ( $titleAsRequested !== null ) { |
217 | $normalizationRedirectResponse = $this->createNormalizationRedirectResponseIfNeeded( |
218 | $page, $titleAsRequested |
219 | ); |
220 | |
221 | if ( $normalizationRedirectResponse !== null ) { |
222 | return $normalizationRedirectResponse; |
223 | } |
224 | } |
225 | |
226 | if ( $this->followWikiRedirects ) { |
227 | $variantRedirectResponse = $this->createVariantRedirectResponseIfNeeded( $page, $titleAsRequested ); |
228 | |
229 | if ( $variantRedirectResponse !== null ) { |
230 | return $variantRedirectResponse; |
231 | } |
232 | |
233 | $wikiRedirectResponse = $this->createWikiRedirectResponseIfNeeded( $page ); |
234 | |
235 | if ( $wikiRedirectResponse !== null ) { |
236 | return $wikiRedirectResponse; |
237 | } |
238 | } |
239 | |
240 | return null; |
241 | } |
242 | |
243 | private function hasVariants(): bool { |
244 | return $this->languageConverterFactory->getLanguageConverter()->hasVariants(); |
245 | } |
246 | |
247 | /** |
248 | * @param string $titleAsRequested |
249 | * @param PageReference $page |
250 | * |
251 | * @return ?PageReference |
252 | */ |
253 | private function findVariantPage( string $titleAsRequested, PageReference $page ): ?PageReference { |
254 | $originalPage = $page; |
255 | $languageConverter = $this->languageConverterFactory->getLanguageConverter(); |
256 | // @phan-suppress-next-line PhanTypeMismatchArgumentSuperType |
257 | $languageConverter->findVariantLink( $titleAsRequested, $page, true ); |
258 | |
259 | if ( $page === $originalPage ) { |
260 | // No variant link found, $page was not updated. |
261 | return null; |
262 | } |
263 | |
264 | return $page; |
265 | } |
266 | } |