mediawiki-extensions-Scribunto/includes/Engines/LuaCommon/LanguageLibrary.php
Umherirrender e126de6a78 Rethrow TimeoutException when using mw.language.formatDate function
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
2023-10-02 21:03:02 +02:00

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 ];
}
}