mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-12-22 12:52:52 +00:00
e126de6a78
timeout while converting timestamps should be a timeout for the whole parse of that wikitext and not hidden by a invalid time. Just let the exception bubble up to the caller See ca71e69f for more information Change-Id: I1f44e45dcc9b052717814990a3f5ce3a1bdf9d26
448 lines
12 KiB
PHP
448 lines
12 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon;
|
|
|
|
use DateTime;
|
|
use DateTimeZone;
|
|
use Exception;
|
|
use Language;
|
|
use MediaWiki\Languages\LanguageNameUtils;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Title\Title;
|
|
use MWTimestamp;
|
|
use User;
|
|
use Wikimedia\RequestTimeout\TimeoutException;
|
|
|
|
class LanguageLibrary extends LibraryBase {
|
|
/** @var Language[] */
|
|
public $langCache = [];
|
|
/** @var array */
|
|
public $timeCache = [];
|
|
/** @var int */
|
|
public $maxLangCacheSize;
|
|
|
|
public function register() {
|
|
// Pre-populate the language cache
|
|
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
|
|
$this->langCache[$contLang->getCode()] = $contLang;
|
|
$this->maxLangCacheSize = $this->getEngine()->getOption( 'maxLangCacheSize' );
|
|
|
|
$statics = [
|
|
'getContLangCode',
|
|
'isSupportedLanguage',
|
|
'isKnownLanguageTag',
|
|
'isValidCode',
|
|
'isValidBuiltInCode',
|
|
'fetchLanguageName',
|
|
'fetchLanguageNames',
|
|
'getFallbacksFor',
|
|
];
|
|
$methods = [
|
|
'lcfirst',
|
|
'ucfirst',
|
|
'lc',
|
|
'uc',
|
|
'caseFold',
|
|
'formatNum',
|
|
'formatDate',
|
|
'formatDuration',
|
|
'getDurationIntervals',
|
|
'parseFormattedNumber',
|
|
'convertPlural',
|
|
'convertGrammar',
|
|
'gender',
|
|
'isRTL',
|
|
];
|
|
$lib = [];
|
|
foreach ( $statics as $name ) {
|
|
$lib[$name] = [ $this, $name ];
|
|
}
|
|
foreach ( $methods as $name ) {
|
|
$lib[$name] = function () use ( $name ) {
|
|
$args = func_get_args();
|
|
return $this->languageMethod( $name, $args );
|
|
};
|
|
}
|
|
return $this->getEngine()->registerInterface( 'mw.language.lua', $lib );
|
|
}
|
|
|
|
/**
|
|
* Handler for getContLangCode
|
|
* @internal
|
|
* @return string[]
|
|
*/
|
|
public function getContLangCode() {
|
|
return [ MediaWikiServices::getInstance()->getContentLanguage()->getCode() ];
|
|
}
|
|
|
|
/**
|
|
* Handler for isSupportedLanguage
|
|
* @internal
|
|
* @param string $code
|
|
* @return bool[]
|
|
*/
|
|
public function isSupportedLanguage( $code ) {
|
|
$this->checkType( 'isSupportedLanguage', 1, $code, 'string' );
|
|
return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isSupportedLanguage( $code ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for isKnownLanguageTag
|
|
* @internal
|
|
* @param string $code
|
|
* @return bool[]
|
|
*/
|
|
public function isKnownLanguageTag( $code ) {
|
|
$this->checkType( 'isKnownLanguageTag', 1, $code, 'string' );
|
|
return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isKnownLanguageTag( $code ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for isValidCode
|
|
* @internal
|
|
* @param string $code
|
|
* @return bool[]
|
|
*/
|
|
public function isValidCode( $code ) {
|
|
$this->checkType( 'isValidCode', 1, $code, 'string' );
|
|
return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidCode( $code ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for isValidBuiltInCode
|
|
* @internal
|
|
* @param string $code
|
|
* @return bool[]
|
|
*/
|
|
public function isValidBuiltInCode( $code ) {
|
|
$this->checkType( 'isValidBuiltInCode', 1, $code, 'string' );
|
|
return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidBuiltInCode( $code ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for fetchLanguageName
|
|
* @internal
|
|
* @param string $code
|
|
* @param null|string $inLanguage
|
|
* @return string[]
|
|
*/
|
|
public function fetchLanguageName( $code, $inLanguage ) {
|
|
$this->checkType( 'fetchLanguageName', 1, $code, 'string' );
|
|
$this->checkTypeOptional( 'fetchLanguageName', 2, $inLanguage, 'string', LanguageNameUtils::AUTONYMS );
|
|
return [ MediaWikiServices::getInstance()->getLanguageNameUtils()
|
|
->getLanguageName( $code, $inLanguage ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for fetchLanguageNames
|
|
* @internal
|
|
* @param null|string $inLanguage
|
|
* @param null|string $include
|
|
* @return string[][]
|
|
*/
|
|
public function fetchLanguageNames( $inLanguage, $include ) {
|
|
$this->checkTypeOptional( 'fetchLanguageNames', 1, $inLanguage, 'string', LanguageNameUtils::AUTONYMS );
|
|
$this->checkTypeOptional( 'fetchLanguageNames', 2, $include, 'string', LanguageNameUtils::DEFINED );
|
|
return [ MediaWikiServices::getInstance()->getLanguageNameUtils()
|
|
->getLanguageNames( $inLanguage, $include ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for fetchLanguageNames
|
|
* @internal
|
|
* @param string $code
|
|
* @return string[][]
|
|
*/
|
|
public function getFallbacksFor( $code ) {
|
|
$this->checkType( 'getFallbacksFor', 1, $code, 'string' );
|
|
$ret = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $code );
|
|
// Make 1-based
|
|
if ( count( $ret ) ) {
|
|
$ret = array_combine( range( 1, count( $ret ) ), $ret );
|
|
}
|
|
return [ $ret ];
|
|
}
|
|
|
|
/**
|
|
* Language object method handler
|
|
* @internal
|
|
* @param string $name
|
|
* @param array $args
|
|
* @return array
|
|
* @throws LuaError
|
|
*/
|
|
public function languageMethod( $name, $args ) {
|
|
$name = strval( $name );
|
|
$code = array_shift( $args );
|
|
if ( !isset( $this->langCache[$code] ) ) {
|
|
if ( count( $this->langCache ) > $this->maxLangCacheSize ) {
|
|
throw new LuaError( 'too many language codes requested' );
|
|
}
|
|
$services = MediaWikiServices::getInstance();
|
|
if ( $services->getLanguageNameUtils()->isValidCode( $code ) ) {
|
|
$this->langCache[$code] = $services->getLanguageFactory()->getLanguage( $code );
|
|
} else {
|
|
throw new LuaError( "language code '$code' is invalid" );
|
|
}
|
|
}
|
|
$lang = $this->langCache[$code];
|
|
switch ( $name ) {
|
|
// Zero arguments
|
|
case 'isRTL':
|
|
return [ $lang->$name() ];
|
|
|
|
// One string argument passed straight through
|
|
case 'lcfirst':
|
|
case 'ucfirst':
|
|
case 'lc':
|
|
case 'uc':
|
|
case 'caseFold':
|
|
$this->checkType( $name, 1, $args[0], 'string' );
|
|
return [ $lang->$name( $args[0] ) ];
|
|
|
|
case 'parseFormattedNumber':
|
|
if ( is_numeric( $args[0] ) ) {
|
|
$args[0] = strval( $args[0] );
|
|
}
|
|
if ( $this->getLuaType( $args[0] ) !== 'string' ) {
|
|
// Be like tonumber(), return nil instead of erroring out
|
|
return [ null ];
|
|
}
|
|
return [ $lang->$name( $args[0] ) ];
|
|
|
|
// Custom handling
|
|
default:
|
|
return $this->$name( $lang, $args );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* convertPlural handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public function convertPlural( $lang, $args ) {
|
|
$number = array_shift( $args );
|
|
$this->checkType( 'convertPlural', 1, $number, 'number' );
|
|
if ( is_array( $args[0] ) ) {
|
|
$args = $args[0];
|
|
}
|
|
$forms = array_values( array_map( 'strval', $args ) );
|
|
return [ $lang->convertPlural( $number, $forms ) ];
|
|
}
|
|
|
|
/**
|
|
* convertGrammar handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public function convertGrammar( $lang, $args ) {
|
|
$this->checkType( 'convertGrammar', 1, $args[0], 'string' );
|
|
$this->checkType( 'convertGrammar', 2, $args[1], 'string' );
|
|
return [ $lang->convertGrammar( $args[0], $args[1] ) ];
|
|
}
|
|
|
|
/**
|
|
* gender handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public function gender( $lang, $args ) {
|
|
$this->checkType( 'gender', 1, $args[0], 'string' );
|
|
$username = trim( array_shift( $args ) );
|
|
|
|
if ( is_array( $args[0] ) ) {
|
|
$args = $args[0];
|
|
}
|
|
$forms = array_values( array_map( 'strval', $args ) );
|
|
|
|
// Shortcuts
|
|
if ( count( $forms ) === 0 ) {
|
|
return [ '' ];
|
|
} elseif ( count( $forms ) === 1 ) {
|
|
return [ $forms[0] ];
|
|
}
|
|
|
|
if ( $username === 'male' || $username === 'female' ) {
|
|
$gender = $username;
|
|
} else {
|
|
$userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
|
|
// default
|
|
$gender = $userOptionsLookup->getDefaultOption( 'gender' );
|
|
|
|
// Check for "User:" prefix
|
|
$title = Title::newFromText( $username );
|
|
if ( $title && $title->getNamespace() === NS_USER ) {
|
|
$username = $title->getText();
|
|
}
|
|
|
|
// check parameter, or use the ParserOptions if in interface message
|
|
$user = User::newFromName( $username );
|
|
if ( $user ) {
|
|
$genderCache = MediaWikiServices::getInstance()->getGenderCache();
|
|
$gender = $genderCache->getGenderOf( $user, __METHOD__ );
|
|
} elseif ( $username === '' ) {
|
|
$parserOptions = $this->getParserOptions();
|
|
if ( $parserOptions->getInterfaceMessage() ) {
|
|
$genderCache = MediaWikiServices::getInstance()->getGenderCache();
|
|
$gender = $genderCache->getGenderOf( $parserOptions->getUserIdentity(), __METHOD__ );
|
|
}
|
|
}
|
|
}
|
|
return [ $lang->gender( $gender, $forms ) ];
|
|
}
|
|
|
|
/**
|
|
* formatNum handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public function formatNum( $lang, $args ) {
|
|
$num = $args[0];
|
|
$this->checkType( 'formatNum', 1, $num, 'number' );
|
|
if ( is_infinite( $num ) ) {
|
|
throw new LuaError( "bad argument #1 to 'formatNum' (infinite)" );
|
|
}
|
|
if ( is_nan( $num ) ) {
|
|
throw new LuaError( "bad argument #1 to 'formatNum' (NaN)" );
|
|
}
|
|
|
|
$noCommafy = false;
|
|
if ( isset( $args[1] ) ) {
|
|
$this->checkType( 'formatNum', 2, $args[1], 'table' );
|
|
$options = $args[1];
|
|
$noCommafy = !empty( $options['noCommafy'] );
|
|
}
|
|
if ( $noCommafy ) {
|
|
return [ $lang->formatNumNoSeparators( $num ) ];
|
|
} else {
|
|
return [ $lang->formatNum( $num ) ];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* formatDate handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
* @throws LuaError
|
|
*/
|
|
public function formatDate( $lang, $args ) {
|
|
$this->checkType( 'formatDate', 1, $args[0], 'string' );
|
|
$this->checkTypeOptional( 'formatDate', 2, $args[1], 'string', '' );
|
|
$this->checkTypeOptional( 'formatDate', 3, $args[2], 'boolean', false );
|
|
|
|
list( $format, $date, $local ) = $args;
|
|
$langcode = $lang->getCode();
|
|
|
|
if ( $date === '' ) {
|
|
$cacheKey = $this->getParserOptions()->getTimestamp();
|
|
$timestamp = new MWTimestamp( $cacheKey );
|
|
$date = $timestamp->getTimestamp( TS_ISO_8601 );
|
|
$useTTL = true;
|
|
} else {
|
|
# Correct for DateTime interpreting 'XXXX' as XX:XX o'clock
|
|
if ( preg_match( '/^[0-9]{4}$/', $date ) ) {
|
|
$date = '00:00 ' . $date;
|
|
}
|
|
|
|
$cacheKey = $date;
|
|
$useTTL = false;
|
|
}
|
|
|
|
if ( isset( $this->timeCache[$format][$cacheKey][$langcode][$local] ) ) {
|
|
$ttl = $this->timeCache[$format][$cacheKey][$langcode][$local][1];
|
|
if ( $useTTL && $ttl !== null ) {
|
|
$this->getEngine()->setTTL( $ttl );
|
|
}
|
|
return [ $this->timeCache[$format][$cacheKey][$langcode][$local][0] ];
|
|
}
|
|
|
|
# Default input timezone is UTC.
|
|
try {
|
|
$utc = new DateTimeZone( 'UTC' );
|
|
$dateObject = new DateTime( $date, $utc );
|
|
} catch ( TimeoutException $ex ) {
|
|
// Unfortunately DateTime throws a generic Exception, but we can't
|
|
// ignore an exception generated by the RequestTimeout library.
|
|
throw $ex;
|
|
} catch ( Exception $ex ) {
|
|
throw new LuaError( "bad argument #2 to 'formatDate': invalid timestamp '$date'" );
|
|
}
|
|
|
|
# Set output timezone.
|
|
if ( $local ) {
|
|
global $wgLocaltimezone;
|
|
if ( isset( $wgLocaltimezone ) ) {
|
|
$tz = new DateTimeZone( $wgLocaltimezone );
|
|
} else {
|
|
$tz = new DateTimeZone( date_default_timezone_get() );
|
|
}
|
|
} else {
|
|
$tz = $utc;
|
|
}
|
|
$dateObject->setTimezone( $tz );
|
|
# Generate timestamp
|
|
$ts = $dateObject->format( 'YmdHis' );
|
|
|
|
if ( $ts < 0 ) {
|
|
throw new LuaError( "mw.language:formatDate() only supports years from 0" );
|
|
} elseif ( $ts >= 100000000000000 ) {
|
|
throw new LuaError( "mw.language:formatDate() only supports years up to 9999" );
|
|
}
|
|
|
|
$ttl = null;
|
|
$ret = $lang->sprintfDate( $format, $ts, $tz, $ttl );
|
|
$this->timeCache[$format][$cacheKey][$langcode][$local] = [ $ret, $ttl ];
|
|
if ( $useTTL && $ttl !== null ) {
|
|
$this->getEngine()->setTTL( $ttl );
|
|
}
|
|
return [ $ret ];
|
|
}
|
|
|
|
/**
|
|
* formatDuration handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public function formatDuration( $lang, $args ) {
|
|
$this->checkType( 'formatDuration', 1, $args[0], 'number' );
|
|
$this->checkTypeOptional( 'formatDuration', 2, $args[1], 'table', [] );
|
|
|
|
list( $seconds, $chosenIntervals ) = $args;
|
|
$chosenIntervals = array_values( $chosenIntervals );
|
|
|
|
$ret = $lang->formatDuration( $seconds, $chosenIntervals );
|
|
return [ $ret ];
|
|
}
|
|
|
|
/**
|
|
* getDurationIntervals handler
|
|
* @internal
|
|
* @param Language $lang
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public function getDurationIntervals( $lang, $args ) {
|
|
$this->checkType( 'getDurationIntervals', 1, $args[0], 'number' );
|
|
$this->checkTypeOptional( 'getDurationIntervals', 2, $args[1], 'table', [] );
|
|
|
|
list( $seconds, $chosenIntervals ) = $args;
|
|
$chosenIntervals = array_values( $chosenIntervals );
|
|
|
|
$ret = $lang->getDurationIntervals( $seconds, $chosenIntervals );
|
|
return [ $ret ];
|
|
}
|
|
}
|