mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-12-18 10:53:37 +00:00
319d7bba52
Implicitly marking parameter $... as nullable is deprecated in PHP 8.4. The explicit nullable type must be used instead. Bug: T376276 Change-Id: I9cafaa51999fd2740da6c4fec9314fb9ace7c327
1040 lines
29 KiB
PHP
1040 lines
29 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon;
|
|
|
|
use Exception;
|
|
use MediaWiki\Extension\Scribunto\Engines\LuaSandbox\LuaSandboxInterpreter;
|
|
use MediaWiki\Extension\Scribunto\Scribunto;
|
|
use MediaWiki\Extension\Scribunto\ScribuntoContent;
|
|
use MediaWiki\Extension\Scribunto\ScribuntoEngineBase;
|
|
use MediaWiki\Extension\Scribunto\ScribuntoException;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\Json\FormatJson;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Parser\Parser;
|
|
use MediaWiki\Parser\PPFrame;
|
|
use MediaWiki\Title\Title;
|
|
use RuntimeException;
|
|
use Wikimedia\ScopedCallback;
|
|
|
|
abstract class LuaEngine extends ScribuntoEngineBase {
|
|
/**
|
|
* Libraries to load. See also the 'ScribuntoExternalLibraries' hook.
|
|
* @var array<string,class-string<LibraryBase>> Maps module names to PHP classes or definition arrays
|
|
*/
|
|
protected static $libraryClasses = [
|
|
'mw.site' => SiteLibrary::class,
|
|
'mw.uri' => UriLibrary::class,
|
|
'mw.ustring' => UstringLibrary::class,
|
|
'mw.language' => LanguageLibrary::class,
|
|
'mw.message' => MessageLibrary::class,
|
|
'mw.title' => TitleLibrary::class,
|
|
'mw.text' => TextLibrary::class,
|
|
'mw.html' => HtmlLibrary::class,
|
|
'mw.hash' => HashLibrary::class,
|
|
];
|
|
|
|
/**
|
|
* Paths for modules that may be loaded from Lua. See also the
|
|
* 'ScribuntoExternalLibraryPaths' hook.
|
|
* @var string[] Paths
|
|
*/
|
|
protected static $libraryPaths = [
|
|
'.',
|
|
'luabit',
|
|
'ustring',
|
|
];
|
|
|
|
/** @var bool */
|
|
protected $loaded = false;
|
|
|
|
/**
|
|
* @var LuaInterpreter|null
|
|
*/
|
|
protected $interpreter;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $mw;
|
|
|
|
/**
|
|
* @var array<string,?PPFrame>
|
|
*/
|
|
protected $currentFrames = [];
|
|
/**
|
|
* @var array<string,string>|null
|
|
*/
|
|
protected $expandCache = [];
|
|
/**
|
|
* @var array<string,array|class-string<LibraryBase>>
|
|
*/
|
|
protected $availableLibraries = [];
|
|
|
|
private const MAX_EXPAND_CACHE_SIZE = 100;
|
|
|
|
/**
|
|
* If luasandbox is installed and usable then use it,
|
|
* otherwise
|
|
*
|
|
* @param array $options
|
|
* @return LuaEngine
|
|
*/
|
|
public static function newAutodetectEngine( array $options ) {
|
|
$engineConf = MediaWikiServices::getInstance()->getMainConfig()->get( 'ScribuntoEngineConf' );
|
|
$engine = 'luastandalone';
|
|
try {
|
|
LuaSandboxInterpreter::checkLuaSandboxVersion();
|
|
$engine = 'luasandbox';
|
|
} catch ( LuaInterpreterNotFoundError | LuaInterpreterBadVersionError $e ) {
|
|
// pass
|
|
}
|
|
|
|
unset( $options['factory'] );
|
|
|
|
// @phan-suppress-next-line PhanTypeMismatchReturnSuperType
|
|
return Scribunto::newEngine( $options + $engineConf[$engine] );
|
|
}
|
|
|
|
/**
|
|
* Create a new interpreter object
|
|
* @return LuaInterpreter
|
|
*/
|
|
abstract protected function newInterpreter();
|
|
|
|
/**
|
|
* @param string $text
|
|
* @param string|bool $chunkName
|
|
* @return LuaModule
|
|
*/
|
|
protected function newModule( $text, $chunkName ) {
|
|
return new LuaModule( $this, $text, $chunkName );
|
|
}
|
|
|
|
/**
|
|
* @param string $message
|
|
* @param array $params
|
|
* @return LuaError
|
|
*/
|
|
public function newLuaError( $message, $params = [] ) {
|
|
return new LuaError( $message, $this->getDefaultExceptionParams() + $params );
|
|
}
|
|
|
|
public function destroy() {
|
|
// Break reference cycles
|
|
$this->interpreter = null;
|
|
$this->mw = [];
|
|
$this->expandCache = null;
|
|
parent::destroy();
|
|
}
|
|
|
|
/**
|
|
* Initialise the interpreter and the base environment
|
|
*/
|
|
public function load() {
|
|
if ( $this->loaded ) {
|
|
return;
|
|
}
|
|
$this->loaded = true;
|
|
|
|
try {
|
|
$this->interpreter = $this->newInterpreter();
|
|
|
|
$funcs = [
|
|
'loadPackage',
|
|
'loadPHPLibrary',
|
|
'frameExists',
|
|
'newChildFrame',
|
|
'getExpandedArgument',
|
|
'getAllExpandedArguments',
|
|
'expandTemplate',
|
|
'callParserFunction',
|
|
'preprocess',
|
|
'incrementExpensiveFunctionCount',
|
|
'isSubsting',
|
|
'getFrameTitle',
|
|
'setTTL',
|
|
'addWarning',
|
|
'loadJsonData',
|
|
];
|
|
|
|
$lib = [];
|
|
foreach ( $funcs as $name ) {
|
|
$lib[$name] = [ $this, $name ];
|
|
}
|
|
|
|
$this->registerInterface( 'mwInit.lua', [] );
|
|
$this->mw = $this->registerInterface( 'mw.lua', $lib,
|
|
[ 'allowEnvFuncs' => $this->options['allowEnvFuncs'] ] );
|
|
|
|
$this->availableLibraries = $this->getLibraries( 'lua', self::$libraryClasses );
|
|
foreach ( $this->availableLibraries as $name => $def ) {
|
|
$this->instantiatePHPLibrary( $name, $def, false );
|
|
}
|
|
} catch ( Exception $ex ) {
|
|
$this->loaded = false;
|
|
$this->interpreter = null;
|
|
throw $ex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a Lua Library
|
|
*
|
|
* This should be called from the library's PHP module's register() method.
|
|
*
|
|
* The value for $interfaceFuncs is used to populate the mw_interface
|
|
* global that is defined when the library's Lua module is loaded. Values
|
|
* must be PHP callables, which will be seen in Lua as functions.
|
|
*
|
|
* @param string $moduleFileName The path to the Lua portion of the library
|
|
* (absolute, or relative to $this->getLuaLibDir())
|
|
* @param array<string,callable> $interfaceFuncs Populates mw_interface
|
|
* @param array $setupOptions Passed to the modules setupInterface() method.
|
|
* @return array Lua package
|
|
*/
|
|
public function registerInterface( $moduleFileName, $interfaceFuncs, $setupOptions = [] ) {
|
|
$this->interpreter->registerLibrary( 'mw_interface', $interfaceFuncs );
|
|
$moduleFileName = $this->normalizeModuleFileName( $moduleFileName );
|
|
$package = $this->loadLibraryFromFile( $moduleFileName );
|
|
if ( !empty( $package['setupInterface'] ) ) {
|
|
$this->interpreter->callFunction( $package['setupInterface'], $setupOptions );
|
|
}
|
|
return $package;
|
|
}
|
|
|
|
/**
|
|
* Return the base path for Lua modules.
|
|
* @return string
|
|
*/
|
|
public function getLuaLibDir() {
|
|
return __DIR__ . '/lualib';
|
|
}
|
|
|
|
/**
|
|
* Normalize a lua module to its full path. If path does not look like an
|
|
* absolute path (i.e. begins with DIRECTORY_SEPARATOR or "X:"), prepend
|
|
* getLuaLibDir()
|
|
*
|
|
* @param string $fileName name of the lua module file
|
|
* @return string
|
|
*/
|
|
protected function normalizeModuleFileName( $fileName ) {
|
|
if ( !preg_match( '<^(?:[a-zA-Z]:)?' . preg_quote( DIRECTORY_SEPARATOR ) . '>', $fileName ) ) {
|
|
$fileName = "{$this->getLuaLibDir()}/{$fileName}";
|
|
}
|
|
return $fileName;
|
|
}
|
|
|
|
/**
|
|
* Get performance characteristics of the Lua engine/interpreter
|
|
*
|
|
* phpCallsRequireSerialization: boolean
|
|
* whether calls between PHP and Lua functions require (slow)
|
|
* serialization of parameters and return values
|
|
*
|
|
* @return array
|
|
*/
|
|
abstract public function getPerformanceCharacteristics();
|
|
|
|
/**
|
|
* Get the current interpreter object
|
|
* @return LuaInterpreter
|
|
*/
|
|
public function getInterpreter() {
|
|
$this->load();
|
|
return $this->interpreter;
|
|
}
|
|
|
|
/**
|
|
* Replaces the list of current frames, and return a ScopedCallback that
|
|
* will reset them when it goes out of scope.
|
|
*
|
|
* @param PPFrame|null $frame If null, an empty frame with no parent will be used
|
|
* @return ScopedCallback
|
|
*/
|
|
private function setupCurrentFrames( ?PPFrame $frame = null ) {
|
|
if ( !$frame ) {
|
|
$frame = $this->getParser()->getPreprocessor()->newFrame();
|
|
}
|
|
|
|
$oldFrames = $this->currentFrames;
|
|
$oldExpandCache = $this->expandCache;
|
|
$this->currentFrames = [
|
|
'current' => $frame,
|
|
'parent' => $frame->parent ?? null,
|
|
];
|
|
$this->expandCache = [];
|
|
|
|
return new ScopedCallback( function () use ( $oldFrames, $oldExpandCache ) {
|
|
$this->currentFrames = $oldFrames;
|
|
$this->expandCache = $oldExpandCache;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Execute a module chunk in a new isolated environment, and return the specified function
|
|
* @param mixed $chunk As accepted by LuaInterpreter::callFunction()
|
|
* @param string $functionName
|
|
* @param PPFrame|null $frame
|
|
* @return mixed
|
|
* @throws ScribuntoException
|
|
*/
|
|
public function executeModule( $chunk, $functionName, $frame ) {
|
|
// $resetFrames is a ScopedCallback, so it has a purpose even though it appears unused.
|
|
$resetFrames = $this->setupCurrentFrames( $frame );
|
|
|
|
$retval = $this->getInterpreter()->callFunction(
|
|
$this->mw['executeModule'], $chunk, $functionName
|
|
);
|
|
if ( !$retval[0] ) {
|
|
// If we get here, it means we asked for an element from the table the module returned,
|
|
// but it returned something other than a table. In this case, $retval[1] contains the type
|
|
// of what it did returned, instead of the value we asked for.
|
|
throw $this->newException(
|
|
'scribunto-lua-notarrayreturn', [ 'args' => [ $retval[1] ] ]
|
|
);
|
|
}
|
|
return $retval[1];
|
|
}
|
|
|
|
/**
|
|
* Execute a module function chunk
|
|
* @param mixed $chunk As accepted by LuaInterpreter::callFunction()
|
|
* @param PPFrame|null $frame
|
|
* @return array
|
|
*/
|
|
public function executeFunctionChunk( $chunk, $frame ) {
|
|
// $resetFrames is a ScopedCallback, so it has a purpose even though it appears unused.
|
|
$resetFrames = $this->setupCurrentFrames( $frame );
|
|
|
|
return $this->getInterpreter()->callFunction(
|
|
$this->mw['executeFunction'],
|
|
$chunk );
|
|
}
|
|
|
|
/**
|
|
* Get data logged by modules
|
|
* @return string Logged data
|
|
*/
|
|
protected function getLogBuffer() {
|
|
if ( !$this->loaded ) {
|
|
return '';
|
|
}
|
|
try {
|
|
$log = $this->getInterpreter()->callFunction( $this->mw['getLogBuffer'] );
|
|
return $log[0];
|
|
} catch ( ScribuntoException $ex ) {
|
|
// Probably time expired, ignore it.
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format the logged data for HTML output
|
|
* @param string $logs Logged data
|
|
* @param bool $localize Whether to localize the message key
|
|
* @return string HTML
|
|
*/
|
|
protected function formatHtmlLogs( $logs, $localize ) {
|
|
$keyMsg = wfMessage( 'scribunto-limitreport-logs' );
|
|
if ( !$localize ) {
|
|
$keyMsg->inLanguage( 'en' )->useDatabase( false );
|
|
}
|
|
return Html::openElement( 'tr' ) .
|
|
Html::rawElement( 'th', [ 'colspan' => 2 ], $keyMsg->parse() ) .
|
|
Html::closeElement( 'tr' ) .
|
|
Html::openElement( 'tr' ) .
|
|
Html::openElement( 'td', [ 'colspan' => 2 ] ) .
|
|
Html::openElement( 'div', [ 'class' => 'mw-collapsible mw-collapsed' ] ) .
|
|
Html::element( 'pre', [ 'class' => 'scribunto-limitreport-logs' ], $logs ) .
|
|
Html::closeElement( 'div' ) .
|
|
Html::closeElement( 'td' ) .
|
|
Html::closeElement( 'tr' );
|
|
}
|
|
|
|
/**
|
|
* Load a library from the given file and execute it in the base environment.
|
|
* @param string $fileName File name/path to load
|
|
* @return array|null the export list, or null if there isn't one.
|
|
*/
|
|
protected function loadLibraryFromFile( $fileName ) {
|
|
static $cache = null;
|
|
|
|
$objectcachefactory = MediaWikiServices::getInstance()->getObjectCacheFactory();
|
|
|
|
if ( !$cache ) {
|
|
$cache = $objectcachefactory->getLocalServerInstance( CACHE_HASH );
|
|
}
|
|
|
|
$mtime = filemtime( $fileName );
|
|
if ( $mtime === false ) {
|
|
throw new RuntimeException( 'Lua file does not exist: ' . $fileName );
|
|
}
|
|
|
|
$cacheKey = $cache->makeGlobalKey( __CLASS__, $fileName );
|
|
$fileData = $cache->get( $cacheKey );
|
|
|
|
$code = false;
|
|
if ( $fileData ) {
|
|
[ $code, $cachedMtime ] = $fileData;
|
|
if ( $cachedMtime < $mtime ) {
|
|
$code = false;
|
|
}
|
|
}
|
|
if ( !$code ) {
|
|
$code = file_get_contents( $fileName );
|
|
if ( $code === false ) {
|
|
throw new RuntimeException( 'Lua file does not exist: ' . $fileName );
|
|
}
|
|
$cache->set( $cacheKey, [ $code, $mtime ], 60 * 5 );
|
|
}
|
|
|
|
# Prepending an "@" to the chunk name makes Lua think it is a filename
|
|
$module = $this->getInterpreter()->loadString( $code, '@' . basename( $fileName ) );
|
|
$ret = $this->getInterpreter()->callFunction( $module );
|
|
return $ret[0] ?? null;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function getGeSHiLanguage() {
|
|
return 'lua';
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function getCodeEditorLanguage() {
|
|
return 'lua';
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function runConsole( array $params ) {
|
|
// $resetFrames is a ScopedCallback, so it has a purpose even though it appears unused.
|
|
$resetFrames = $this->setupCurrentFrames();
|
|
|
|
/**
|
|
* TODO: provide some means for giving correct line numbers for errors
|
|
* in console input, and for producing an informative error message
|
|
* if there is an error in prevQuestions.
|
|
*
|
|
* Maybe each console line could be evaluated as a different chunk,
|
|
* apparently that's what lua.c does.
|
|
*/
|
|
$code = "return function (__init, exe)\n" .
|
|
"if not exe then exe = function(...) return true, ... end end\n" .
|
|
"local p = select(2, exe(__init) )\n" .
|
|
"__init, exe = nil, nil\n" .
|
|
"local print = mw.log\n";
|
|
foreach ( $params['prevQuestions'] as $q ) {
|
|
if ( substr( $q, 0, 1 ) === '=' ) {
|
|
$code .= "print(" . substr( $q, 1 ) . ")";
|
|
} else {
|
|
$code .= $q;
|
|
}
|
|
$code .= "\n";
|
|
}
|
|
$code .= "mw.clearLogBuffer()\n";
|
|
if ( substr( $params['question'], 0, 1 ) === '=' ) {
|
|
// Treat a statement starting with "=" as a return statement, like in lua.c
|
|
$code .= "local ret = mw.allToString(" . substr( $params['question'], 1 ) . ")\n" .
|
|
"return ret, mw.getLogBuffer()\n";
|
|
} else {
|
|
$code .= $params['question'] . "\n" .
|
|
"return nil, mw.getLogBuffer()\n";
|
|
}
|
|
$code .= "end\n";
|
|
|
|
if ( $params['title']->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
|
|
$contentModule = $this->newModule(
|
|
$params['content'], $params['title']->getPrefixedDBkey() );
|
|
$contentInit = $contentModule->getInitChunk();
|
|
$contentExe = $this->mw['executeModule'];
|
|
} else {
|
|
$contentInit = $params['content'];
|
|
$contentExe = null;
|
|
}
|
|
|
|
$consoleModule = $this->newModule(
|
|
$code,
|
|
wfMessage( 'scribunto-console-current-src' )->text()
|
|
);
|
|
$consoleInit = $consoleModule->getInitChunk();
|
|
$ret = $this->getInterpreter()->callFunction( $this->mw['executeModule'], $consoleInit, false );
|
|
$func = $ret[1];
|
|
$ret = $this->getInterpreter()->callFunction( $func, $contentInit, $contentExe );
|
|
|
|
return [
|
|
'return' => $ret[0] ?? null,
|
|
'print' => $ret[1] ?? '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Workalike for luaL_checktype()
|
|
*
|
|
* @param string $funcName The Lua function name, for use in error messages
|
|
* @param array $args The argument array
|
|
* @param int $index0 The zero-based argument index
|
|
* @param string|string[] $type The allowed type names as given by gettype()
|
|
* @param string $msgType The type name used in the error message
|
|
* @throws LuaError
|
|
*/
|
|
public function checkType( $funcName, $args, $index0, $type, $msgType ) {
|
|
if ( !is_array( $type ) ) {
|
|
$type = [ $type ];
|
|
}
|
|
if ( !isset( $args[$index0] ) || !in_array( gettype( $args[$index0] ), $type, true ) ) {
|
|
$index1 = $index0 + 1;
|
|
throw new LuaError( "bad argument #$index1 to '$funcName' ($msgType expected)" );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Workalike for luaL_checkstring()
|
|
*
|
|
* @param string $funcName The Lua function name, for use in error messages
|
|
* @param array $args The argument array
|
|
* @param int $index0 The zero-based argument index
|
|
*/
|
|
public function checkString( $funcName, $args, $index0 ) {
|
|
$this->checkType( $funcName, $args, $index0, 'string', 'string' );
|
|
}
|
|
|
|
/**
|
|
* Workalike for luaL_checknumber()
|
|
*
|
|
* @param string $funcName The Lua function name, for use in error messages
|
|
* @param array $args The argument array
|
|
* @param int $index0 The zero-based argument index
|
|
*/
|
|
public function checkNumber( $funcName, $args, $index0 ) {
|
|
$this->checkType( $funcName, $args, $index0, [ 'integer', 'double' ], 'number' );
|
|
}
|
|
|
|
/**
|
|
* Instantiate and register a library.
|
|
* @param string $name
|
|
* @param array|class-string<LibraryBase> $def
|
|
* @param bool $loadDeferred
|
|
* @return array|null
|
|
*/
|
|
private function instantiatePHPLibrary( $name, $def, $loadDeferred ) {
|
|
$def = $this->availableLibraries[$name];
|
|
if ( is_string( $def ) ) {
|
|
/** @var LibraryBase $class */
|
|
$class = new $def( $this );
|
|
} else {
|
|
if ( !$loadDeferred && !empty( $def['deferLoad'] ) ) {
|
|
return null;
|
|
}
|
|
if ( isset( $def['class'] ) ) {
|
|
/** @var LibraryBase $class */
|
|
$class = new $def['class']( $this );
|
|
} else {
|
|
throw new RuntimeException( "No class for library \"$name\"" );
|
|
}
|
|
}
|
|
return $class->register();
|
|
}
|
|
|
|
/**
|
|
* Handler for the loadPHPLibrary() callback. Register the specified
|
|
* library and return its function table. It's not necessary to cache the
|
|
* function table in the object instance, since there is caching in a
|
|
* wrapper on the Lua side.
|
|
* @internal
|
|
* @param string $name
|
|
* @return array
|
|
*/
|
|
public function loadPHPLibrary( $name ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'loadPHPLibrary', $args, 0 );
|
|
|
|
$ret = null;
|
|
if ( isset( $this->availableLibraries[$name] ) ) {
|
|
$ret = $this->instantiatePHPLibrary( $name, $this->availableLibraries[$name], true );
|
|
}
|
|
|
|
return [ $ret ];
|
|
}
|
|
|
|
/**
|
|
* Handler for the loadPackage() callback. Load the specified
|
|
* module and return its chunk. It's not necessary to cache the resulting
|
|
* chunk in the object instance, since there is caching in a wrapper on the
|
|
* Lua side.
|
|
* @internal
|
|
* @param string $name
|
|
* @return array
|
|
*/
|
|
public function loadPackage( $name ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'loadPackage', $args, 0 );
|
|
|
|
# This is what Lua does for its built-in loaders
|
|
$luaName = str_replace( '.', '/', $name ) . '.lua';
|
|
$paths = $this->getLibraryPaths( 'lua', self::$libraryPaths );
|
|
foreach ( $paths as $path ) {
|
|
$fileName = $this->normalizeModuleFileName( "$path/$luaName" );
|
|
if ( !file_exists( $fileName ) ) {
|
|
continue;
|
|
}
|
|
$code = file_get_contents( $fileName );
|
|
$init = $this->interpreter->loadString( $code, "@$luaName" );
|
|
return [ $init ];
|
|
}
|
|
|
|
$title = Title::newFromText( $name );
|
|
if ( !$title || !$title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
|
|
return [];
|
|
}
|
|
|
|
$module = $this->fetchModuleFromParser( $title );
|
|
if ( $module ) {
|
|
// @phan-suppress-next-line PhanUndeclaredMethod
|
|
return [ $module->getInitChunk() ];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function for the implementation of frame methods
|
|
*
|
|
* @param string $frameId
|
|
* @return PPFrame
|
|
*
|
|
* @throws LuaError
|
|
*/
|
|
protected function getFrameById( $frameId ) {
|
|
if ( $frameId === 'empty' ) {
|
|
return $this->getParser()->getPreprocessor()->newFrame();
|
|
} elseif ( isset( $this->currentFrames[$frameId] ) ) {
|
|
// @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive
|
|
return $this->currentFrames[$frameId];
|
|
} else {
|
|
throw new LuaError( 'invalid frame ID' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for frameExists()
|
|
*
|
|
* @internal
|
|
* @param string $frameId
|
|
* @return array
|
|
*/
|
|
public function frameExists( $frameId ) {
|
|
return [ $frameId === 'empty' || isset( $this->currentFrames[$frameId] ) ];
|
|
}
|
|
|
|
/**
|
|
* Handler for newChildFrame()
|
|
*
|
|
* @internal
|
|
* @param string $frameId
|
|
* @param string $title
|
|
* @param array $args
|
|
* @return array
|
|
* @throws LuaError
|
|
*/
|
|
public function newChildFrame( $frameId, $title, array $args ) {
|
|
if ( count( $this->currentFrames ) > 100 ) {
|
|
throw new LuaError( 'newChild: too many frames' );
|
|
}
|
|
|
|
$frame = $this->getFrameById( $frameId );
|
|
if ( $title === false ) {
|
|
$title = $frame->getTitle();
|
|
} else {
|
|
$title = Title::newFromText( $title );
|
|
if ( !$title ) {
|
|
throw new LuaError( 'newChild: invalid title' );
|
|
}
|
|
}
|
|
$args = $this->getParser()->getPreprocessor()->newPartNodeArray( $args );
|
|
$newFrame = $frame->newChild( $args, $title );
|
|
$newFrameId = 'frame' . count( $this->currentFrames );
|
|
$this->currentFrames[$newFrameId] = $newFrame;
|
|
return [ $newFrameId ];
|
|
}
|
|
|
|
/**
|
|
* Handler for getTitle()
|
|
*
|
|
* @internal
|
|
* @param string $frameId
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getFrameTitle( $frameId ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
return [ $frame->getTitle()->getPrefixedText() ];
|
|
}
|
|
|
|
/**
|
|
* Handler for setTTL()
|
|
* @internal
|
|
* @param int $ttl
|
|
*/
|
|
public function setTTL( $ttl ) {
|
|
$args = func_get_args();
|
|
$this->checkNumber( 'setTTL', $args, 0 );
|
|
|
|
$frame = $this->getFrameById( 'current' );
|
|
$frame->setTTL( $ttl );
|
|
}
|
|
|
|
/**
|
|
* Handler for getExpandedArgument()
|
|
* @internal
|
|
* @param string $frameId
|
|
* @param string $name
|
|
* @return array
|
|
*/
|
|
public function getExpandedArgument( $frameId, $name ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'getExpandedArgument', $args, 0 );
|
|
|
|
$frame = $this->getFrameById( $frameId );
|
|
$this->getInterpreter()->pauseUsageTimer();
|
|
$result = $frame->getArgument( $name );
|
|
if ( $result === false ) {
|
|
return [];
|
|
} else {
|
|
return [ $result ];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for getAllExpandedArguments()
|
|
* @internal
|
|
* @param string $frameId
|
|
* @return array
|
|
*/
|
|
public function getAllExpandedArguments( $frameId ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
$this->getInterpreter()->pauseUsageTimer();
|
|
return [ $frame->getArguments() ];
|
|
}
|
|
|
|
/**
|
|
* Handler for expandTemplate()
|
|
* @internal
|
|
* @param string $frameId
|
|
* @param string $titleText
|
|
* @param array $args
|
|
* @return array
|
|
* @throws LuaError
|
|
*/
|
|
public function expandTemplate( $frameId, $titleText, $args ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
$title = Title::newFromText( $titleText, NS_TEMPLATE );
|
|
if ( !$title ) {
|
|
throw new LuaError( "expandTemplate: invalid title \"$titleText\"" );
|
|
}
|
|
|
|
if ( $frame->depth >= $this->parser->getOptions()->getMaxTemplateDepth() ) {
|
|
throw new LuaError( 'expandTemplate: template depth limit exceeded' );
|
|
}
|
|
if ( MediaWikiServices::getInstance()->getNamespaceInfo()->isNonincludable( $title->getNamespace() ) ) {
|
|
throw new LuaError( 'expandTemplate: template inclusion denied' );
|
|
}
|
|
|
|
[ $dom, $finalTitle ] = $this->parser->getTemplateDom( $title );
|
|
if ( $dom === false ) {
|
|
throw new LuaError( "expandTemplate: template \"$titleText\" does not exist" );
|
|
}
|
|
if ( !$frame->loopCheck( $finalTitle ) ) {
|
|
throw new LuaError( 'expandTemplate: template loop detected' );
|
|
}
|
|
|
|
$fargs = $this->getParser()->getPreprocessor()->newPartNodeArray( $args );
|
|
$newFrame = $frame->newChild( $fargs, $finalTitle );
|
|
$text = $this->doCachedExpansion( $newFrame, $dom,
|
|
[
|
|
'frameId' => $frameId,
|
|
'template' => $finalTitle->getPrefixedDBkey(),
|
|
'args' => $args
|
|
] );
|
|
return [ $text ];
|
|
}
|
|
|
|
/**
|
|
* Handler for callParserFunction()
|
|
* @internal
|
|
* @param string $frameId
|
|
* @param string $function
|
|
* @param array $args
|
|
* @throws LuaError
|
|
* @return array
|
|
* @suppress PhanImpossibleCondition
|
|
*/
|
|
public function callParserFunction( $frameId, $function, $args ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
|
|
# Make zero-based, without screwing up named args
|
|
$args = array_merge( [], $args );
|
|
|
|
# Sort, since we can't rely on the order coming in from Lua
|
|
uksort( $args, static function ( $a, $b ) {
|
|
if ( is_int( $a ) !== is_int( $b ) ) {
|
|
return is_int( $a ) ? -1 : 1;
|
|
}
|
|
if ( is_int( $a ) ) {
|
|
return $a - $b;
|
|
}
|
|
return strcmp( $a, $b );
|
|
} );
|
|
|
|
# Be user-friendly
|
|
$colonPos = strpos( $function, ':' );
|
|
if ( $colonPos !== false ) {
|
|
array_unshift( $args, trim( substr( $function, $colonPos + 1 ) ) );
|
|
$function = substr( $function, 0, $colonPos );
|
|
}
|
|
if ( !isset( $args[0] ) ) {
|
|
# It's impossible to call a parser function from wikitext without
|
|
# supplying an arg 0. Insist that one be provided via Lua, too.
|
|
throw new LuaError( 'callParserFunction: At least one unnamed parameter ' .
|
|
'(the parameter that comes after the colon in wikitext) ' .
|
|
'must be provided'
|
|
);
|
|
}
|
|
|
|
$result = $this->parser->callParserFunction( $frame, $function, $args );
|
|
if ( !$result['found'] ) {
|
|
throw new LuaError( "callParserFunction: function \"$function\" was not found" );
|
|
}
|
|
|
|
# Set defaults for various flags
|
|
$result += [
|
|
'nowiki' => false,
|
|
'isChildObj' => false,
|
|
'isLocalObj' => false,
|
|
'isHTML' => false,
|
|
'title' => false,
|
|
];
|
|
|
|
$text = $result['text'];
|
|
if ( $result['isChildObj'] ) {
|
|
$fargs = $this->getParser()->getPreprocessor()->newPartNodeArray( $args );
|
|
$newFrame = $frame->newChild( $fargs, $result['title'] );
|
|
if ( $result['nowiki'] ) {
|
|
$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
|
|
} else {
|
|
$text = $newFrame->expand( $text );
|
|
}
|
|
}
|
|
if ( $result['isLocalObj'] && $result['nowiki'] ) {
|
|
$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
|
|
$result['isLocalObj'] = false;
|
|
}
|
|
|
|
# Replace raw HTML by a placeholder
|
|
if ( $result['isHTML'] ) {
|
|
$text = $this->parser->insertStripItem( $text );
|
|
} elseif ( $result['nowiki'] ) {
|
|
# Escape nowiki-style return values
|
|
$text = wfEscapeWikiText( $text );
|
|
}
|
|
|
|
if ( $result['isLocalObj'] ) {
|
|
$text = $frame->expand( $text );
|
|
}
|
|
|
|
return [ "$text" ];
|
|
}
|
|
|
|
/**
|
|
* Handler for preprocess()
|
|
* @internal
|
|
* @param string $frameId
|
|
* @param string $text
|
|
* @return array
|
|
* @throws LuaError
|
|
*/
|
|
public function preprocess( $frameId, $text ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'preprocess', $args, 0 );
|
|
|
|
$frame = $this->getFrameById( $frameId );
|
|
|
|
if ( !$frame ) {
|
|
throw new LuaError( 'attempt to call mw.preprocess with no frame' );
|
|
}
|
|
|
|
// Don't count the time for expanding all the frame arguments against
|
|
// the Lua time limit.
|
|
$this->getInterpreter()->pauseUsageTimer();
|
|
$frame->getArguments();
|
|
$this->getInterpreter()->unpauseUsageTimer();
|
|
|
|
$text = $this->doCachedExpansion( $frame, $text,
|
|
[
|
|
'frameId' => $frameId,
|
|
'inputText' => $text
|
|
] );
|
|
return [ $text ];
|
|
}
|
|
|
|
/**
|
|
* Increment the expensive function count, and throw if limit exceeded
|
|
*
|
|
* @internal
|
|
* @throws LuaError
|
|
* @return null
|
|
*/
|
|
public function incrementExpensiveFunctionCount() {
|
|
if ( !$this->getParser()->incrementExpensiveFunctionCount() ) {
|
|
throw new LuaError( "too many expensive function calls" );
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Adds a warning to be displayed upon preview
|
|
*
|
|
* @internal
|
|
* @param string $text wikitext
|
|
*/
|
|
public function addWarning( $text ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'addWarning', $args, 0 );
|
|
|
|
// Message localization has to happen on the Lua side
|
|
$this->getParser()->getOutput()->addWarningMsg(
|
|
'scribunto-lua-warning',
|
|
$text
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return whether the parser is currently substing
|
|
*
|
|
* @internal
|
|
* @return array
|
|
*/
|
|
public function isSubsting() {
|
|
// See Parser::braceSubstitution, OT_WIKI is the switch
|
|
return [ $this->getParser()->getOutputType() === Parser::OT_WIKI ];
|
|
}
|
|
|
|
/**
|
|
* @param PPFrame $frame
|
|
* @param string|array $input
|
|
* @param mixed $cacheKey
|
|
* @return string
|
|
*/
|
|
private function doCachedExpansion( $frame, $input, $cacheKey ) {
|
|
$hash = md5( serialize( $cacheKey ) );
|
|
if ( isset( $this->expandCache[$hash] ) ) {
|
|
return $this->expandCache[$hash];
|
|
}
|
|
|
|
if ( is_scalar( $input ) ) {
|
|
$input = str_replace( [ "\r\n", "\r" ], "\n", $input );
|
|
$dom = $this->parser->getPreprocessor()->preprocessToObj(
|
|
$input, $frame->depth ? Parser::PTD_FOR_INCLUSION : 0 );
|
|
} else {
|
|
$dom = $input;
|
|
}
|
|
$ret = $frame->expand( $dom );
|
|
if ( !$frame->isVolatile() ) {
|
|
if ( count( $this->expandCache ) > self::MAX_EXPAND_CACHE_SIZE ) {
|
|
reset( $this->expandCache );
|
|
$oldHash = key( $this->expandCache );
|
|
unset( $this->expandCache[$oldHash] );
|
|
}
|
|
$this->expandCache[$hash] = $ret;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Implements mw.loadJsonData()
|
|
*
|
|
* @param string $title Title text, type-checked in Lua
|
|
* @return string[]
|
|
*/
|
|
public function loadJsonData( $title ) {
|
|
$this->incrementExpensiveFunctionCount();
|
|
|
|
$titleObj = Title::newFromText( $title );
|
|
if ( !$titleObj || !$titleObj->exists() || !$titleObj->hasContentModel( CONTENT_MODEL_JSON ) ) {
|
|
throw new LuaError(
|
|
"bad argument #1 to 'mw.loadJsonData' ('$title' is not a valid JSON page)"
|
|
);
|
|
}
|
|
|
|
$parser = $this->getParser();
|
|
[ $text, $finalTitle ] = $parser->fetchTemplateAndTitle( $titleObj );
|
|
|
|
$json = FormatJson::decode( $text, true );
|
|
if ( is_array( $json ) ) {
|
|
$json = TextLibrary::reindexArrays( $json, false );
|
|
}
|
|
// We'll throw an error for non-tables on the Lua side
|
|
|
|
return [ $json ];
|
|
}
|
|
|
|
/**
|
|
* @see Content::updateRedirect
|
|
*
|
|
* @param ScribuntoContent $content
|
|
* @param Title $target
|
|
* @return ScribuntoContent
|
|
*/
|
|
public function updateRedirect( ScribuntoContent $content, Title $target ): ScribuntoContent {
|
|
if ( !$content->isRedirect() ) {
|
|
return $content;
|
|
}
|
|
return $this->makeRedirectContent( $target );
|
|
}
|
|
|
|
/**
|
|
* @see Content::getRedirectTarget
|
|
*
|
|
* @param ScribuntoContent $content
|
|
* @return Title|null
|
|
*/
|
|
public function getRedirectTarget( ScribuntoContent $content ) {
|
|
$text = $content->getText();
|
|
preg_match( '/^return require \[\[(.*?)\]\]/', $text, $matches );
|
|
if ( isset( $matches[1] ) ) {
|
|
$title = Title::newFromText( $matches[1] );
|
|
// Can only redirect to other Scribunto pages
|
|
if ( $title && $title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
|
|
// Have a title, check that the current content equals what
|
|
// the redirect content should be
|
|
if ( $content->equals( $this->makeRedirectContent( $title ) ) ) {
|
|
return $title;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @see ContentHandler::makeRedirectContent
|
|
* @param Title $destination
|
|
* @param string $text
|
|
* @return ScribuntoContent
|
|
*/
|
|
public function makeRedirectContent( Title $destination, $text = '' ) {
|
|
$targetPage = $destination->getPrefixedText();
|
|
$redirectText = "return require [[$targetPage]]";
|
|
return new ScribuntoContent( $redirectText );
|
|
}
|
|
|
|
/**
|
|
* @see ContentHandler::supportsRedirects
|
|
* @return bool true
|
|
*/
|
|
public function supportsRedirects(): bool {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class_alias( LuaEngine::class, 'Scribunto_LuaEngine' );
|