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