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