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