Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.99% covered (warning)
72.99%
127 / 174
50.00% covered (danger)
50.00%
7 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Babel
73.41% covered (warning)
73.41%
127 / 173
50.00% covered (danger)
50.00%
7 / 14
91.31
0.00% covered (danger)
0.00%
0 / 1
 Render
91.89% covered (success)
91.89%
34 / 37
0.00% covered (danger)
0.00%
0 / 1
9.04
 mGenerateContentTower
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 setExtensionData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 mGenerateContent
67.50% covered (warning)
67.50%
27 / 40
0.00% covered (danger)
0.00%
0 / 1
8.68
 mTemplateLinkBatch
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 mPageExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 mValidTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mParseParameter
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
7.04
 getUserLanguageInfo
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getCachedUserLanguageInfo
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 getLanguages
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
3.00
 getCachedUserLanguages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserLanguages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserLanguagesDB
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
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
16declare( strict_types = 1 );
17
18namespace MediaWiki\Babel;
19
20use MediaWiki\Babel\BabelBox\LanguageBabelBox;
21use MediaWiki\Babel\BabelBox\NotBabelBox;
22use MediaWiki\Babel\BabelBox\NullBabelBox;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Title\Title;
25use MediaWiki\User\UserIdentity;
26use MediaWiki\WikiMap\WikiMap;
27use Parser;
28use ParserOutput;
29
30/**
31 * Main class for the Babel extension.
32 */
33class 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|}
95EOT;
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
450class_alias( Babel::class, 'Babel' );