Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.00% covered (warning)
84.00%
42 / 50
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMyLanguage
85.71% covered (warning)
85.71%
42 / 49
50.00% covered (danger)
50.00%
3 / 6
31.45
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
 getRedirect
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 findTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findTitleForTransclusion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findTitleInternal
94.87% covered (success)
94.87%
37 / 39
0.00% covered (danger)
0.00%
0 / 1
23.07
 personallyIdentifiableTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Languages\LanguageNameUtils;
10use MediaWiki\Page\RedirectLookup;
11use MediaWiki\SpecialPage\RedirectSpecialArticle;
12use MediaWiki\Title\Title;
13
14/**
15 * Redirect to the appropriate translated version of a page if it exists.
16 *
17 * Usage: [[Special:MyLanguage/Page name|link text]]
18 *
19 * @since 1.24
20 * @ingroup SpecialPage
21 * @author Niklas Laxström
22 * @author Siebrand Mazeland
23 * @copyright Copyright © 2010-2013 Niklas Laxström, Siebrand Mazeland
24 */
25class SpecialMyLanguage extends RedirectSpecialArticle {
26
27    private LanguageNameUtils $languageNameUtils;
28    private RedirectLookup $redirectLookup;
29
30    public function __construct(
31        LanguageNameUtils $languageNameUtils,
32        RedirectLookup $redirectLookup
33    ) {
34        parent::__construct( 'MyLanguage' );
35        $this->languageNameUtils = $languageNameUtils;
36        $this->redirectLookup = $redirectLookup;
37    }
38
39    /**
40     * If the special page is a redirect, then get the Title object it redirects to.
41     * False otherwise.
42     *
43     * @param string|null $subpage
44     * @return Title
45     */
46    public function getRedirect( $subpage ) {
47        $title = $this->findTitle( $subpage );
48        // Go to the main page if given invalid title.
49        if ( !$title ) {
50            $title = Title::newMainPage();
51        }
52        return $title;
53    }
54
55    /**
56     * Find a title.
57     *
58     * This may return the base page, e.g. if the UI and
59     * content language are the same.
60     *
61     * Examples, assuming the UI language is fi and the content language
62     * is en:
63     * - input Page: returns Page/fi if it exists, otherwise Page
64     * - input Page/de: returns Page/fi if it exists, otherwise Page/de
65     * if it exists, otherwise Page
66     *
67     * @param string|null $subpage
68     * @return Title|null
69     */
70    public function findTitle( $subpage ) {
71        return $this->findTitleInternal( $subpage, false );
72    }
73
74    /**
75     * Find a title for transclusion. This avoids returning the base
76     * page if a suitable alternative exists.
77     *
78     * Examples, assuming the UI language is fi and the content language
79     * is en:
80     * - input Page: returns Page/fi if it exists, otherwise Page/en if
81     * it exists, otherwise Page
82     * - input Page/de: returns Page/fi if it exists, otherwise Page/de
83     * if it exists, otherwise Page/en if it exists, otherwise Page
84     *
85     * @param string|null $subpage
86     * @return Title|null
87     */
88    public function findTitleForTransclusion( $subpage ) {
89        return $this->findTitleInternal( $subpage, true );
90    }
91
92    /**
93     * Find a title, depending on the content language and the user's
94     * interface language.
95     *
96     * @param string|null $subpage
97     * @param bool $forTransclusion
98     * @return Title|null
99     */
100    private function findTitleInternal( $subpage, $forTransclusion ) {
101        // base = title without the language code suffix
102        // provided = the title as it was given
103        $base = $provided = null;
104        if ( $subpage !== null ) {
105            $provided = Title::newFromText( $subpage );
106            $base = $provided;
107
108            if ( $provided && str_contains( $subpage, '/' ) ) {
109                $pos = strrpos( $subpage, '/' );
110                $basepage = substr( $subpage, 0, $pos );
111                $code = substr( $subpage, $pos + 1 );
112                if ( $code !== '' && $this->languageNameUtils->isKnownLanguageTag( $code ) ) {
113                    $base = Title::newFromText( $basepage );
114                }
115            }
116        }
117
118        if ( !$base || !$base->canExist() ) {
119            // No subpage provided or base page does not exist
120            return null;
121        }
122
123        $fragment = '';
124        if ( $base->isRedirect() ) {
125            $target = $this->redirectLookup->getRedirectTarget( $base );
126            if ( $target !== null ) {
127                $base = Title::newFromLinkTarget( $target );
128                // Preserve the fragment from the redirect target
129                $fragment = $base->getFragment();
130            }
131        }
132
133        $uiLang = $this->getLanguage();
134        $baseLang = $base->getPageLanguage();
135
136        // T309329: Always use subpages for transclusion
137        if ( !$forTransclusion && $baseLang->equals( $uiLang ) ) {
138            // Short circuit when the current UI language is the
139            // page's content language to avoid unnecessary page lookups.
140            return $base;
141        }
142
143        // Check for a subpage in the current UI language
144        $proposed = $base->getSubpage( $uiLang->getCode() );
145        if ( $proposed && $proposed->exists() ) {
146            if ( $fragment !== '' ) {
147                $proposed->setFragment( $fragment );
148            }
149            return $proposed;
150        }
151
152        // Explicit language code given and the page exists
153        if ( $provided !== $base && $provided->exists() ) {
154            // Not based on the redirect target, don't need the fragment
155            return $provided;
156        }
157
158        // Check for fallback languages specified by the UI language
159        $possibilities = $uiLang->getFallbackLanguages();
160        foreach ( $possibilities as $lang ) {
161            // $base already include fragments
162            // T309329: Always use subpages for transclusion
163            // T333187: Do not ignore base language page if matched
164            if ( !$forTransclusion && $lang === $baseLang->getCode() ) {
165                return $base;
166            }
167            // Look for subpages if is for transclusion or didn't match base page language
168            $proposed = $base->getSubpage( $lang );
169            if ( $proposed && $proposed->exists() ) {
170                if ( $fragment !== '' ) {
171                    $proposed->setFragment( $fragment );
172                }
173                return $proposed;
174            }
175        }
176
177        // When all else has failed, return the base page
178        return $base;
179    }
180
181    /**
182     * Target can identify a specific user's language preference.
183     *
184     * @see T109724
185     * @since 1.27
186     * @return bool
187     */
188    public function personallyIdentifiableTarget() {
189        return true;
190    }
191}
192
193/**
194 * Retain the old class name for backwards compatibility.
195 * @deprecated since 1.41
196 */
197class_alias( SpecialMyLanguage::class, 'SpecialMyLanguage' );