Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.19% covered (warning)
79.19%
156 / 197
77.78% covered (warning)
77.78%
14 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
LanguageLibrary
79.19% covered (warning)
79.19%
156 / 197
77.78% covered (warning)
77.78%
14 / 18
98.78
0.00% covered (danger)
0.00%
0 / 1
 register
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
3
 getContLangCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSupportedLanguage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isKnownLanguageTag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isValidCode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isValidBuiltInCode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fetchLanguageName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 fetchLanguageNames
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getFallbacksFor
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 toBcp47Code
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 languageMethod
79.31% covered (warning)
79.31%
23 / 29
0.00% covered (danger)
0.00%
0 / 1
16.99
 convertPlural
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 convertGrammar
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 gender
38.46% covered (danger)
38.46%
10 / 26
0.00% covered (danger)
0.00%
0 / 1
39.20
 formatNum
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
5.58
 formatDate
65.12% covered (warning)
65.12%
28 / 43
0.00% covered (danger)
0.00%
0 / 1
22.32
 formatDuration
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getDurationIntervals
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon;
4
5use DateTime;
6use DateTimeZone;
7use Exception;
8use Language;
9use LanguageCode;
10use MediaWiki\Languages\LanguageNameUtils;
11use MediaWiki\MainConfigNames;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15use MediaWiki\Utils\MWTimestamp;
16use Wikimedia\RequestTimeout\TimeoutException;
17
18class LanguageLibrary extends LibraryBase {
19    /** @var Language[] */
20    public $langCache = [];
21    /** @var array */
22    public $timeCache = [];
23    /** @var int */
24    public $maxLangCacheSize;
25
26    public function register() {
27        // Pre-populate the language cache
28        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
29        $this->langCache[$contLang->getCode()] = $contLang;
30        $this->maxLangCacheSize = $this->getEngine()->getOption( 'maxLangCacheSize' );
31
32        $statics = [
33            'getContLangCode',
34            'isSupportedLanguage',
35            'isKnownLanguageTag',
36            'isValidCode',
37            'isValidBuiltInCode',
38            'fetchLanguageName',
39            'fetchLanguageNames',
40            'getFallbacksFor',
41            'toBcp47Code',
42        ];
43        $methods = [
44            'lcfirst',
45            'ucfirst',
46            'lc',
47            'uc',
48            'caseFold',
49            'formatNum',
50            'formatDate',
51            'formatDuration',
52            'getDurationIntervals',
53            'parseFormattedNumber',
54            'convertPlural',
55            'convertGrammar',
56            'gender',
57            'isRTL',
58        ];
59        $lib = [];
60        foreach ( $statics as $name ) {
61            $lib[$name] = [ $this, $name ];
62        }
63        foreach ( $methods as $name ) {
64            $lib[$name] = function () use ( $name ) {
65                $args = func_get_args();
66                return $this->languageMethod( $name, $args );
67            };
68        }
69        return $this->getEngine()->registerInterface( 'mw.language.lua', $lib );
70    }
71
72    /**
73     * Handler for getContLangCode
74     * @internal
75     * @return string[]
76     */
77    public function getContLangCode() {
78        return [ MediaWikiServices::getInstance()->getContentLanguage()->getCode() ];
79    }
80
81    /**
82     * Handler for isSupportedLanguage
83     * @internal
84     * @param string $code
85     * @return bool[]
86     */
87    public function isSupportedLanguage( $code ) {
88        $this->checkType( 'isSupportedLanguage', 1, $code, 'string' );
89        return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isSupportedLanguage( $code ) ];
90    }
91
92    /**
93     * Handler for isKnownLanguageTag
94     * @internal
95     * @param string $code
96     * @return bool[]
97     */
98    public function isKnownLanguageTag( $code ) {
99        $this->checkType( 'isKnownLanguageTag', 1, $code, 'string' );
100        return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isKnownLanguageTag( $code ) ];
101    }
102
103    /**
104     * Handler for isValidCode
105     * @internal
106     * @param string $code
107     * @return bool[]
108     */
109    public function isValidCode( $code ) {
110        $this->checkType( 'isValidCode', 1, $code, 'string' );
111        return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidCode( $code ) ];
112    }
113
114    /**
115     * Handler for isValidBuiltInCode
116     * @internal
117     * @param string $code
118     * @return bool[]
119     */
120    public function isValidBuiltInCode( $code ) {
121        $this->checkType( 'isValidBuiltInCode', 1, $code, 'string' );
122        return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidBuiltInCode( $code ) ];
123    }
124
125    /**
126     * Handler for fetchLanguageName
127     * @internal
128     * @param string $code
129     * @param null|string $inLanguage
130     * @return string[]
131     */
132    public function fetchLanguageName( $code, $inLanguage ) {
133        $this->checkType( 'fetchLanguageName', 1, $code, 'string' );
134        $this->checkTypeOptional( 'fetchLanguageName', 2, $inLanguage, 'string', LanguageNameUtils::AUTONYMS );
135        return [ MediaWikiServices::getInstance()->getLanguageNameUtils()
136            ->getLanguageName( $code, $inLanguage ) ];
137    }
138
139    /**
140     * Handler for fetchLanguageNames
141     * @internal
142     * @param null|string $inLanguage
143     * @param null|string $include
144     * @return string[][]
145     */
146    public function fetchLanguageNames( $inLanguage, $include ) {
147        $this->checkTypeOptional( 'fetchLanguageNames', 1, $inLanguage, 'string', LanguageNameUtils::AUTONYMS );
148        $this->checkTypeOptional( 'fetchLanguageNames', 2, $include, 'string', LanguageNameUtils::DEFINED );
149        return [ MediaWikiServices::getInstance()->getLanguageNameUtils()
150            ->getLanguageNames( $inLanguage, $include ) ];
151    }
152
153    /**
154     * Handler for fetchLanguageNames
155     * @internal
156     * @param string $code
157     * @return string[][]
158     */
159    public function getFallbacksFor( $code ) {
160        $this->checkType( 'getFallbacksFor', 1, $code, 'string' );
161        $ret = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $code );
162        // Make 1-based
163        if ( count( $ret ) ) {
164            $ret = array_combine( range( 1, count( $ret ) ), $ret );
165        }
166        return [ $ret ];
167    }
168
169    /**
170     * Handler for toBcp47Code
171     * @internal
172     * @param string $code a MediaWiki-internal code
173     * @return string[] a BCP-47 language tag
174     */
175    public function toBcp47Code( $code ) {
176        $this->checkType( 'toBcp47Code', 1, $code, 'string' );
177        $ret = LanguageCode::bcp47( $code );
178        return [ $ret ];
179    }
180
181    /**
182     * Language object method handler
183     * @internal
184     * @param string $name
185     * @param array $args
186     * @return array
187     * @throws LuaError
188     */
189    public function languageMethod( string $name, array $args ): array {
190        if ( !is_string( $args[0] ?? null ) ) {
191            throw new LuaError(
192                "invalid code property of language object when calling $name"
193            );
194        }
195        $code = array_shift( $args );
196        if ( !isset( $this->langCache[$code] ) ) {
197            if ( count( $this->langCache ) > $this->maxLangCacheSize ) {
198                throw new LuaError( 'too many language codes requested' );
199            }
200            $services = MediaWikiServices::getInstance();
201            if ( $services->getLanguageNameUtils()->isValidCode( $code ) ) {
202                $this->langCache[$code] = $services->getLanguageFactory()->getLanguage( $code );
203            } else {
204                throw new LuaError( "language code '$code' is invalid" );
205            }
206        }
207        $lang = $this->langCache[$code];
208        switch ( $name ) {
209            // Zero arguments
210            case 'isRTL':
211                return [ $lang->$name() ];
212
213            // One string argument passed straight through
214            case 'lcfirst':
215            case 'ucfirst':
216            case 'lc':
217            case 'uc':
218            case 'caseFold':
219                $this->checkType( $name, 1, $args[0], 'string' );
220                return [ $lang->$name( $args[0] ) ];
221
222            case 'parseFormattedNumber':
223                if ( is_numeric( $args[0] ) ) {
224                    $args[0] = strval( $args[0] );
225                }
226                if ( $this->getLuaType( $args[0] ) !== 'string' ) {
227                    // Be like tonumber(), return nil instead of erroring out
228                    return [ null ];
229                }
230                return [ $lang->$name( $args[0] ) ];
231
232            // Custom handling
233            default:
234                return $this->$name( $lang, $args );
235        }
236    }
237
238    /**
239     * convertPlural handler
240     * @internal
241     * @param Language $lang
242     * @param array $args
243     * @return array
244     */
245    public function convertPlural( $lang, $args ) {
246        $number = array_shift( $args );
247        $this->checkType( 'convertPlural', 1, $number, 'number' );
248        if ( is_array( $args[0] ) ) {
249            $args = $args[0];
250        }
251        $forms = array_values( array_map( 'strval', $args ) );
252        return [ $lang->convertPlural( $number, $forms ) ];
253    }
254
255    /**
256     * convertGrammar handler
257     * @internal
258     * @param Language $lang
259     * @param array $args
260     * @return array
261     */
262    public function convertGrammar( $lang, $args ) {
263        $this->checkType( 'convertGrammar', 1, $args[0], 'string' );
264        $this->checkType( 'convertGrammar', 2, $args[1], 'string' );
265        return [ $lang->convertGrammar( $args[0], $args[1] ) ];
266    }
267
268    /**
269     * gender handler
270     * @internal
271     * @param Language $lang
272     * @param array $args
273     * @return array
274     */
275    public function gender( $lang, $args ) {
276        $this->checkType( 'gender', 1, $args[0], 'string' );
277        $username = trim( array_shift( $args ) );
278
279        if ( is_array( $args[0] ) ) {
280            $args = $args[0];
281        }
282        $forms = array_values( array_map( 'strval', $args ) );
283
284        // Shortcuts
285        if ( count( $forms ) === 0 ) {
286            return [ '' ];
287        } elseif ( count( $forms ) === 1 ) {
288            return [ $forms[0] ];
289        }
290
291        if ( $username === 'male' || $username === 'female' ) {
292            $gender = $username;
293        } else {
294            $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
295            // default
296            $gender = $userOptionsLookup->getDefaultOption( 'gender' );
297
298            // Check for "User:" prefix
299            $title = Title::newFromText( $username );
300            if ( $title && $title->getNamespace() === NS_USER ) {
301                $username = $title->getText();
302            }
303
304            // check parameter, or use the ParserOptions if in interface message
305            $user = User::newFromName( $username );
306            if ( $user ) {
307                $genderCache = MediaWikiServices::getInstance()->getGenderCache();
308                $gender = $genderCache->getGenderOf( $user, __METHOD__ );
309            } elseif ( $username === '' ) {
310                $parserOptions = $this->getParserOptions();
311                if ( $parserOptions->getInterfaceMessage() ) {
312                    $genderCache = MediaWikiServices::getInstance()->getGenderCache();
313                    $gender = $genderCache->getGenderOf( $parserOptions->getUserIdentity(), __METHOD__ );
314                }
315            }
316        }
317        return [ $lang->gender( $gender, $forms ) ];
318    }
319
320    /**
321     * formatNum handler
322     * @internal
323     * @param Language $lang
324     * @param array $args
325     * @return array
326     */
327    public function formatNum( $lang, $args ) {
328        $num = $args[0];
329        $this->checkType( 'formatNum', 1, $num, 'number' );
330        if ( is_infinite( $num ) ) {
331            throw new LuaError( "bad argument #1 to 'formatNum' (infinite)" );
332        }
333        if ( is_nan( $num ) ) {
334            throw new LuaError( "bad argument #1 to 'formatNum' (NaN)" );
335        }
336
337        $noCommafy = false;
338        if ( isset( $args[1] ) ) {
339            $this->checkType( 'formatNum', 2, $args[1], 'table' );
340            $options = $args[1];
341            $noCommafy = !empty( $options['noCommafy'] );
342        }
343        if ( $noCommafy ) {
344            return [ $lang->formatNumNoSeparators( $num ) ];
345        } else {
346            return [ $lang->formatNum( $num ) ];
347        }
348    }
349
350    /**
351     * formatDate handler
352     * @internal
353     * @param Language $lang
354     * @param array $args
355     * @return array
356     * @throws LuaError
357     */
358    public function formatDate( $lang, $args ) {
359        $this->checkType( 'formatDate', 1, $args[0], 'string' );
360        $this->checkTypeOptional( 'formatDate', 2, $args[1], 'string', '' );
361        $this->checkTypeOptional( 'formatDate', 3, $args[2], 'boolean', false );
362
363        [ $format, $date, $local ] = $args;
364        $langcode = $lang->getCode();
365
366        if ( $date === '' ) {
367            $cacheKey = $this->getParserOptions()->getTimestamp();
368            $timestamp = new MWTimestamp( $cacheKey );
369            $date = $timestamp->getTimestamp( TS_ISO_8601 );
370            $useTTL = true;
371        } else {
372            # Correct for DateTime interpreting 'XXXX' as XX:XX o'clock
373            if ( preg_match( '/^[0-9]{4}$/', $date ) ) {
374                $date = '00:00 ' . $date;
375            }
376
377            $cacheKey = $date;
378            $useTTL = false;
379        }
380
381        if ( isset( $this->timeCache[$format][$cacheKey][$langcode][$local] ) ) {
382            $ttl = $this->timeCache[$format][$cacheKey][$langcode][$local][1];
383            if ( $useTTL && $ttl !== null ) {
384                $this->getEngine()->setTTL( $ttl );
385            }
386            return [ $this->timeCache[$format][$cacheKey][$langcode][$local][0] ];
387        }
388
389        # Default input timezone is UTC.
390        try {
391            $utc = new DateTimeZone( 'UTC' );
392            $dateObject = new DateTime( $date, $utc );
393        } catch ( TimeoutException $ex ) {
394            // Unfortunately DateTime throws a generic Exception, but we can't
395            // ignore an exception generated by the RequestTimeout library.
396            throw $ex;
397        } catch ( Exception $ex ) {
398            throw new LuaError( "bad argument #2 to 'formatDate': invalid timestamp '$date'" );
399        }
400
401        # Set output timezone.
402        if ( $local ) {
403            $localtimezone = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Localtimezone );
404            if ( isset( $localtimezone ) ) {
405                $tz = new DateTimeZone( $localtimezone );
406            } else {
407                $tz = new DateTimeZone( date_default_timezone_get() );
408            }
409        } else {
410            $tz = $utc;
411        }
412        $dateObject->setTimezone( $tz );
413        # Generate timestamp
414        $ts = $dateObject->format( 'YmdHis' );
415
416        if ( $ts < 0 ) {
417            throw new LuaError( "mw.language:formatDate() only supports years from 0" );
418        } elseif ( $ts >= 100000000000000 ) {
419            throw new LuaError( "mw.language:formatDate() only supports years up to 9999" );
420        }
421
422        $ttl = null;
423        $ret = $lang->sprintfDate( $format, $ts, $tz, $ttl );
424        $this->timeCache[$format][$cacheKey][$langcode][$local] = [ $ret, $ttl ];
425        if ( $useTTL && $ttl !== null ) {
426            $this->getEngine()->setTTL( $ttl );
427        }
428        return [ $ret ];
429    }
430
431    /**
432     * formatDuration handler
433     * @internal
434     * @param Language $lang
435     * @param array $args
436     * @return array
437     */
438    public function formatDuration( $lang, $args ) {
439        $this->checkType( 'formatDuration', 1, $args[0], 'number' );
440        $this->checkTypeOptional( 'formatDuration', 2, $args[1], 'table', [] );
441
442        [ $seconds, $chosenIntervals ] = $args;
443        $chosenIntervals = array_values( $chosenIntervals );
444
445        $ret = $lang->formatDuration( $seconds, $chosenIntervals );
446        return [ $ret ];
447    }
448
449    /**
450     * getDurationIntervals handler
451     * @internal
452     * @param Language $lang
453     * @param array $args
454     * @return array
455     */
456    public function getDurationIntervals( $lang, $args ) {
457        $this->checkType( 'getDurationIntervals', 1, $args[0], 'number' );
458        $this->checkTypeOptional( 'getDurationIntervals', 2, $args[1], 'table', [] );
459
460        [ $seconds, $chosenIntervals ] = $args;
461        $chosenIntervals = array_values( $chosenIntervals );
462
463        $ret = $lang->getDurationIntervals( $seconds, $chosenIntervals );
464        return [ $ret ];
465    }
466}