Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.99% |
127 / 174 |
|
50.00% |
7 / 14 |
CRAP | |
0.00% |
0 / 1 |
Babel | |
73.41% |
127 / 173 |
|
50.00% |
7 / 14 |
91.31 | |
0.00% |
0 / 1 |
Render | |
91.89% |
34 / 37 |
|
0.00% |
0 / 1 |
9.04 | |||
mGenerateContentTower | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
setExtensionData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
mGenerateContent | |
67.50% |
27 / 40 |
|
0.00% |
0 / 1 |
8.68 | |||
mTemplateLinkBatch | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
mPageExists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
mValidTitle | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
mParseParameter | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
7.04 | |||
getUserLanguageInfo | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getCachedUserLanguageInfo | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
getLanguages | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
3.00 | |||
getCachedUserLanguages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUserLanguages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserLanguagesDB | |
36.36% |
4 / 11 |
|
0.00% |
0 / 1 |
15.28 |
1 | <?php |
2 | /** |
3 | * Contains main code. |
4 | * |
5 | * @file |
6 | * @author Robert Leverington |
7 | * @author Robin Pepermans |
8 | * @author Niklas Laxström |
9 | * @author Brian Wolff |
10 | * @author Purodha Blissenbach |
11 | * @author Sam Reed |
12 | * @author Siebrand Mazeland |
13 | * @license GPL-2.0-or-later |
14 | */ |
15 | |
16 | declare( strict_types = 1 ); |
17 | |
18 | namespace MediaWiki\Babel; |
19 | |
20 | use MediaWiki\Babel\BabelBox\LanguageBabelBox; |
21 | use MediaWiki\Babel\BabelBox\NotBabelBox; |
22 | use MediaWiki\Babel\BabelBox\NullBabelBox; |
23 | use MediaWiki\MediaWikiServices; |
24 | use MediaWiki\Title\Title; |
25 | use MediaWiki\User\UserIdentity; |
26 | use MediaWiki\WikiMap\WikiMap; |
27 | use Parser; |
28 | use ParserOutput; |
29 | |
30 | /** |
31 | * Main class for the Babel extension. |
32 | */ |
33 | class Babel { |
34 | /** |
35 | * @var Title |
36 | */ |
37 | protected static $title; |
38 | |
39 | /** |
40 | * Render the Babel tower. |
41 | * |
42 | * @param Parser $parser |
43 | * @param string ...$parameters |
44 | * @return string Babel tower. |
45 | */ |
46 | public static function Render( Parser $parser, string ...$parameters ): string { |
47 | global $wgBabelUseUserLanguage, $wgBabelAllowOverride; |
48 | self::$title = $parser->getTitle(); |
49 | |
50 | self::mTemplateLinkBatch( $parameters ); |
51 | |
52 | $parser->getOutput()->addModuleStyles( [ 'ext.babel' ] ); |
53 | |
54 | $content = self::mGenerateContentTower( $parser, $parameters ); |
55 | |
56 | if ( preg_match( '/^plain\s*=\s*\S/', reset( $parameters ) ) ) { |
57 | return $content; |
58 | } |
59 | |
60 | if ( $wgBabelUseUserLanguage ) { |
61 | $uiLang = $parser->getOptions()->getUserLangObj(); |
62 | } else { |
63 | $uiLang = self::$title->getPageLanguage(); |
64 | } |
65 | |
66 | $top = wfMessage( 'babel', self::$title->getDBkey() )->inLanguage( $uiLang ); |
67 | |
68 | if ( $top->isDisabled() ) { |
69 | $top = ''; |
70 | } else { |
71 | $top = $top->text(); |
72 | $url = wfMessage( 'babel-url' )->inContentLanguage(); |
73 | if ( !$url->isDisabled() ) { |
74 | $top = '[[' . $url->text() . '|' . $top . ']]'; |
75 | } |
76 | $top = '! class="mw-babel-header" | ' . $top; |
77 | } |
78 | $footer = wfMessage( 'babel-footer', self::$title->getDBkey() )->inLanguage( $uiLang ); |
79 | |
80 | $url = wfMessage( 'babel-footer-url' )->inContentLanguage(); |
81 | $showFooter = ''; |
82 | if ( !$footer->isDisabled() && !$url->isDisabled() ) { |
83 | $showFooter = '! class="mw-babel-footer" | [[' . |
84 | $url->text() . '|' . $footer->text() . ']]'; |
85 | } |
86 | |
87 | $tower = <<<EOT |
88 | {|class="mw-babel-wrapper" |
89 | $top |
90 | |- |
91 | | $content |
92 | |- |
93 | $showFooter |
94 | |} |
95 | EOT; |
96 | if ( $wgBabelAllowOverride ) { |
97 | // Make sure the page shows up as transcluding MediaWiki:babel-category-override |
98 | $title = Title::makeTitle( NS_MEDIAWIKI, "Babel-category-override" ); |
99 | $revision = $parser->fetchCurrentRevisionRecordOfTitle( $title ); |
100 | if ( $revision === null ) { |
101 | $revid = null; |
102 | } else { |
103 | $revid = $revision->getId(); |
104 | } |
105 | // Passing null here when the page doesn't exist matches what the core parser does |
106 | // even though the documentation of the method says otherwise. |
107 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
108 | $parser->getOutput()->addTemplate( $title, $title->getArticleID(), $revid ); |
109 | } |
110 | return $tower; |
111 | } |
112 | |
113 | /** |
114 | * @param Parser $parser |
115 | * @param string[] $parameters |
116 | * |
117 | * @return string Wikitext |
118 | */ |
119 | private static function mGenerateContentTower( Parser $parser, array $parameters ): string { |
120 | $content = ''; |
121 | // collects name=value parameters to be passed to wiki templates. |
122 | $templateParameters = []; |
123 | |
124 | $generateCategories = !preg_match( '/^nocat\s*=\s*\S/', reset( $parameters ) ); |
125 | |
126 | foreach ( $parameters as $name ) { |
127 | if ( strpos( $name, '=' ) !== false ) { |
128 | $templateParameters[] = $name; |
129 | continue; |
130 | } |
131 | |
132 | $content .= self::mGenerateContent( $parser, $name, $templateParameters, $generateCategories ); |
133 | } |
134 | |
135 | return $content; |
136 | } |
137 | |
138 | private static function setExtensionData( |
139 | ParserOutput $parserOutput, |
140 | string $code, |
141 | string $level |
142 | ): void { |
143 | $data = $parserOutput->getExtensionData( 'babel' ) ?: []; |
144 | $data[ BabelLanguageCodes::getCategoryCode( $code ) ] = $level; |
145 | $parserOutput->setExtensionData( 'babel', $data ); |
146 | } |
147 | |
148 | /** |
149 | * @param Parser $parser |
150 | * @param string $name |
151 | * @param string[] $templateParameters |
152 | * @param bool $generateCategories Whether to add categories |
153 | * @return string Wikitext |
154 | */ |
155 | private static function mGenerateContent( |
156 | Parser $parser, |
157 | string $name, |
158 | array $templateParameters, |
159 | bool $generateCategories |
160 | ): string { |
161 | $components = self::mParseParameter( $name ); |
162 | |
163 | $template = wfMessage( 'babel-template', $name )->inContentLanguage()->text(); |
164 | $parserOutput = $parser->getOutput(); |
165 | |
166 | if ( $name === '' ) { |
167 | $box = new NullBabelBox(); |
168 | } elseif ( $components !== false ) { |
169 | // Valid parameter syntax (with lowercase language code), babel box |
170 | $box = new LanguageBabelBox( |
171 | self::$title, |
172 | $components['code'], |
173 | $components['level'], |
174 | ); |
175 | self::setExtensionData( $parserOutput, $components['code'], $components['level'] ); |
176 | } elseif ( self::mPageExists( $template ) ) { |
177 | // Check for an existing template |
178 | $templateParameters[0] = $template; |
179 | $template = implode( '|', $templateParameters ); |
180 | $box = new NotBabelBox( |
181 | self::$title->getPageLanguage()->getDir(), |
182 | $parser->replaceVariables( "{{{$template}}}" ) |
183 | ); |
184 | } elseif ( self::mValidTitle( $template ) ) { |
185 | // Non-existing page, so try again as a babel box, |
186 | // with converting the code to lowercase |
187 | $components2 = self::mParseParameter( $name, true ); |
188 | if ( $components2 !== false ) { |
189 | $box = new LanguageBabelBox( |
190 | self::$title, |
191 | $components2['code'], |
192 | $components2['level'], |
193 | ); |
194 | self::setExtensionData( $parserOutput, |
195 | $components2['code'], $components2['level'] ); |
196 | } else { |
197 | // Non-existent page and invalid parameter syntax, red link. |
198 | $box = new NotBabelBox( |
199 | self::$title->getPageLanguage()->getDir(), |
200 | '[[' . $template . ']]' |
201 | ); |
202 | } |
203 | } else { |
204 | // Invalid title, output raw. |
205 | $box = new NotBabelBox( |
206 | self::$title->getPageLanguage()->getDir(), |
207 | $template |
208 | ); |
209 | } |
210 | if ( $generateCategories ) { |
211 | $box->addCategories( $parserOutput ); |
212 | } |
213 | |
214 | return $box->render(); |
215 | } |
216 | |
217 | /** |
218 | * Performs a link batch on a series of templates. |
219 | * |
220 | * @param string[] $parameters Templates to perform the link batch on. |
221 | */ |
222 | protected static function mTemplateLinkBatch( array $parameters ): void { |
223 | $titles = []; |
224 | foreach ( $parameters as $name ) { |
225 | $title = Title::newFromText( wfMessage( 'babel-template', $name )->inContentLanguage()->text() ); |
226 | if ( is_object( $title ) ) { |
227 | $titles[] = $title; |
228 | } |
229 | } |
230 | |
231 | $batch = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch( $titles ); |
232 | $batch->setCaller( __METHOD__ ); |
233 | $batch->execute(); |
234 | } |
235 | |
236 | /** |
237 | * Identify whether or not a page exists. |
238 | * |
239 | * @param string $name Name of the page to check. |
240 | * @return bool Indication of whether the page exists. |
241 | */ |
242 | protected static function mPageExists( string $name ): bool { |
243 | $titleObj = Title::newFromText( $name ); |
244 | |
245 | return ( is_object( $titleObj ) && $titleObj->exists() ); |
246 | } |
247 | |
248 | /** |
249 | * Identify whether the passed string would make a valid page name. |
250 | * |
251 | * @param string $name Name of page to check. |
252 | * @return bool Indication of whether the title is valid. |
253 | */ |
254 | protected static function mValidTitle( string $name ): bool { |
255 | $titleObj = Title::newFromText( $name ); |
256 | |
257 | return is_object( $titleObj ); |
258 | } |
259 | |
260 | /** |
261 | * Parse a parameter, getting a language code and level. |
262 | * |
263 | * @param string $parameter Parameter. |
264 | * @param bool $strtolower Whether to convert the language code to lowercase |
265 | * @return array|bool [ 'code' => xx, 'level' => xx ] false on failure |
266 | */ |
267 | protected static function mParseParameter( string $parameter, bool $strtolower = false ) { |
268 | global $wgBabelDefaultLevel, $wgBabelCategoryNames; |
269 | $return = []; |
270 | |
271 | $babelCode = $strtolower ? strtolower( $parameter ) : $parameter; |
272 | // Try treating the parameter as a language code (for default level). |
273 | $code = BabelLanguageCodes::getCode( $babelCode ); |
274 | if ( $code !== false ) { |
275 | $return['code'] = $code; |
276 | $return['level'] = $wgBabelDefaultLevel; |
277 | return $return; |
278 | } |
279 | // Try splitting the parameter in to language and level, split on last hyphen. |
280 | $lastSplit = strrpos( $parameter, '-' ); |
281 | if ( $lastSplit === false ) { |
282 | return false; |
283 | } |
284 | $code = substr( $parameter, 0, $lastSplit ); |
285 | $level = substr( $parameter, $lastSplit + 1 ); |
286 | |
287 | $babelCode = $strtolower ? strtolower( $code ) : $code; |
288 | // Validate code. |
289 | $return['code'] = BabelLanguageCodes::getCode( $babelCode ); |
290 | if ( $return['code'] === false ) { |
291 | return false; |
292 | } |
293 | // Validate level. |
294 | $level = strtoupper( $level ); |
295 | if ( !isset( $wgBabelCategoryNames[$level] ) ) { |
296 | return false; |
297 | } |
298 | $return['level'] = $level; |
299 | |
300 | return $return; |
301 | } |
302 | |
303 | /** |
304 | * Gets the language information a user has set up with Babel. |
305 | * This function gets the actual info directly from the database. |
306 | * For performance, it is recommended to use |
307 | * getCachedUserLanguageInfo instead. |
308 | * |
309 | * @param UserIdentity $user |
310 | * @return string[] [ language code => level ], sorted by language code |
311 | */ |
312 | public static function getUserLanguageInfo( UserIdentity $user ): array { |
313 | $userLanguageInfo = self::getUserLanguagesDB( $user ); |
314 | |
315 | ksort( $userLanguageInfo ); |
316 | |
317 | return $userLanguageInfo; |
318 | } |
319 | |
320 | /** |
321 | * Gets the language information a user has set up with Babel, |
322 | * from the cache. It's recommended to use this when this will |
323 | * be called frequently. |
324 | * |
325 | * @param UserIdentity $user |
326 | * @return string[] [ language code => level ], sorted by language code |
327 | * |
328 | * @since Version 1.10.0 |
329 | */ |
330 | public static function getCachedUserLanguageInfo( UserIdentity $user ): array { |
331 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
332 | $userId = $user->getId(); |
333 | $key = $cache->makeKey( 'babel-local-languages', $userId ); |
334 | $checkKeys = [ $key ]; |
335 | $centralId = MediaWikiServices::getInstance()->getCentralIdLookupFactory() |
336 | ->getLookup()->centralIdFromLocalUser( $user ); |
337 | |
338 | if ( $centralId ) { |
339 | $checkKeys[] = $cache->makeGlobalKey( 'babel-central-languages', $centralId ); |
340 | } |
341 | |
342 | return $cache->getWithSetCallback( |
343 | $key, |
344 | $cache::TTL_MINUTE * 30, |
345 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $userId, $user ) { |
346 | wfDebug( "Babel: cache miss for user $userId\n" ); |
347 | |
348 | return self::getUserLanguageInfo( $user ); |
349 | }, |
350 | [ |
351 | 'checkKeys' => $checkKeys, |
352 | ] |
353 | ); |
354 | } |
355 | |
356 | /** |
357 | * Gets only the languages codes list out of the user language info. |
358 | * |
359 | * @param string[] $languageInfo [ language code => level ], the return value of |
360 | * getUserLanguageInfo. |
361 | * @param string|null $level Minimal level as given in $wgBabelCategoryNames |
362 | * @return string[] List of language codes. Sorted by level if $level is not null, |
363 | * otherwise sorted by language code |
364 | * |
365 | * @since Version 1.10.0 |
366 | */ |
367 | private static function getLanguages( array $languageInfo, ?string $level ): array { |
368 | if ( !$languageInfo ) { |
369 | return []; |
370 | } |
371 | |
372 | if ( $level !== null ) { |
373 | // filter down the set, note that this uses a text sort! |
374 | $languageInfo = array_filter( |
375 | $languageInfo, |
376 | static function ( $value ) use ( $level ) { |
377 | return ( strcmp( $value, $level ) >= 0 ); |
378 | } |
379 | ); |
380 | // sort and retain keys |
381 | uasort( |
382 | $languageInfo, |
383 | static function ( $a, $b ) { |
384 | return -strcmp( $a, $b ); |
385 | } |
386 | ); |
387 | } |
388 | |
389 | return array_keys( $languageInfo ); |
390 | } |
391 | |
392 | /** |
393 | * Gets the cached list of languages a user has set up with Babel. |
394 | * |
395 | * @param UserIdentity $user |
396 | * @param string|null $level Minimal level as given in $wgBabelCategoryNames |
397 | * @return string[] List of language codes. Sorted by level if $level is not null, |
398 | * otherwise sorted by language code |
399 | * |
400 | * @since Version 1.10.0 |
401 | */ |
402 | public static function getCachedUserLanguages( |
403 | UserIdentity $user, |
404 | string $level = null |
405 | ): array { |
406 | return self::getLanguages( self::getCachedUserLanguageInfo( $user ), $level ); |
407 | } |
408 | |
409 | /** |
410 | * Gets the list of languages a user has set up with Babel. |
411 | * For performance reasons, it is recommended to use getCachedUserLanguages. |
412 | * |
413 | * @param UserIdentity $user |
414 | * @param string|null $level Minimal level as given in $wgBabelCategoryNames |
415 | * @return string[] List of language codes. Sorted by level if $level is not null, |
416 | * otherwise sorted by language code |
417 | * |
418 | * @since Version 1.9.0 |
419 | */ |
420 | public static function getUserLanguages( UserIdentity $user, string $level = null ): array { |
421 | return self::getLanguages( self::getUserLanguageInfo( $user ), $level ); |
422 | } |
423 | |
424 | private static function getUserLanguagesDB( UserIdentity $user ): array { |
425 | global $wgBabelCentralDb; |
426 | |
427 | $babelDB = new Database(); |
428 | $result = $babelDB->getForUser( $user->getId() ); |
429 | /** If local data or no central source, return */ |
430 | if ( $result || !$wgBabelCentralDb ) { |
431 | return $result; |
432 | } |
433 | |
434 | if ( $wgBabelCentralDb === WikiMap::getCurrentWikiId() ) { |
435 | // We are the central wiki, so no fallback we can do |
436 | return []; |
437 | } |
438 | |
439 | $lookup = MediaWikiServices::getInstance()->getCentralIdLookupFactory()->getLookup(); |
440 | if ( !$lookup->isAttached( $user ) |
441 | || !$lookup->isAttached( $user, $wgBabelCentralDb ) |
442 | ) { |
443 | return []; |
444 | } |
445 | |
446 | return $babelDB->getForRemoteUser( $wgBabelCentralDb, $user->getName() ); |
447 | } |
448 | } |
449 | |
450 | class_alias( Babel::class, 'Babel' ); |