Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialLandingCheck
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 9
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
72
 determineLocalServerType
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 routeRedirect
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 externalRedirect
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 localRedirect
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
90
 setLocalServerType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getLocalServerType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This checks to see if a version of a landing page exists for the user's language and country.
5 * If not, it looks for a version localized for the user's language. If that doesn't exist either,
6 * it looks for the English version. If any of those exist, it then redirects the user.
7 */
8
9namespace MediaWiki\Extension\LandingCheck;
10
11use MediaWiki\Languages\LanguageFallback;
12use MediaWiki\Languages\LanguageNameUtils;
13use MediaWiki\SpecialPage\SpecialPage;
14use MediaWiki\Title\Title;
15use MediaWiki\Utils\UrlUtils;
16
17class SpecialLandingCheck extends SpecialPage {
18    /** @var LanguageNameUtils */
19    private $languageNameUtils;
20
21    /** @var LanguageFallback */
22    private $languageFallback;
23
24    /** @var UrlUtils */
25    private $urlUtils;
26
27    protected $localServerType = null;
28    /**
29     * If basic is set to true, do a local redirect, ignore priority, and don't pass tracking
30     * params. This is for non-fundraising links that just need localization.
31     *
32     * @var bool
33     */
34    protected $basic = false;
35
36    /**
37     * If anchor text is passed add that to the end of the created url so that it can be used to
38     * position the resulting page. This is currently used only for non-fundraising links that need
39     * localization and therefore is only checked if basic (above) is true.
40     *
41     * @var string|null
42     */
43    protected $anchor = null;
44
45    /**
46     * @param LanguageNameUtils $languageNameUtils
47     * @param LanguageFallback $languageFallback
48     * @param UrlUtils $urlUtils
49     */
50    public function __construct(
51        LanguageNameUtils $languageNameUtils,
52        LanguageFallback $languageFallback,
53        UrlUtils $urlUtils
54    ) {
55        // Register special page
56        parent::__construct( 'LandingCheck' );
57        $this->languageNameUtils = $languageNameUtils;
58        $this->languageFallback = $languageFallback;
59        $this->urlUtils = $urlUtils;
60    }
61
62    /**
63     * @param string $sub
64     */
65    public function execute( $sub ) {
66        global $wgPriorityCountries;
67        $request = $this->getRequest();
68
69        // If we have a subpage; assume it's a language like an internationalized page
70
71        $language = 'en';
72        $path = explode( '/', $sub );
73        if ( $this->languageNameUtils->isValidCode( $path[count( $path ) - 1] ) ) {
74            $language = $sub;
75        }
76
77        // Pull in query string parameters
78        $language = $request->getVal( 'language', $language );
79        $this->basic = $request->getBool( 'basic' );
80        $country = $request->getVal( 'country' );
81        $this->anchor = $request->getVal( 'anchor' );
82
83        // if the language is false-ish, set to default
84        if ( !$language ) {
85            $language = 'en';
86        }
87
88        // if it's not a supported language, but the section before a
89        // dash or underscore is, use that
90        if ( !$this->languageNameUtils->isSupportedLanguage( $language ) ) {
91            $parts = preg_split( '/[-_]/', $language );
92            if ( $this->languageNameUtils->isSupportedLanguage( $parts[0] ) ) {
93                $language = $parts[0];
94            }
95        }
96
97        // Use the GeoIP cookie if available.
98        if ( !$country ) {
99            $geoip = $request->getCookie( 'GeoIP', '' );
100            if ( $geoip ) {
101                $components = explode( ':', $geoip );
102                $country = $components[0];
103            }
104        }
105
106        if ( !$country ) {
107            $country = 'US'; // Default
108        }
109
110        // determine if we are fulfilling a request for a priority country
111        $priority = in_array( $country, $wgPriorityCountries );
112
113        // handle the actual redirect
114        $this->routeRedirect( $country, $language, $priority );
115    }
116
117    /**
118     * Determine whether this server is configured as the priority or normal server
119     *
120     * If this is neither the priority nor normal server, assumes 'local' - meaning
121     * this server should be handling the request.
122     * @return string
123     */
124    public function determineLocalServerType() {
125        global $wgServer, $wgLandingCheckPriorityURLBase, $wgLandingCheckNormalURLBase;
126
127        $localServerDetails = $this->urlUtils->parse( $wgServer );
128
129        if ( $localServerDetails === null ) {
130            return 'local';
131        }
132
133        if ( $wgLandingCheckPriorityURLBase !== null ) {
134            $priorityServerDetails = $this->urlUtils->parse( $wgLandingCheckPriorityURLBase );
135            if ( $priorityServerDetails !== null
136                && $localServerDetails[ 'host' ] === $priorityServerDetails[ 'host' ]
137            ) {
138                return 'priority';
139            }
140        }
141
142        if ( $wgLandingCheckNormalURLBase !== null ) {
143            $normalServerDetails = $this->urlUtils->parse( $wgLandingCheckNormalURLBase );
144            if ( $normalServerDetails !== null
145                && $localServerDetails[ 'host' ] === $normalServerDetails[ 'host' ]
146            ) {
147                return 'normal';
148            }
149        }
150
151        return 'local';
152    }
153
154    /**
155     * Route the request to the appropriate redirect method
156     * @param string $country
157     * @param string $language
158     * @param bool $priority Whether or not we handle this request on behalf of a priority country
159     */
160    public function routeRedirect( $country, $language, $priority ) {
161        $localServerType = $this->getLocalServerType();
162
163        if ( $this->basic ) {
164            $this->localRedirect( $country, $language, false );
165
166        } elseif ( $localServerType == 'local' ) {
167            $this->localRedirect( $country, $language, $priority );
168
169        } elseif ( $priority && $localServerType == 'priority' ) {
170            $this->localRedirect( $country, $language, $priority );
171
172        } elseif ( !$priority && $localServerType == 'normal' ) {
173            $this->localRedirect( $country, $language, $priority );
174
175        } else {
176            $this->externalRedirect( $priority );
177        }
178    }
179
180    /**
181     * Handle an external redirect
182     *
183     * The external redirect should point to another instance of LandingCheck
184     * which will ultimately handle the request.
185     * @param bool $priority
186     */
187    public function externalRedirect( $priority ) {
188        global $wgLandingCheckPriorityURLBase, $wgLandingCheckNormalURLBase;
189
190        if ( $priority ) {
191            $urlBase = $wgLandingCheckPriorityURLBase;
192
193        } else {
194            $urlBase = $wgLandingCheckNormalURLBase;
195        }
196
197        $query = $this->getRequest()->getValues();
198        unset( $query[ 'title' ] );
199
200        // @phan-suppress-next-line PhanTypeMismatchArgument urlBase always not null
201        $url = wfAppendQuery( $urlBase, $query );
202        $this->getOutput()->redirect( $url );
203    }
204
205    /**
206     * Handle local redirect
207     * @param string $country
208     * @param string $language
209     * @param bool $priority Whether or not we handle this request on behalf of a priority country
210     */
211    public function localRedirect( $country, $language, $priority = false ) {
212        $out = $this->getOutput();
213        $request = $this->getRequest();
214        $landingPage = $request->getVal( 'landing_page', 'Donate' );
215
216        /**
217         * Construct new query string for tracking
218         *
219         * Note that both 'language' and 'uselang' get set to
220         *     $request->getVal( 'language', 'en' )
221         * This is wacky, yet by design! This is a unique oddity to fundraising
222         * stuff, but CentralNotice converts wgUserLanguage to 'language' rather than
223         * 'uselang'. Ultimately, this is something that should probably be rectified
224         * in CentralNotice. Until then, this is what we've got.
225         */
226        $tracking = wfArrayToCgi( [
227            'utm_source' => $request->getVal( 'utm_source' ),
228            'utm_medium' => $request->getVal( 'utm_medium' ),
229            'utm_campaign' => $request->getVal( 'utm_campaign' ),
230            'utm_key' => $request->getVal( 'utm_key' ),
231            'language' => $language,
232            'uselang' => $language, // for {{int:xxx}} rendering
233            'country' => $country,
234            'referrer' => $request->getHeader( 'referer' )
235        ] );
236
237        if ( $priority ) {
238            // Build array of landing pages to check for
239            $targetTexts = [
240                $landingPage . '/' . $country . '/' . $language,
241                $landingPage . '/' . $country,
242                $landingPage . '/' . $language
243            ];
244        } else {
245            // Build array of landing pages to check for
246            $targetTexts = [
247                $landingPage . '/' . $language . '/' . $country,
248                $landingPage . '/' . $language
249            ];
250            // Add fallback languages
251            $fallbacks = $this->languageFallback->getAll( $language );
252            foreach ( $fallbacks as $fallback ) {
253                $targetTexts[] = $landingPage . '/' . $fallback;
254            }
255        }
256
257        // Go through the possible landing pages and redirect the user as soon as one is found to exist
258        foreach ( $targetTexts as $targetText ) {
259            $target = Title::newFromText( $targetText );
260            if ( $target && $target->isKnown() && $target->getNamespace() == NS_MAIN ) {
261                if ( $this->basic ) {
262                    if ( $this->anchor !== null ) {
263                        $out->redirect( $target->getLocalURL() . '#' . $this->anchor );
264                    } else {
265                        $out->redirect( $target->getLocalURL() );
266                    }
267                } else {
268                    $out->redirect( $target->getLocalURL( $tracking ) );
269                }
270                return;
271            }
272        }
273
274        // Output a simple error message if no pages were found
275        $this->setHeaders();
276        $this->outputHeader();
277        $out->addWikiMsg( 'landingcheck-nopage' );
278    }
279
280    /**
281     * Setter for $this->localServerType
282     * @param string|null $type
283     */
284    public function setLocalServerType( $type = null ) {
285        if ( !$type ) {
286            $this->localServerType = $this->determineLocalServerType();
287        } else {
288            $this->localServerType = $type;
289        }
290    }
291
292    /**
293     * Getter for $this->localServerType
294     * @return string
295     */
296    public function getLocalServerType() {
297        if ( !$this->localServerType ) {
298            $this->setLocalServerType();
299        }
300        return $this->localServerType;
301    }
302
303    protected function getGroupName() {
304        return 'contribution';
305    }
306}