Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
17.04% |
84 / 493 |
|
16.67% |
8 / 48 |
CRAP | |
0.00% |
0 / 1 |
WikimediaIncubator | |
17.04% |
84 / 493 |
|
16.67% |
8 / 48 |
21021.32 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onUserGetDefaultOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onLoadUserOptions | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
onGetPreferences | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
12 | |||
getTestWikiLanguages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
validateCodePreference | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
filterCodePreference | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
validateLanguageCode | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
3.58 | |||
analyzePrefix | |
96.67% |
29 / 30 |
|
0.00% |
0 / 1 |
16 | |||
validatePrefix | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getUrlParam | |
33.33% |
3 / 9 |
|
0.00% |
0 / 1 |
21.52 | |||
getProject | |
62.50% |
10 / 16 |
|
0.00% |
0 / 1 |
15.27 | |||
isContentProject | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
displayPrefix | |
78.57% |
11 / 14 |
|
0.00% |
0 / 1 |
8.63 | |||
selectedIncubationWiki | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
displayPrefixedTitle | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onMagicWordwgVariableIDs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onParserGetVariableValueSwitch | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
shouldWeShowUnprefixedError | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
7.18 | |||
onGetUserPermissionsErrors | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
132 | |||
onMovePageIsValidMove | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onContributionsToolLinks | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getConf | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
getExistingWikis | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canWeCheckDB | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDB | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
getDBClosedWikis | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
getDBState | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
onShowMissingArticle | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
110 | |||
onShowMissingArticleForInfoPages | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
onParserFirstCallInit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderParserFunction | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
onEditFormPreloadText | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getSubdomain | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getMainPage | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
onMediaWikiPerformAction | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
onTitleIsAlwaysKnown | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
onPageContentLanguage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
onArticleParserOptions | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
onSpecialSearchCreateLink | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
156 | |||
onSpecialSearchPowerBox | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onSpecialSearchSetupEngine | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
preg_quote_slash | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeExternalLinkText | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onMakeGlobalVariablesScript | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onGetDefaultSortkey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
generateHtmlTitle | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
20 | |||
onBeforePageDisplay | |
0.00% |
0 / 24 |
|
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 | |
13 | namespace MediaWiki\Extension\WikimediaIncubator; |
14 | |
15 | use Article; |
16 | use HtmlArmor; |
17 | use InvalidArgumentException; |
18 | use Language; |
19 | use MediaWiki\Actions\ActionEntryPoint; |
20 | use MediaWiki\Content\Hook\PageContentLanguageHook; |
21 | use MediaWiki\Hook\BeforePageDisplayHook; |
22 | use MediaWiki\Hook\ContributionsToolLinksHook; |
23 | use MediaWiki\Hook\EditFormPreloadTextHook; |
24 | use MediaWiki\Hook\GetDefaultSortkeyHook; |
25 | use MediaWiki\Hook\MagicWordwgVariableIDsHook; |
26 | use MediaWiki\Hook\MakeGlobalVariablesScriptHook; |
27 | use MediaWiki\Hook\MediaWikiPerformActionHook; |
28 | use MediaWiki\Hook\MovePageIsValidMoveHook; |
29 | use MediaWiki\Hook\ParserFirstCallInitHook; |
30 | use MediaWiki\Hook\ParserGetVariableValueSwitchHook; |
31 | use MediaWiki\Hook\SpecialSearchCreateLinkHook; |
32 | use MediaWiki\Hook\SpecialSearchSetupEngineHook; |
33 | use MediaWiki\Hook\TitleIsAlwaysKnownHook; |
34 | use MediaWiki\Html\Html; |
35 | use MediaWiki\Languages\LanguageFactory; |
36 | use MediaWiki\Languages\LanguageNameUtils; |
37 | use MediaWiki\Linker\Linker; |
38 | use MediaWiki\MediaWikiServices; |
39 | use MediaWiki\Output\OutputPage; |
40 | use MediaWiki\Page\Hook\ArticleParserOptionsHook; |
41 | use MediaWiki\Page\Hook\ShowMissingArticleHook; |
42 | use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook; |
43 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
44 | use MediaWiki\Request\WebRequest; |
45 | use MediaWiki\Search\Hook\SpecialSearchPowerBoxHook; |
46 | use MediaWiki\SpecialPage\SpecialPage; |
47 | use MediaWiki\Specials\SpecialSearch; |
48 | use MediaWiki\Title\Title; |
49 | use MediaWiki\User\Hook\UserGetDefaultOptionsHook; |
50 | use MediaWiki\User\Options\Hook\LoadUserOptionsHook; |
51 | use MediaWiki\User\User; |
52 | use MediaWiki\User\UserIdentity; |
53 | use Parser; |
54 | use ParserOptions; |
55 | use RequestContext; |
56 | use SearchEngine; |
57 | use Skin; |
58 | use Xml; |
59 | |
60 | class 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 | } |