Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.74% covered (success)
94.74%
72 / 76
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageRedirectHelper
94.74% covered (success)
94.74%
72 / 76
66.67% covered (warning)
66.67%
8 / 12
29.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%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 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    /**
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}