Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
17.04% covered (danger)
17.04%
84 / 493
16.67% covered (danger)
16.67%
8 / 48
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikimediaIncubator
17.04% covered (danger)
17.04%
84 / 493
16.67% covered (danger)
16.67%
8 / 48
21021.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onUserGetDefaultOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onLoadUserOptions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
12
 getTestWikiLanguages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateCodePreference
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 filterCodePreference
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 validateLanguageCode
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 analyzePrefix
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
16
 validatePrefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUrlParam
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
21.52
 getProject
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
15.27
 isContentProject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 displayPrefix
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
8.63
 selectedIncubationWiki
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 displayPrefixedTitle
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onMagicWordwgVariableIDs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onParserGetVariableValueSwitch
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 shouldWeShowUnprefixedError
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
7.18
 onGetUserPermissionsErrors
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
132
 onMovePageIsValidMove
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onContributionsToolLinks
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getConf
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 getExistingWikis
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canWeCheckDB
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getDBClosedWikis
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getDBState
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 onShowMissingArticle
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
110
 onShowMissingArticleForInfoPages
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderParserFunction
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 onEditFormPreloadText
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getSubdomain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getMainPage
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 onMediaWikiPerformAction
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 onTitleIsAlwaysKnown
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 onPageContentLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 onArticleParserOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 onSpecialSearchCreateLink
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
 onSpecialSearchPowerBox
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onSpecialSearchSetupEngine
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 preg_quote_slash
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeExternalLinkText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onGetDefaultSortkey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 generateHtmlTitle
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
20
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Main class of the WikimediaIncubator extension.
4 * Implement test wiki preference, magic word and prefix check on edit page,
5 * and contains general functions for other classes.
6 *
7 * @file
8 * @ingroup Extensions
9 * @author Robin Pepermans (SPQRobin)
10 * @author Jon Harald Søby
11 */
12
13namespace MediaWiki\Extension\WikimediaIncubator;
14
15use Article;
16use HtmlArmor;
17use InvalidArgumentException;
18use Language;
19use MediaWiki\Actions\ActionEntryPoint;
20use MediaWiki\Content\Hook\PageContentLanguageHook;
21use MediaWiki\Hook\BeforePageDisplayHook;
22use MediaWiki\Hook\ContributionsToolLinksHook;
23use MediaWiki\Hook\EditFormPreloadTextHook;
24use MediaWiki\Hook\GetDefaultSortkeyHook;
25use MediaWiki\Hook\MagicWordwgVariableIDsHook;
26use MediaWiki\Hook\MakeGlobalVariablesScriptHook;
27use MediaWiki\Hook\MediaWikiPerformActionHook;
28use MediaWiki\Hook\MovePageIsValidMoveHook;
29use MediaWiki\Hook\ParserFirstCallInitHook;
30use MediaWiki\Hook\ParserGetVariableValueSwitchHook;
31use MediaWiki\Hook\SpecialSearchCreateLinkHook;
32use MediaWiki\Hook\SpecialSearchSetupEngineHook;
33use MediaWiki\Hook\TitleIsAlwaysKnownHook;
34use MediaWiki\Html\Html;
35use MediaWiki\Languages\LanguageFactory;
36use MediaWiki\Languages\LanguageNameUtils;
37use MediaWiki\Linker\Linker;
38use MediaWiki\MediaWikiServices;
39use MediaWiki\Output\OutputPage;
40use MediaWiki\Page\Hook\ArticleParserOptionsHook;
41use MediaWiki\Page\Hook\ShowMissingArticleHook;
42use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
43use MediaWiki\Preferences\Hook\GetPreferencesHook;
44use MediaWiki\Request\WebRequest;
45use MediaWiki\Search\Hook\SpecialSearchPowerBoxHook;
46use MediaWiki\SpecialPage\SpecialPage;
47use MediaWiki\Specials\SpecialSearch;
48use MediaWiki\Title\Title;
49use MediaWiki\User\Hook\UserGetDefaultOptionsHook;
50use MediaWiki\User\Options\Hook\LoadUserOptionsHook;
51use MediaWiki\User\User;
52use MediaWiki\User\UserIdentity;
53use Parser;
54use ParserOptions;
55use RequestContext;
56use SearchEngine;
57use Skin;
58use Xml;
59
60class WikimediaIncubator implements
61    ContributionsToolLinksHook,
62    GetPreferencesHook,
63    UserGetDefaultOptionsHook,
64    LoadUserOptionsHook,
65    MagicWordwgVariableIDsHook,
66    ParserGetVariableValueSwitchHook,
67    GetUserPermissionsErrorsHook,
68    MovePageIsValidMoveHook,
69    ShowMissingArticleHook,
70    EditFormPreloadTextHook,
71    MediaWikiPerformActionHook,
72    TitleIsAlwaysKnownHook,
73    ParserFirstCallInitHook,
74    PageContentLanguageHook,
75    ArticleParserOptionsHook,
76    MakeGlobalVariablesScriptHook,
77    SpecialSearchCreateLinkHook,
78    SpecialSearchPowerBoxHook,
79    SpecialSearchSetupEngineHook,
80    GetDefaultSortkeyHook,
81    BeforePageDisplayHook
82{
83    // Used in places that expect the name of a project when no
84    // project has been selected.
85    private const NO_PROJECT_SELECTED = 'none';
86
87    private LanguageFactory $languageFactory;
88
89    /**
90     * Initialize this hook implementation class with needed services.
91     */
92    public function __construct( LanguageFactory $languageFactory ) {
93        $this->languageFactory = $languageFactory;
94    }
95
96    /**
97     * Add default preference
98     * @param array &$defOpt
99     */
100    public function onUserGetDefaultOptions( &$defOpt ) {
101        global $wmincPref;
102
103        $defOpt[$wmincPref . '-project'] = self::NO_PROJECT_SELECTED;
104    }
105
106    /**
107     * Fallback to no project selected for users without a valid language code.
108     *
109     * @param UserIdentity $user
110     * @param array &$options
111     */
112    public function onLoadUserOptions( UserIdentity $user, array &$options ): void {
113        global $wmincPref;
114
115        $langCode = $options[$wmincPref . '-code'] ?? '';
116        if ( self::isContentProject( $user, $options[$wmincPref . '-project'] )
117            && !self::validateLanguageCode( $langCode )
118        ) {
119            $options[$wmincPref . '-project'] = self::NO_PROJECT_SELECTED;
120        }
121    }
122
123    /**
124     * Add preferences
125     * @param User $user
126     * @param array &$preferences
127     */
128    public function onGetPreferences( $user, &$preferences ) {
129        global $wmincPref, $wmincProjects, $wmincProjectSite, $wmincLangCodeLength;
130
131        $preferences['language']['help-message'] = 'wminc-prefinfo-language';
132
133        $projectList = array_combine( array_keys( $wmincProjects ), array_column( $wmincProjects, 'name' ) );
134        foreach ( $wmincProjects as $projectCode => $metadata ) {
135            if ( $metadata['sister'] ) {
136                unset( $projectList[$projectCode] );
137            }
138        }
139        $projectList = array_flip( $projectList );
140
141        $prefinsert = [
142            $wmincPref . '-project' => [
143                'type' => 'select',
144                'options' =>
145                    [ wfMessage( 'wminc-testwiki-none' )->plain() => self::NO_PROJECT_SELECTED ] +
146                    $projectList +
147                    [ wfMessage( 'wminc-testwiki-site' )->plain() => $wmincProjectSite['short'] ],
148                'section' => 'personal/i18n',
149                'label-message' => 'wminc-testwiki',
150                'id' => $wmincPref . '-project',
151                'help-message' => 'wminc-prefinfo-project',
152            ],
153            $wmincPref . '-code' => [
154                'type' => 'text',
155                'section' => 'personal/i18n',
156                'label-message' => 'wminc-testwiki-code',
157                'id' => $wmincPref . '-code',
158                'maxlength' => (int)$wmincLangCodeLength,
159                'size' => (int)$wmincLangCodeLength,
160                'help' => wfMessage( 'wminc-prefinfo-code' )->parse() .
161                    self::getTestWikiLanguages(),
162                'list' => 'wminc-testwiki-codelist',
163                'validation-callback' => static function ( $input, $alldata ) use ( $user ) {
164                    return WikimediaIncubator::validateCodePreference( $user, $input, $alldata );
165                },
166                'filter-callback' => [ self::class, 'filterCodePreference' ],
167            ],
168        ];
169
170        $preferences = wfArrayInsertAfter( $preferences, $prefinsert, 'language' );
171    }
172
173    /**
174     * Add a datalist with languages in MediaWiki,
175     * to suggest common language codes
176     * @return string HTML
177     */
178    private static function getTestWikiLanguages() {
179        $list = MediaWikiServices::getInstance()->getLanguageNameUtils()
180            ->getLanguageNames( LanguageNameUtils::AUTONYMS, LanguageNameUtils::ALL );
181        $t = '<datalist id="wminc-testwiki-codelist">' . "\n";
182        foreach ( $list as $code => $name ) {
183            $t .= Xml::element( 'option', [ 'value' => $code ],
184                $code . ' - ' . $name ) . "\n";
185        }
186        $t .= '</datalist>';
187        return $t;
188    }
189
190    /**
191     * For the preferences above
192     * @param User $user
193     * @param string $input
194     * @param array $alldata
195     * @return string|true
196     */
197    public static function validateCodePreference( User $user, $input, $alldata ) {
198        global $wmincPref;
199        # If the user selected a project that NEEDS a language code,
200        # but the user DID NOT enter a valid language code, give an error
201        $filteredInput = self::filterCodePreference( $input );
202        if ( isset( $alldata[$wmincPref . '-project'] )
203            && self::isContentProject( $user, $alldata[$wmincPref . '-project'] )
204            && !self::validateLanguageCode( $filteredInput )
205        ) {
206            return Xml::element( 'span', [ 'class' => 'error' ],
207                wfMessage( 'wminc-prefinfo-error' )->plain() );
208        }
209        return true;
210    }
211
212    /**
213     * For the preferences above
214     * @param string|null $input
215     * @return string|true
216     */
217    public static function filterCodePreference( $input ) {
218        return $input === null ? '' : trim( strtolower( $input ) );
219    }
220
221    /**
222     * This validates a given language code.
223     * Only "xx[x]" and "xx[x]-x[xxxxxxxx]" are allowed.
224     * @param string $code
225     * @return bool
226     */
227    public static function validateLanguageCode( $code ) {
228        global $wmincLangCodeLength;
229        if ( strlen( $code ) > $wmincLangCodeLength ) {
230            return false;
231        }
232        if ( $code == 'be-x-old' ) {
233            return true; # one exception... waiting to be renamed to be-tarask
234        }
235        return (bool)preg_match( '/^[a-z][a-z][a-z]?(-[a-z]+)?$/', $code );
236    }
237
238    /**
239     * This validates a full prefix in a given title.
240     * It gives an array with the project and language code, containing
241     * the key 'error' if it is invalid.
242     * Use validatePrefix() if you just want true or false.
243     * Use displayPrefixedTitle() to make a prefix page title.
244     *
245     * @param Title|string $input The title to check (if string, don't include namespace)
246     * @param bool $onlyInfoPage Whether to validate only the prefix, or
247     * also allow other text within the page title (Wx/xxx vs Wx/xxx/Text)
248     * @param bool $allowSister Whether to allow sister projects when checking
249     * for the project code.
250     * @return array with 'error' or 'project', 'lang', 'prefix' and
251     *                     optionally 'realtitle'
252     */
253    public static function analyzePrefix( $input, $onlyInfoPage = false, $allowSister = false ) {
254        $data = [ 'error' => null ];
255        if ( $input instanceof Title ) {
256            global $wmincTestWikiNamespaces;
257            $title = $input->getText();
258            if ( !in_array( $input->getNamespace(), $wmincTestWikiNamespaces ) ) {
259                return [ 'error' => 'notestwikinamespace' ];
260            }
261            if ( $onlyInfoPage && $input->getNamespace() != NS_MAIN ) {
262                # Info pages are only in the main NS
263                return [ 'error' => 'nomainnamespace' ];
264            }
265        } else {
266            $title = $input;
267        }
268        # split title into parts
269        $titleparts = explode( '/', $title );
270        # Test if array, has a language code and project code has proper length
271        if ( !isset( $titleparts[1] ) || strlen( $titleparts[0] ) != 2 ) {
272            $data['error'] = 'noslash';
273        } else {
274            # get the x from Wx/...
275            $data['project'] = $titleparts[0][1] ?? '';
276            $data['lang'] = $titleparts[1]; # language code
277            $data['prefix'] = 'W' . $data['project'] . '/' . $data['lang'];
278            # check language code
279            if ( !self::validateLanguageCode( $data['lang'] ) ) {
280                $data['error'] = 'invalidlangcode';
281            }
282        }
283        global $wmincProjects;
284        $listProjects = [];
285        foreach ( $wmincProjects as $projectCode => $metadata ) {
286            if ( $metadata['sister'] && $allowSister ) {
287                array_push( $listProjects, $projectCode );
288            } elseif ( !$metadata['sister'] ) {
289                array_push( $listProjects, $projectCode );
290            }
291        }
292        $listProjects = array_map( [ __CLASS__, 'preg_quote_slash' ], $listProjects );
293        if ( !preg_match( '/^W(' . implode( '|', $listProjects ) . ')\/[a-z-]+' .
294            ( $onlyInfoPage ? '$/' : '(\/.+)?$/' ), $title ) ) {
295            $data['error'] = 'invalidprefix';
296        }
297        if ( !$onlyInfoPage && $data['error'] != 'invalidprefix' ) { # there is a Page_title
298            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
299            $prefixn = strlen( $data['prefix'] . '/' ); # number of chars in prefix
300            # get Page_title from Wx/xx/Page_title
301            $data['realtitle'] = substr( $title, $prefixn );
302        }
303        return $data; # return an array with information
304    }
305
306    /**
307     * This returns simply true or false based on analyzePrefix().
308     * @param string|Title $title
309     * @param bool $onlyprefix
310     * @return bool
311     */
312    public static function validatePrefix( $title, $onlyprefix = false ) {
313        $data = self::analyzePrefix( $title, $onlyprefix );
314        return !$data['error'];
315    }
316
317    /**
318     * Get &testwiki=wx/xx and validate that prefix.
319     * Returns the array of analyzePrefix() on success.
320     * @return array|false
321     */
322    public static function getUrlParam() {
323        global $wgRequest;
324        $urlParam = $wgRequest->getVal( 'testwiki' );
325        if ( !$urlParam ) {
326            return false;
327        }
328        $val = self::analyzePrefix( ucfirst( $urlParam ), true );
329        if ( $val['error'] || !isset( $val['project'] ) || !isset( $val['lang'] )
330            || !$val['project'] || !$val['lang'] ) {
331            return false;
332        }
333        $val['prefix'] = strtolower( $val['prefix'] );
334        return $val;
335    }
336
337    /**
338     * Returns the project code or name if the given project code or name (or preference by default)
339     * is one of the projects using the format Wx/xxx (as defined in $wmincProjects)
340     * Returns false if it is not valid.
341     * @param UserIdentity $user
342     * @param string $project The project code
343     * @param bool $returnName Whether to return the project name instead of the code
344     * @param bool $includeSister Whether to include sister projects
345     * @return string|false
346     */
347    public static function getProject(
348        UserIdentity $user,
349        $project = '',
350        $returnName = false,
351        $includeSister = false
352    ) {
353        global $wmincPref, $wmincProjects;
354        $url = self::getUrlParam();
355        if ( $project ) {
356            $r = $project; # Precedence to given value
357        } elseif ( $url ) {
358            $r = $url['project']; # Otherwise URL &testwiki= if set
359        } else {
360            $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
361            $r = $userOptionsLookup->getOption( $user, $wmincPref . '-project' ); # Defaults to preference
362        }
363        $projects = $wmincProjects;
364        foreach ( $projects as $projectCode => $metadata ) {
365            if ( !$includeSister && $metadata['sister'] ) {
366                unset( $projects[$projectCode] );
367            }
368        }
369        if ( array_key_exists( $r, $projects ) ) {
370            # If a code is given, return what is wanted
371            return $returnName ? $projects[$r]['name'] : $r;
372        } elseif ( array_search( $r, array_column( $projects, 'name' ) ) ) {
373            # If a name is given, return what is wanted
374            return $returnName ? $r : array_search( $r, array_column( $projects, 'name' ) );
375        }
376        # Unknown code or name given -> false
377        return false;
378    }
379
380    /**
381     * Returns a simple boolean based on getProject()
382     * @param UserIdentity $user
383     * @param string $project
384     * @param bool $returnName
385     * @param bool $includeSister
386     * @return bool
387     */
388    public static function isContentProject(
389        UserIdentity $user,
390        $project = '',
391        $returnName = false,
392        $includeSister = false
393    ) {
394        return (bool)self::getProject( $user, $project, $returnName, $includeSister );
395    }
396
397    /**
398     * display the prefix by the given project and code
399     * (or the URL &testwiki= or user preference if no parameters are given)
400     * @param string $project
401     * @param string $code
402     * @param bool $allowSister
403     * @return string
404     */
405    public static function displayPrefix( $project = '', $code = '', $allowSister = false ) {
406        global $wmincProjects;
407        $user = RequestContext::getMain()->getUser(); // A lot of callers lack context
408        if ( $project && $code ) {
409            $projectvalue = $project;
410            $codevalue = $code;
411        } else {
412            global $wmincPref;
413            $url = self::getUrlParam();
414            $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
415            $projectPref = $userOptionsLookup->getOption( $user, $wmincPref . '-project' );
416            $codePref = $userOptionsLookup->getOption( $user, $wmincPref . '-code' );
417            $projectvalue = ( $url ? $url['project'] : $projectPref );
418            $codevalue = ( $url ? $url['lang'] : $codePref );
419        }
420        $sister = $allowSister && $wmincProjects[$projectvalue]['sister'];
421        if ( self::isContentProject( $user, $projectvalue ) || $sister ) {
422            // if parameters are set OR it falls back to user pref and
423            // he has a content project pref set  -> return the prefix
424            return 'W' . $projectvalue . '/' . $codevalue; // return the prefix
425        } else {
426            // fall back to user pref with NO content pref set
427            // -> still provide the value (probably 'none' or 'inc')
428            return $projectvalue;
429        }
430    }
431
432    /**
433     * @return string|null The prefix used for the selected wiki under incubation,
434     *  or null when no wiki is selected.
435     */
436    private static function selectedIncubationWiki() {
437        global $wmincProjectSite;
438
439        $prefix = self::displayPrefix();
440        // These values have special meanings and are not actual wikis under incubation.
441        if ( $prefix === self::NO_PROJECT_SELECTED || $prefix === $wmincProjectSite['short'] ) {
442            return null;
443        }
444        return $prefix;
445    }
446
447    /**
448     * Makes a full prefixed title of a given page title and namespace
449     * @param string $title
450     * @param int $ns numeric value of namespace
451     * @param bool $translateNs whether the namespaces should be in the user's language
452     * @return Title
453     */
454    public static function displayPrefixedTitle( $title, $ns = 0, $translateNs = true ) {
455        global $wmincTestWikiNamespaces;
456
457        $lang = RequestContext::getMain()->getLanguage();
458        if ( !$translateNs ) {
459            $lang = MediaWikiServices::getInstance()->getLanguageFactory()
460                ->getLanguage( 'en' );
461        }
462
463        if ( in_array( $ns, $wmincTestWikiNamespaces ) ) {
464            /* Standard namespace as defined by
465            * $wmincTestWikiNamespaces, so use format:
466            * TITLE + NS => NS:Wx/xxx/TITLE
467            */
468            return Title::makeTitleSafe( $ns, self::displayPrefix() . '/' . $title );
469        }
470
471        /* Non-standard namespace, so use format:
472        * TITLE + NS => Wx/xxx/NS:TITLE
473        * (with localized namespace name)
474        */
475        return Title::makeTitleSafe( 0, self::displayPrefix() . '/' .
476            $lang->getNsText( $ns ) . ':' . $title );
477    }
478
479    public function onMagicWordwgVariableIDs( &$magicWords ) {
480        $magicWords[] = 'usertestwiki';
481    }
482
483    public function onParserGetVariableValueSwitch( $parser, &$cache, $magicWordId, &$ret, $frame ) {
484        if ( $magicWordId === 'usertestwiki' ) {
485            $p = self::displayPrefix();
486            $ret = $cache[$magicWordId] = $p ?: self::NO_PROJECT_SELECTED;
487        }
488    }
489
490    /**
491     * Whether we should show an error message that the page is unprefixed
492     * @param Title $title Title object
493     * @return bool
494     */
495    public static function shouldWeShowUnprefixedError( $title ) {
496        global $wmincTestWikiNamespaces, $wmincProjectSite, $wmincPseudoCategoryNSes;
497        $prefixdata = self::analyzePrefix( $title->getText() );
498        $ns = $title->getNamespace();
499        $categories = array_map( [ __CLASS__, 'preg_quote_slash' ], $wmincPseudoCategoryNSes );
500        if ( !$prefixdata['error'] ) {
501            # no error in prefix -> no error to show
502            return false;
503        } elseif ( self::displayPrefix() == $wmincProjectSite['short'] ) {
504            # If user has "project" (Incubator) as test wiki preference, it isn't needed to check
505            return false;
506        } elseif ( !in_array( $ns, $wmincTestWikiNamespaces ) ) {
507            # OK if it's not in one of the content namespaces
508            return false;
509        } elseif ( ( $ns == NS_CATEGORY || $ns == NS_CATEGORY_TALK ) &&
510            preg_match( '/^(' . implode( '|', $categories ) . '):.+$/', $title->getText() ) ) {
511            # allowed unprefixed categories
512            return false;
513        }
514        return true;
515    }
516
517    /**
518     * This does several things:
519     * Disables editing pages belonging to existing wikis (+ shows message)
520     * Disables creating an unprefixed page (+ shows error message)
521     * See also: WikimediaIncubator::onShowMissingArticle()
522     * @param Title $title
523     * @param User $user
524     * @param string $action
525     * @param array &$result
526     * @return bool
527     */
528    public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
529        $titletext = $title->getText();
530        $prefixdata = self::analyzePrefix( $titletext );
531
532        if ( self::getDBState( $prefixdata ) == 'existing' ) {
533            if ( $prefixdata['prefix'] == $titletext &&
534                ( $title->exists() || $user->isAllowed( 'editinterface' ) ) ) {
535                # if it's an info page, allow if the page exists or the user has 'editinterface' right
536                return true;
537            }
538            # no permission if the wiki already exists
539            $link = self::getSubdomain( $user, $prefixdata['lang'],
540                $prefixdata['project'], ( $title->getNsText() ? $title->getNsText() . ':' : '' ) .
541                ( $prefixdata['realtitle'] ?? $titletext )
542            );
543            # faking external link to support prot-rel URLs
544            $link = "[$link " . self::makeExternalLinkText( $link ) . "]";
545            $result = [ 'wminc-error-wiki-exists', $link ];
546            return $action != 'edit';
547        }
548
549        if ( !self::shouldWeShowUnprefixedError( $title ) || $action != 'edit' || $title->exists() ) {
550            # only check if needed & if on page creation
551            return true;
552        } elseif ( $prefixdata['error'] == 'invalidlangcode' ) {
553            $result = [ 'wminc-error-wronglangcode', $prefixdata['lang'] ];
554        } elseif ( self::isContentProject( $user ) ) {
555            # If the user has a test wiki pref, suggest a page title with prefix
556            $suggesttitle = $prefixdata['realtitle'] ?? $titletext;
557            $suggest = self::displayPrefixedTitle( $suggesttitle, $title->getNamespace() );
558            # Suggest to create a prefixed page
559            $result = [ 'wminc-error-unprefixed-suggest', $suggest ];
560        } else {
561            $result = [ 'wminc-error-unprefixed' ];
562        }
563        return $action != 'edit';
564    }
565
566    public function onMovePageIsValidMove( $oldTitle, $newTitle, $status ) {
567        if ( self::shouldWeShowUnprefixedError( $newTitle ) ) {
568            # there should be an error with the new page title
569            $status->fatal( 'wminc-error-move-unprefixed' );
570            return false;
571        }
572
573        return true;
574    }
575
576    /**
577     * Add a link to Special:ViewUserLang from Special:Contributions/USERNAME
578     * if the user has 'viewuserlang' permission
579     * Based on code from extension LookupUser made by Tim Starling
580     * @param int $id
581     * @param Title $nt
582     * @param array &$links
583     * @param SpecialPage $sp
584     */
585    public function onContributionsToolLinks( $id, Title $nt, array &$links, SpecialPage $sp ) {
586        if ( $sp->getUser()->isAllowed( 'viewuserlang' ) ) {
587            $user = $nt->getText();
588            $links['viewuserlang'] = $sp->getLinkRenderer()->makeKnownLink(
589                SpecialPage::getTitleFor( 'ViewUserLang', $user ),
590                $sp->msg( 'wminc-viewuserlang' )->text()
591            );
592        }
593    }
594
595    /**
596     * Convenience function to access $wgConf->get()
597     * @param UserIdentity $user
598     * @param string $setting the setting to call
599     * @param string $lang the language code
600     * @param string $project the project code or name
601     * @return mixed the setting from $wgConf->settings
602     */
603    public static function getConf( UserIdentity $user, $setting, $lang, $project ) {
604        if ( !self::canWeCheckDB() ) {
605            return false;
606        }
607        global $wmincProjects, $wgConf;
608        $wgConf->loadFullData();
609        $lang = strtolower( $lang );
610        $langHyphen = str_replace( '_', '-', $lang );
611        $langUnderscore = str_replace( '-', '_', $lang );
612        $projectName = self::getProject( $user, $project, true, true );
613        $projectCode = self::getProject( $user, $project, false, true );
614        if ( !$projectCode ) {
615            global $wmincMultilingualProjects;
616            $projectCode = array_search( $project, $wmincMultilingualProjects );
617        }
618        $site = strtolower( $projectName );
619        $params = [
620            'lang' => $langHyphen,
621            'site' => $site,
622        ];
623        $dbSuffix = $wmincProjects[$projectCode]['dbsuffix'] ?? $site;
624        $wikiTag = $wmincProjects[$projectCode]['wikitag'] ?? $site;
625        return $wgConf->get( $setting, $langUnderscore . $dbSuffix, $wikiTag, $params );
626    }
627
628    private static function getExistingWikis(): array {
629        global $wmincExistingWikis, $wgLocalDatabases;
630        // This configuration mainly existed for testing and local development.
631        // It is not used by Wikimedia in production.
632        return $wmincExistingWikis ?? $wgLocalDatabases;
633    }
634
635    /**
636     * Do we know the databases of the existing wikis?
637     * @return bool
638     */
639    public static function canWeCheckDB() {
640        global $wmincProjects;
641        return (bool)array_column( $wmincProjects, 'dbsuffix' );
642    }
643
644    /**
645     * Given an incubator testwiki prefix, get the database name of the
646     * corresponding wiki, whether it exists or not
647     * @param array $prefix Array from WikimediaIncubator::analyzePrefix();
648     * @return false|string
649     */
650    public static function getDB( $prefix ) {
651        if ( !self::canWeCheckDB() ) {
652            return false;
653        } elseif ( !$prefix || $prefix['error'] ) {
654            return false; # shouldn't be, but you never know
655        }
656        global $wmincProjects, $wgDummyLanguageCodes;
657        $dbLang = str_replace( '-', '_', $prefix['lang'] );
658        $project = $prefix['project'];
659        $dbProject = $wmincProjects[$project]['dbsuffix'] ?? $project;
660        $redirectcode = array_search( $prefix['lang'], $wgDummyLanguageCodes );
661        if ( $redirectcode ) {
662            $prefix['lang'] = $redirectcode;
663        }
664        return strtolower( $dbLang . $dbProject );
665    }
666
667    /**
668     * @return false|array Array with closed databases
669     */
670    public static function getDBClosedWikis() {
671        global $wmincClosedWikis;
672        if ( !self::canWeCheckDB() || !$wmincClosedWikis ) {
673            return false;
674        }
675        # Is probably a file, but it might be that an array is given
676        return is_array( $wmincClosedWikis ) ? $wmincClosedWikis :
677            array_map( 'trim', file( $wmincClosedWikis ) );
678    }
679
680    /**
681     * @param array $prefix Array from WikimediaIncubator::analyzePrefix();
682     * @return false|string 'existing' 'closed' 'missing'
683     */
684    public static function getDBState( $prefix ) {
685        $db = self::getDB( $prefix );
686        if ( !$db ) {
687            return false;
688        }
689        $existingWikis = self::getExistingWikis();
690        $closed = self::getDBClosedWikis();
691        if ( !in_array( $db, $existingWikis ) ) {
692            return 'missing'; # not in the list
693        } elseif ( is_array( $closed ) && in_array( $db, $closed ) ) {
694            return 'closed'; # in the list of closed wikis
695        }
696        return 'existing';
697    }
698
699    /**
700     * If existing wiki: show message or redirect if &testwiki is set to that
701     * Missing article on Wx/xx info pages: show welcome page
702     * See also: WikimediaIncubator::onGetUserPermissionsErrors()
703     * @param Article $article
704     * @return True
705     */
706    public function onShowMissingArticle( $article ) {
707        $title = $article->getTitle();
708        $prefix = self::analyzePrefix(
709            $title,
710            true, /* only info pages */
711            true /* also sister projects */
712        );
713        if ( !$prefix['error'] ) {
714            self::onShowMissingArticleForInfoPages( $article, $prefix );
715            return true;
716        }
717
718        $out = $article->getContext()->getOutput();
719        global $wmincProjects;
720        $prefix2 = self::analyzePrefix( $title, false, true );
721        $p = $prefix2['project'] ?? '';
722        $user = $article->getContext()->getUser();
723        if ( self::getDBState( $prefix2 ) == 'existing' ) {
724            $link = self::getSubdomain( $user, $prefix2['lang'], $p,
725                ( $title->getNsText() ? $title->getNsText() . ':' : '' ) .
726                ( $prefix2['realtitle'] ?? $title->getText() )
727            );
728            if ( self::displayPrefix() == $prefix2['prefix'] ) {
729                # Redirect to the existing wiki if the user has this wiki as preference
730                $out->redirect( $link );
731                return true;
732            } else {
733                # Show a link to the existing wiki
734                $showLink = self::makeExternalLinkText( $link, true );
735                $out->addHtml( '<div class="wminc-wiki-exists">' .
736                    wfMessage( 'wminc-error-wiki-exists' )->rawParams( $showLink )->escaped() .
737                '</div>' );
738            }
739        } elseif ( array_key_exists( $p, $wmincProjects ) && $wmincProjects[$p]['sister'] ) {
740            # A sister project is not hosted here, so direct the user to the relevant wiki
741            $link = self::getSubdomain( $user, $prefix2['lang'], $p,
742                ( $title->getNsText() ? $title->getNsText() . ':' : '' ) .
743                ( $prefix2['realtitle'] ?? $title->getText() )
744            );
745            $showLink = self::makeExternalLinkText( $link, true );
746            $out->addHtml( '<div class="wminc-wiki-sister">' .
747                wfMessage( 'wminc-error-wiki-sister' )->rawParams( $showLink )->escaped() .
748            '</div>' );
749        } elseif ( self::shouldWeShowUnprefixedError( $title ) ) {
750            # Unprefixed pages
751            if ( self::isContentProject( $user ) ) {
752                # If the user has a test wiki pref, suggest a page title with prefix
753                $suggesttitle = $prefix2['realtitle'] ?? $title->getText();
754                $suggest = self::displayPrefixedTitle( $suggesttitle, $title->getNamespace() );
755                # Suggest to create a prefixed page
756                $out->addHtml( '<div class="wminc-unprefixed-suggest">' .
757                    wfMessage( 'wminc-error-unprefixed-suggest', $suggest )->parseAsBlock() .
758                '</div>' );
759            } else {
760                $out->addWikiMsg( 'wminc-error-unprefixed' );
761            }
762        }
763        return true;
764    }
765
766    /**
767     * Use the InfoPage class to show a nice welcome page
768     * depending on whether it belongs to an existing, closed or missing wiki
769     * @param Article $article
770     * @param array $prefix
771     */
772    public static function onShowMissingArticleForInfoPages( $article, $prefix ) {
773        $out = $article->getContext()->getOutput();
774        $title = $article->getTitle();
775        $out->addModuleStyles( 'WikimediaIncubator.InfoPage' );
776        $infopage = new InfoPage( $title, $prefix, $article->getContext()->getUser() );
777        $dbstate = self::getDBState( $prefix );
778        if ( $dbstate == 'existing' ) {
779            $infopage->mSubStatus = 'beforeincubator';
780            $out->addHtml( $infopage->showExistingWiki() );
781        } elseif ( $dbstate == 'closed' ) {
782            $infopage->mSubStatus = 'imported';
783            $out->addHtml( $infopage->showIncubatingWiki() );
784        } elseif ( self::getMainPage( $prefix['lang'], $prefix['prefix'] )->exists() ) {
785            $infopage->mSubStatus = 'open';
786            $out->addHtml( $infopage->showIncubatingWiki() );
787        } else {
788            $out->addHtml( $infopage->showMissingWiki() );
789        }
790        # Set the page title from "Wx/xyz - Incubator" to "Wikiproject Language - Incubator"
791        $out->setHTMLTitle( wfMessage( 'pagetitle', $infopage->mFormatTitle )->text() );
792    }
793
794    public function onParserFirstCallInit( $parser ) {
795        $parser->setFunctionHook( 'infopage', [ self::class, 'renderParserFunction' ] );
796    }
797
798    /**
799     * #infopage parser function
800     * @param Parser $parser
801     * @param string ...$parseOptions
802     * @return array|string
803     */
804    public static function renderParserFunction( Parser $parser, ...$parseOptions ) {
805        $title = $parser->getTitle();
806        $prefix = self::analyzePrefix( $title );
807        if ( $prefix['error'] ) {
808            return '<span class="error">' .
809                wfMessage( 'wminc-infopage-error' )->plain() . '</span>';
810        }
811        $infopage = new InfoPage( $title, $prefix, $parser->getUserIdentity() );
812        $infopage->mOptions = [
813            'status' => 'open',
814            # other (optional) options: mainpage
815        ];
816
817        foreach ( $parseOptions as $parseOption ) {
818            if ( strpos( $parseOption, '=' ) === false ) {
819                continue;
820            }
821            [ $key, $value ] = explode( '=', $parseOption, 2 );
822            $key = strtolower( trim( $key ) );
823            $infopage->mOptions[$key] = trim( $value );
824        }
825
826        $infopage->mSubStatus = $infopage->mOptions['status'];
827
828        $parser->getOutput()->addModuleStyles( [ 'WikimediaIncubator.InfoPage' ] );
829        $parser->getOptions()->getUserLangObj(); # we have to split the cache by language
830
831        # Set <h1> heading
832        $parser->getOutput()->setTitleText( htmlspecialchars( $infopage->mFormatTitle ) );
833
834        if ( in_array( $infopage->mSubStatus, [ 'created', 'beforeincubator' ] ) ) {
835            $return = $infopage->showExistingWiki();
836        } elseif ( self::getMainPage( $prefix['lang'], $prefix['prefix'] )->exists() ) {
837            $return = $infopage->showIncubatingWiki();
838        } else {
839            // open wiki, no test wiki main page => missing
840            $return = $infopage->showMissingWiki();
841        }
842
843        return [ $return, 'noparse' => true, 'nowiki' => true, 'isHTML' => true ];
844    }
845
846    /**
847     * When creating a new info page, help the user by prefilling it
848     * @param string &$text
849     * @param Title $title
850     */
851    public function onEditFormPreloadText( &$text, $title ) {
852        $prefix = self::analyzePrefix(
853            $title,
854            true, /* only info page */
855            false /* no sister projects */
856        );
857        if ( !$prefix['error'] ) {
858            $text = wfMessage( 'wminc-infopage-prefill', $prefix['prefix'] )->plain();
859        }
860    }
861
862    /**
863     * This forms a URL based on the language and project.
864     * @param UserIdentity $user
865     * @param string $lang Language code
866     * @param string $project Project code or name
867     * @param string $title Page name
868     * @return string
869     */
870    public static function getSubdomain( UserIdentity $user, $lang, $project, $title = '' ) {
871        global $wgArticlePath;
872        return self::getConf( $user, 'wgServer', $lang, $project ) .
873            ( $title ? str_replace( '$1', str_replace( ' ', '_', $title ), $wgArticlePath ) : '' );
874    }
875
876    /**
877     * Make "Wx/xxx/Main Page"
878     * @param string $langCode The language code
879     * @param string|null $prefix the "Wx/xxx" prefix to add
880     * @return Title
881     * @throws InvalidArgumentException
882     */
883    public static function getMainPage( $langCode, $prefix = null ) {
884        # Take the "mainpage" msg in the given language
885        $msg = wfMessage( 'mainpage' )->inLanguage( $langCode )->plain();
886        $mainpage = $prefix !== null ? $prefix . '/' . $msg : $msg;
887        $title = Title::newFromText( $mainpage );
888        if ( $title === null ) {
889            throw new InvalidArgumentException(
890                "'mainpage' message for language '$langCode' is an invalid title"
891            );
892        }
893        if ( $title->exists() ) {
894            return $title; # If it exists, use it
895        }
896        $mainpage = $prefix !== null ? $prefix . '/Main_Page' : 'Main_Page';
897        $title2 = Title::newFromText( $mainpage );
898        if ( $title2->exists() ) {
899            return $title2; # Try the English "Main Page"
900        }
901        return $title; # Nothing exists, use the original again
902    }
903
904    /**
905     * Redirect if &goto=mainpage on info pages
906     * @param OutputPage $output
907     * @param Article $page
908     * @param Title $title
909     * @param User $user
910     * @param WebRequest $request
911     * @param ActionEntryPoint $mediaWiki
912     * @return bool
913     */
914    public function onMediaWikiPerformAction( $output, $page, $title, $user, $request, $mediaWiki ) {
915        $prefix = self::analyzePrefix( $title, true );
916        if ( $prefix['error'] || $request->getVal( 'goto' ) != 'mainpage' ) {
917            return true;
918        }
919
920        $dbstate = self::getDBState( $prefix );
921        if ( !$dbstate ) {
922            return true;
923        }
924        if ( $dbstate == 'existing' ) {
925            # redirect to the existing lang.wikiproject.org if it exists
926            $output->redirect( self::getSubdomain( $user, $prefix['lang'], $prefix['project'] ) );
927            return false;
928        }
929        $params = [ 'redirectfrom' => 'infopage' ];
930        $uselang = $request->getVal( 'uselang' );
931        if ( $uselang ) {
932            # pass through the &uselang parameter
933            $params['uselang'] = $uselang;
934        }
935        $mainpage = self::getMainPage( $prefix['lang'], $prefix['prefix'] );
936        if ( $mainpage->exists() ) {
937            # Only redirect to the main page if that page exists
938            $output->redirect( $mainpage->getFullURL( $params ) );
939            return false;
940        }
941        return true;
942    }
943
944    /**
945     * Valid Wx/xyz info pages should be considered as existing pages
946     * Note: TitleIsAlwaysKnown hook exists since 1.20
947     * @param Title $title
948     * @param bool &$isKnown
949     */
950    public function onTitleIsAlwaysKnown( $title, &$isKnown ) {
951        $prefix = self::analyzePrefix( $title, true, true );
952        if ( !$prefix['error'] ) {
953            $isKnown = true;
954        }
955    }
956
957    /**
958     * Override the page language of test wiki content pages.
959     *
960     * This affects not just the page view, but also the reported meta data
961     * for this page title in other contexts (e.g. API queries, Page information).
962     *
963     * For pages belonging to an incubating test wiki, deterministically
964     * set the page content language to the language of the test wiki.
965     *
966     * @param Title $title
967     * @param Language &$pageLang
968     * @param mixed $userLang Unused, T299369
969     */
970    public function onPageContentLanguage( $title, &$pageLang, $userLang ) {
971        $prefix = self::analyzePrefix( $title, /* onlyInfoPage*/ false );
972        if ( !$prefix['error'] && !self::validatePrefix( $title, true ) ) {
973            $pageLang = $this->languageFactory->getLanguage( $prefix['lang'] );
974        }
975    }
976
977    /**
978     * Override the Parser language on page views.
979     *
980     * For info pages, localise the content in the current user's language
981     * (akin to interface messages and special pages).
982     *
983     * @param Article $article
984     * @param ParserOptions $parserOptions
985     */
986    public function onArticleParserOptions( Article $article, ParserOptions $parserOptions ) {
987        $title = $article->getTitle();
988        $prefix = self::analyzePrefix( $title, true );
989        if ( !$prefix['error'] ) {
990            $pageLang = RequestContext::getMain()->getLanguage();
991            $parserOptions->setTargetLanguage( $pageLang );
992        }
993    }
994
995    /**
996     * Search: Adapt the default message to show a more descriptive one,
997     * along with an adapted link.
998     * @param Title $title
999     * @param array &$params
1000     */
1001    public function onSpecialSearchCreateLink( $title, &$params ) {
1002        if ( $title->isKnown() ) {
1003            return;
1004        }
1005        global $wmincProjectSite, $wmincTestWikiNamespaces;
1006        $prefix = self::displayPrefix();
1007
1008        $newNs = $title->getNamespace();
1009        $newTitle = $title->getText();
1010        $newTitleData = self::analyzePrefix( $newTitle, false, true );
1011        if ( !in_array( $title->getNamespace(), $wmincTestWikiNamespaces ) ) {
1012            # namespace not affected by the prefix system: show normal msg
1013            return;
1014        } elseif ( $prefix == $wmincProjectSite['short'] ) {
1015            $newNs = NS_PROJECT;
1016        } elseif ( self::getDBState( $newTitleData ) == 'existing' ) {
1017            # the wiki already exists
1018            $link = self::getSubdomain(
1019                RequestContext::getMain()->getUser(), // No context
1020                $newTitleData['lang'], $newTitleData['project'],
1021                ( $title->getNsText() ? $title->getNsText() . ':' : '' ) .
1022                ( $newTitleData['realtitle'] ?? $title->getText() )
1023            );
1024            $params[0] = 'wminc-error-wiki-exists';
1025            $params[1] = "[$link " . self::makeExternalLinkText( $link ) . "]";
1026            return;
1027        } elseif ( $newTitleData['error'] ) {
1028            # only add a prefix to the title if there is no prefix
1029            # ('error' by analyzePrefix)
1030            $newTitle = $prefix . '/' . $newTitle;
1031        }
1032
1033        $t = Title::newFromText( $newTitle, $newNs );
1034        if ( $t && $t->isKnown() ) {
1035            # use the default message if the suggested title exists
1036            $params[0] = 'searchmenu-exists';
1037            $params[1] = wfEscapeWikiText( $t->getPrefixedText() );
1038            return;
1039        }
1040        $params[] = wfEscapeWikiText( $t ? $t->getPrefixedText() : $newTitle );
1041        $params[0] = $prefix && $prefix != self::NO_PROJECT_SELECTED
1042            ? 'wminc-search-nocreate-suggest' : 'wminc-search-nocreate-nopref';
1043    }
1044
1045    /**
1046     * Search: Add an input form to enter a test wiki prefix.
1047     * @param array &$showSections
1048     * @param string $term
1049     * @param array &$opts
1050     */
1051    public function onSpecialSearchPowerBox( &$showSections, $term, &$opts ) {
1052        $showSections['testwiki'] = Xml::label( wfMessage( 'wminc-testwiki' )->text(), 'testwiki' )
1053            . ' ' . Xml::input( 'testwiki', 20, self::displayPrefix(), [ 'id' => 'testwiki' ] );
1054    }
1055
1056    /**
1057     * Search: Search by default in the test wiki of the user's preference (or url &testwiki).
1058     * @param SpecialSearch $search
1059     * @param string $profile
1060     * @param SearchEngine $engine
1061     */
1062    public function onSpecialSearchSetupEngine( $search, $profile, $engine ) {
1063        if ( !$engine->prefix ) {
1064            $engine->prefix = self::selectedIncubationWiki() ?? '';
1065        }
1066    }
1067
1068    private static function preg_quote_slash( $str ) {
1069        return preg_quote( $str, '/' );
1070    }
1071
1072    /**
1073     * @param string $url
1074     * @param bool $callLinker Whether to call makeExternalLink()
1075     * @return string
1076     */
1077    public static function makeExternalLinkText( $url, $callLinker = false ) {
1078        # when displaying a URL, if it contains 'http://' or 'https://' it's ok to leave it,
1079        # but for protocol-relative URLs, it's nicer to remove the '//'
1080        $linktext = ltrim( $url, '/' );
1081        return $callLinker ? Linker::makeExternalLink( $url, $linktext ) : $linktext;
1082    }
1083
1084    /**
1085     * Set global variables for use in scripts
1086     * @param array &$vars
1087     * @param OutputPage $out
1088     */
1089    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
1090        $title = $out->getTitle();
1091        $prefix = self::analyzePrefix( $title );
1092        if ( !$prefix[ 'error' ] ) {
1093            $vars[ 'wgWmincTestwikiPrefix' ] = $prefix[ 'prefix' ];
1094            $vars[ 'wgWmincTestwikiProject' ] = $prefix[ 'project' ];
1095            $vars[ 'wgWmincTestwikiLanguage' ] = $prefix[ 'lang' ];
1096            if ( $prefix[ 'realtitle' ] ) {
1097                $vars[ 'wgWmincRealPagename' ] = $prefix[ 'realtitle' ];
1098            }
1099        }
1100    }
1101
1102    /**
1103     * Use real page title as default sort key
1104     * @param Title $title
1105     * @param string &$sortkey
1106     */
1107    public function onGetDefaultSortkey( $title, &$sortkey ) {
1108        $prefix = self::analyzePrefix( $title );
1109        if ( !$prefix[ 'error' ] && $prefix[ 'realtitle' ] ) {
1110            $sortkey = $prefix[ 'realtitle' ];
1111        }
1112    }
1113
1114    /**
1115     * Generate an HTML title.
1116     *
1117     * Generate an HTML title (for use in the <h1> tag) with spans around
1118     * the prefix and real title.
1119     *
1120     * @param string $namespace
1121     * @param string $prefix
1122     * @param string $realTitle
1123     * @param string $displayTitle HTML
1124     * @return string
1125     */
1126    private static function generateHtmlTitle( $namespace, $prefix, $realTitle, $displayTitle ) {
1127        // Is there an actual, valid {{DISPLAYTITLE}} on the page? If the
1128        // $displayTitle *doesn't* contain the following string, there is.
1129        // Validation of the display title is already done by the time the
1130        // hook below is run, so what it returns can be considered safe HTML.
1131        $useDisplayTitle = !preg_match( '/mw-page-title-main/', $displayTitle );
1132
1133        // If the display title contains the prefix uninterrupted, we can
1134        // safely remove the prefix and attach the remainder to the prefix
1135        // to form the full page title. This will allow future-proof
1136        // display titles like {{DISPLAYTITLE:''{{PAGENAME}}''}} work
1137        // as well.
1138        // An example of an "interrupted" prefix would be e.g.
1139        // "Wp/<b>xx</b>/<i>Title</i>".
1140        $displayTitleExploded = explode( '/', $displayTitle );
1141        $displayTitleSliced = array_slice( $displayTitleExploded, 0, 2 );
1142        $displayTitlePrefix = implode( '/', $displayTitleSliced ) . '/';
1143        $displayTitleContainsPrefix = str_ends_with( $displayTitlePrefix, $prefix . '/' );
1144
1145        // If the display title doesn't contain the prefix uninterrupted,
1146        // we just return the display title without doing anything else.
1147        // This is similar to how core deals with DISPLAYTITLE.
1148        if ( $useDisplayTitle && !$displayTitleContainsPrefix ) {
1149            return $displayTitle;
1150        } elseif ( $useDisplayTitle ) {
1151            $realTitle = preg_replace(
1152                '~' . preg_quote( $prefix, '~' ) . '/~',
1153                '',
1154                $displayTitle,
1155                1
1156            );
1157        }
1158
1159        $prefixsplit = explode( '/', $prefix );
1160
1161        $project = Html::element(
1162            'span',
1163            [
1164                'class' => 'ext-wminc-title-project',
1165                'dir' => 'ltr',
1166            ],
1167            $prefixsplit[0]
1168        );
1169
1170        $langCode = Html::element(
1171            'span',
1172            [
1173                'class' => 'ext-wminc-title-langcode',
1174                'dir' => 'ltr',
1175            ],
1176            $prefixsplit[1]
1177        );
1178
1179        $title = Html::rawElement(
1180            'span',
1181            [ 'class' => 'ext-wminc-title-prefix' ],
1182            $project . '/' . $langCode . '/'
1183        );
1184        // Needs to be rawElement to allow passing HTML from the display title.
1185        $title .= Html::rawElement(
1186            'span',
1187            [ 'class' => 'ext-wminc-title-realtitle' ],
1188            $realTitle
1189        );
1190
1191        return Parser::formatPageTitle( $namespace, ':', new HtmlArmor( $title ) );
1192    }
1193
1194    /**
1195     * Set the page title.
1196     *
1197     * Encapsulate the test wiki prefix in the <h1> element with <span> tags
1198     * with relevant classes in order to let them be styled separately.
1199     *
1200     * Also changes the HTML's <title> element to remove the prefix, but only for
1201     * pages in the main namespace.
1202     *
1203     * The function takes into consideration DISPLAYTITLEs and language
1204     * conversion; when converting the title, this function ensures that
1205     * only the "realtitle" (i.e. the part after the prefix) is converted,
1206     * and not the prefix as well.
1207     *
1208     * @param OutputPage $out
1209     * @param Skin $skin
1210     * @return void
1211     */
1212    public function onBeforePageDisplay( $out, $skin ): void {
1213        $prefix = self::analyzePrefix( $out->getTitle() );
1214        $action = $out->getContext()->getActionName();
1215
1216        if ( !$prefix['error'] && $prefix['realtitle'] && $action === 'view' ) {
1217            $service = MediaWikiServices::getInstance();
1218            $displayTitle = $out->getDisplayTitle();
1219            $languageConverter = $service->getLanguageConverterFactory()
1220                ->getLanguageConverter( $out->getTitle()->getPageLanguage() );
1221            $convertedTitle = $languageConverter->convert( $prefix['realtitle'] );
1222
1223            // Change the page title (i.e. what's inside <h1> tags) according
1224            // to our formatting (see self::generatHtmlTitle() above)
1225            $title = self::generateHtmlTitle(
1226                $out->getTitle()->getNsText(),
1227                $prefix['prefix'],
1228                $convertedTitle,
1229                $displayTitle
1230            );
1231            $out->setPageTitle( $title );
1232
1233            // Set the page's <title> element to the prefixless version of the
1234            // page name, but only in the main namespace.
1235            $namespace = $out->getTitle()->getNamespace();
1236
1237            if ( $namespace === 0 ) {
1238                $titleElementText = $out->msg( 'wminc-title-element' )
1239                    ->params( $convertedTitle, $prefix['prefix'] )
1240                    ->text();
1241                $pageTitleMsg = $out->msg( 'pagetitle' )
1242                    ->params( $titleElementText )
1243                    ->text();
1244                $out->setHTMLTitle( $pageTitleMsg );
1245            }
1246        }
1247    }
1248}