mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-11-26 01:05:22 +00:00
8b27dc742d
To allow Lua libraries to mark functions as expensive, add an incrementExpensiveFunctionCount() method to Scribunto_LuaEngine that will call the corresponding Parser method and throw an error if the limit is exceeded. Also allow libraries to do the same thing from Lua by calling mw.incrementExpensiveFunctionCount(). Change-Id: I56fded32b1077eff3980371e9abc9b3b7581f7b5
566 lines
16 KiB
PHP
566 lines
16 KiB
PHP
<?php
|
|
|
|
abstract class Scribunto_LuaEngine extends ScribuntoEngineBase {
|
|
static $libraryClasses = array(
|
|
);
|
|
|
|
protected $loaded = false;
|
|
protected $executeModuleFunc, $interpreter;
|
|
protected $mw;
|
|
protected $currentFrame = false;
|
|
protected $expandCache = array();
|
|
protected $loadedLibraries = array();
|
|
|
|
const MAX_EXPAND_CACHE_SIZE = 100;
|
|
|
|
var $libraryPaths = array(
|
|
'.',
|
|
'luabit',
|
|
'stringtools',
|
|
);
|
|
|
|
/**
|
|
* Create a new interpreter object
|
|
*/
|
|
abstract function newInterpreter();
|
|
|
|
protected function newModule( $text, $chunkName ) {
|
|
return new Scribunto_LuaModule( $this, $text, $chunkName );
|
|
}
|
|
|
|
public function newLuaError( $message, $params = array() ) {
|
|
return new Scribunto_LuaError( $message, $this->getDefaultExceptionParams() + $params );
|
|
}
|
|
|
|
public function destroy() {
|
|
// Break reference cycles
|
|
$this->interpreter = null;
|
|
$this->executeModuleFunc = null;
|
|
$this->mw = null;
|
|
$this->expandCache = null;
|
|
parent::destroy();
|
|
}
|
|
|
|
/**
|
|
* Initialise the interpreter and the base environment
|
|
*/
|
|
public function load() {
|
|
if( $this->loaded ) {
|
|
return;
|
|
}
|
|
$this->loaded = true;
|
|
|
|
$this->interpreter = $this->newInterpreter();
|
|
|
|
$funcs = array(
|
|
'loadPackage',
|
|
'parentFrameExists',
|
|
'getExpandedArgument',
|
|
'getAllExpandedArguments',
|
|
'expandTemplate',
|
|
'preprocess',
|
|
'incrementExpensiveFunctionCount',
|
|
);
|
|
|
|
$lib = array();
|
|
foreach ( $funcs as $name ) {
|
|
$lib[$name] = array( $this, $name );
|
|
}
|
|
|
|
$this->mw = $this->registerInterface( 'mw.lua', $lib,
|
|
array( 'allowEnvFuncs' => $this->options['allowEnvFuncs'] ) );
|
|
|
|
foreach ( self::$libraryClasses as $name => $class ) {
|
|
$this->loadedLibraries[$name] = new $class( $this );
|
|
$this->loadedLibraries[$name]->register();
|
|
}
|
|
}
|
|
|
|
public function registerInterface( $moduleFileName, $interfaceFuncs, $setupOptions = array() ) {
|
|
$this->interpreter->registerLibrary( 'mw_interface', $interfaceFuncs );
|
|
$package = $this->loadLibraryFromFile( "{$this->getLuaLibDir()}/{$moduleFileName}" );
|
|
if ( $package['setupInterface'] ) {
|
|
$this->interpreter->callFunction( $package['setupInterface'], $setupOptions );
|
|
}
|
|
return $package;
|
|
}
|
|
|
|
public function getLuaLibDir() {
|
|
return dirname( __FILE__ ) .'/lualib';
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public abstract function getPerformanceCharacteristics();
|
|
|
|
/**
|
|
* Get the current interpreter object
|
|
*/
|
|
public function getInterpreter() {
|
|
$this->load();
|
|
return $this->interpreter;
|
|
}
|
|
|
|
/**
|
|
* Execute a module chunk in a new isolated environment
|
|
*/
|
|
public function executeModule( $chunk ) {
|
|
return $this->getInterpreter()->callFunction( $this->mw['executeModule'], $chunk );
|
|
}
|
|
|
|
/**
|
|
* Execute a module function chunk
|
|
*/
|
|
public function executeFunctionChunk( $chunk, $frame ) {
|
|
$oldFrame = $this->currentFrame;
|
|
$this->currentFrame = $frame;
|
|
$result = $this->getInterpreter()->callFunction(
|
|
$this->mw['executeFunction'],
|
|
$chunk );
|
|
$this->currentFrame = $oldFrame;
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Load a library from the given file and execute it in the base environment.
|
|
* Return the export list, or null if there isn't one.
|
|
*/
|
|
protected function loadLibraryFromFile( $fileName ) {
|
|
$code = file_get_contents( $fileName );
|
|
if ( $code === false ) {
|
|
throw new MWException( 'Lua file does not exist: ' . $fileName );
|
|
}
|
|
# 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 isset( $ret[0] ) ? $ret[0] : null;
|
|
}
|
|
|
|
public function getGeSHiLanguage() {
|
|
return 'lua';
|
|
}
|
|
|
|
public function getCodeEditorLanguage() {
|
|
return 'lua';
|
|
}
|
|
|
|
public function runConsole( $params ) {
|
|
/**
|
|
* 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)\n" .
|
|
"local p = mw.executeModule(__init)\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 .= "return mw.allToString(" . substr( $params['question'], 1 ) . "), mw.getLogBuffer()\n";
|
|
} else {
|
|
$code .= $params['question'] . "\n" .
|
|
"return nil, mw.getLogBuffer()\n";
|
|
}
|
|
$code .= "end\n";
|
|
|
|
$contentModule = $this->newModule(
|
|
$params['content'], $params['title']->getPrefixedDBkey() );
|
|
$contentInit = $contentModule->getInitChunk();
|
|
|
|
$consoleModule = $this->newModule(
|
|
$code,
|
|
wfMessage( 'scribunto-console-current-src' )->text()
|
|
);
|
|
$consoleInit = $consoleModule->getInitChunk();
|
|
$ret = $this->executeModule( $consoleInit );
|
|
$func = $ret[0];
|
|
$ret = $this->getInterpreter()->callFunction( $func, $contentInit );
|
|
return array(
|
|
'return' => isset( $ret[0] ) ? $ret[0] : null,
|
|
'print' => isset( $ret[1] ) ? $ret[1] : '',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Workalike for luaL_checktype()
|
|
*
|
|
* @param $funcName The Lua function name, for use in error messages
|
|
* @param $args The argument array
|
|
* @param $index0 The zero-based argument index
|
|
* @param $type The type name as given by gettype()
|
|
* @param $msgType The type name used in the error message
|
|
*/
|
|
public function checkType( $funcName, $args, $index0, $type, $msgType ) {
|
|
if ( !isset( $args[$index0] ) || gettype( $args[$index0] ) !== $type ) {
|
|
$index1 = $index0 + 1;
|
|
throw new Scribunto_LuaError( "bad argument #$index1 to '$funcName' ($msgType expected)" );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Workalike for luaL_checkstring()
|
|
*
|
|
* @param $funcName The Lua function name, for use in error messages
|
|
* @param $args The argument array
|
|
* @param $index0 The zero-based argument index
|
|
*/
|
|
public function checkString( $funcName, $args, $index0 ) {
|
|
$this->checkType( $funcName, $args, $index0, 'string', 'string' );
|
|
}
|
|
|
|
/**
|
|
* Workalike for luaL_checknumber()
|
|
*
|
|
* @param $funcName The Lua function name, for use in error messages
|
|
* @param $args The argument array
|
|
* @param $index0 The zero-based argument index
|
|
*/
|
|
public function checkNumber( $funcName, $args, $index0 ) {
|
|
$this->checkType( $funcName, $args, $index0, 'double', 'number' );
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
function loadPackage( $name ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'loadPackage', $args, 0 );
|
|
|
|
foreach ( $this->libraryPaths as $path ) {
|
|
$fileName = $this->getLuaLibDir() . "/$path/$name.lua";
|
|
if ( !file_exists( $fileName ) ) {
|
|
continue;
|
|
}
|
|
$code = file_get_contents( $fileName );
|
|
$init = $this->interpreter->loadString( $code, "@$name.lua" );
|
|
return array( $init );
|
|
}
|
|
|
|
$title = Title::newFromText( $name );
|
|
if ( !$title || $title->getNamespace() != NS_MODULE ) {
|
|
return array();
|
|
}
|
|
|
|
$module = $this->fetchModuleFromParser( $title );
|
|
if ( $module ) {
|
|
return array( $module->getInitChunk() );
|
|
} else {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function for the implementation of frame methods
|
|
*/
|
|
protected function getFrameById( $frameId ) {
|
|
if ( !$this->currentFrame ) {
|
|
return false;
|
|
}
|
|
if ( $frameId === 'parent' ) {
|
|
if ( !isset( $this->currentFrame->parent ) ) {
|
|
return false;
|
|
} else {
|
|
return $this->currentFrame->parent;
|
|
}
|
|
} elseif ( $frameId === 'current' ) {
|
|
return $this->currentFrame;
|
|
} else {
|
|
throw new Scribunto_LuaError( 'invalid frame ID' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for parentFrameExists()
|
|
*/
|
|
function parentFrameExists() {
|
|
$frame = $this->getFrameById( 'parent' );
|
|
return array( $frame !== false );
|
|
}
|
|
|
|
/**
|
|
* Handler for getExpandedArgument()
|
|
*/
|
|
function getExpandedArgument( $frameId, $name ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'getExpandedArgument', $args, 0 );
|
|
|
|
$frame = $this->getFrameById( $frameId );
|
|
if ( $frame === false ) {
|
|
return array();
|
|
}
|
|
$result = $frame->getArgument( $name );
|
|
if ( $result === false ) {
|
|
return array();
|
|
} else {
|
|
return array( $result );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for getAllExpandedArguments()
|
|
*/
|
|
function getAllExpandedArguments( $frameId ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
if ( $frame === false ) {
|
|
return array();
|
|
}
|
|
return array( $frame->getArguments() );
|
|
}
|
|
|
|
/**
|
|
* Handler for expandTemplate()
|
|
*/
|
|
function expandTemplate( $frameId, $titleText, $args ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
if ( $frame === false ) {
|
|
throw new Scribunto_LuaError( 'attempt to call mw.expandTemplate with no frame' );
|
|
}
|
|
|
|
$title = Title::newFromText( $titleText, NS_TEMPLATE );
|
|
if ( !$title ) {
|
|
throw new Scribunto_LuaError( 'expandTemplate: invalid title' );
|
|
}
|
|
|
|
if ( $frame->depth >= $this->parser->mOptions->getMaxTemplateDepth() ) {
|
|
throw new Scribunto_LuaError( 'expandTemplate: template depth limit exceeded' );
|
|
}
|
|
if ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
|
|
throw new Scribunto_LuaError( 'expandTemplate: template inclusion denied' );
|
|
}
|
|
|
|
list( $dom, $finalTitle ) = $this->parser->getTemplateDom( $title );
|
|
if ( $dom === false ) {
|
|
throw new Scribunto_LuaError( "expandTemplate: template \"$titleText\" does not exist" );
|
|
}
|
|
if ( !$frame->loopCheck( $finalTitle ) ) {
|
|
throw new Scribunto_LuaError( 'expandTemplate: template loop detected' );
|
|
}
|
|
|
|
$newFrame = $this->parser->getPreprocessor()->newCustomFrame( $args );
|
|
$text = $this->doCachedExpansion( $newFrame, $dom,
|
|
array(
|
|
'template' => $finalTitle->getPrefixedDBkey(),
|
|
'args' => $args
|
|
) );
|
|
return array( $text );
|
|
}
|
|
|
|
/**
|
|
* Handler for preprocess()
|
|
*/
|
|
function preprocess( $frameId, $text ) {
|
|
$args = func_get_args();
|
|
$this->checkString( 'preprocess', $args, 0 );
|
|
|
|
$frame = $this->getFrameById( $frameId );
|
|
|
|
if ( !$frame ) {
|
|
throw new Scribunto_LuaError( 'attempt to call mw.preprocess with no frame' );
|
|
}
|
|
$text = $this->doCachedExpansion( $frame, $text,
|
|
array(
|
|
'inputText' => $text,
|
|
'args' => $frame->getArguments()
|
|
) );
|
|
return array( $text );
|
|
}
|
|
|
|
/**
|
|
* Increment the expensive function count, and throw if limit exceeded
|
|
*
|
|
* @return null
|
|
*/
|
|
public function incrementExpensiveFunctionCount() {
|
|
if ( !$this->getParser()->incrementExpensiveFunctionCount() ) {
|
|
throw new Scribunto_LuaError( "too many expensive function calls" );
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function doCachedExpansion( $frame, $input, $cacheKey ) {
|
|
$hash = md5( serialize( $cacheKey ) );
|
|
if ( !isset( $this->expandCache[$hash] ) ) {
|
|
if ( is_scalar( $input ) ) {
|
|
$dom = $this->parser->getPreprocessor()->preprocessToObj(
|
|
$input, Parser::PTD_FOR_INCLUSION );
|
|
} else {
|
|
$dom = $input;
|
|
}
|
|
if ( count( $this->expandCache ) > self::MAX_EXPAND_CACHE_SIZE ) {
|
|
reset( $this->expandCache );
|
|
$oldHash = key( $this->expandCache );
|
|
unset( $this->expandCache[$oldHash] );
|
|
}
|
|
$this->expandCache[$hash] = $frame->expand( $dom );
|
|
}
|
|
return $this->expandCache[$hash];
|
|
}
|
|
}
|
|
|
|
class Scribunto_LuaModule extends ScribuntoModuleBase {
|
|
protected $initChunk;
|
|
|
|
/**
|
|
* @param $name string
|
|
* @return Scribunto_LuaFunction
|
|
*/
|
|
protected function newFunction( $name ) {
|
|
return new Scribunto_LuaFunction( $this, $name, $contents ); // FIXME: $contents is undefined
|
|
}
|
|
|
|
public function validate() {
|
|
try {
|
|
$this->getInitChunk();
|
|
} catch ( ScribuntoException $e ) {
|
|
return $e->toStatus();
|
|
}
|
|
return Status::newGood();
|
|
}
|
|
|
|
/**
|
|
* Execute the module function and return the export table.
|
|
*/
|
|
public function execute() {
|
|
$init = $this->getInitChunk();
|
|
$ret = $this->engine->executeModule( $init );
|
|
if( !$ret ) {
|
|
throw $this->engine->newException( 'scribunto-lua-noreturn' );
|
|
}
|
|
if( !is_array( $ret[0] ) ) {
|
|
throw $this->engine->newException( 'scribunto-lua-notarrayreturn' );
|
|
}
|
|
return $ret[0];
|
|
}
|
|
|
|
/**
|
|
* Get the chunk which, when called, will return the export table.
|
|
*/
|
|
public function getInitChunk() {
|
|
if ( !$this->initChunk ) {
|
|
$this->initChunk = $this->engine->getInterpreter()->loadString(
|
|
$this->code,
|
|
// Prepending an "=" to the chunk name avoids truncation or a "[string" prefix
|
|
'=' . $this->chunkName );
|
|
}
|
|
return $this->initChunk;
|
|
}
|
|
|
|
/**
|
|
* Invoke a function within the module. Return the expanded wikitext result.
|
|
*/
|
|
public function invoke( $name, $frame ) {
|
|
$exports = $this->execute();
|
|
if ( !isset( $exports[$name] ) ) {
|
|
throw $this->engine->newException( 'scribunto-common-nosuchfunction' );
|
|
}
|
|
|
|
$result = $this->engine->executeFunctionChunk( $exports[$name], $frame );
|
|
if ( isset( $result[0] ) ) {
|
|
return $result[0];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class Scribunto_LuaError extends ScribuntoException {
|
|
var $luaMessage, $lineMap = array();
|
|
|
|
function __construct( $message, $options = array() ) {
|
|
$this->luaMessage = $message;
|
|
$options = $options + array( 'args' => array( $message ) );
|
|
if ( isset( $options['module'] ) && isset( $options['line'] ) ) {
|
|
$msg = 'scribunto-lua-error-location';
|
|
} else {
|
|
$msg = 'scribunto-lua-error';
|
|
}
|
|
|
|
parent::__construct( $msg, $options );
|
|
}
|
|
|
|
function getLuaMessage() {
|
|
return $this->luaMessage;
|
|
}
|
|
|
|
function setLineMap( $map ) {
|
|
$this->lineMap = $map;
|
|
}
|
|
|
|
/**
|
|
* @param array $options Options for message processing. Currently supports:
|
|
* $options['msgOptions']['content'] to use content language.
|
|
* @return bool|string
|
|
*/
|
|
function getScriptTraceHtml( $options = array() ) {
|
|
if ( !isset( $this->params['trace'] ) ) {
|
|
return false;
|
|
}
|
|
if ( isset( $options['msgOptions'] ) ){
|
|
$msgOptions = $options['msgOptions'];
|
|
} else {
|
|
$msgOptions = array();
|
|
}
|
|
|
|
$s = '<ol class="scribunto-trace">';
|
|
foreach ( $this->params['trace'] as $info ) {
|
|
$short_src = $srcdefined = $info['short_src'];
|
|
$currentline = $info['currentline'];
|
|
|
|
$src = htmlspecialchars( $short_src );
|
|
if ( $currentline > 0 ) {
|
|
$src .= ':' . htmlspecialchars( $currentline );
|
|
|
|
$title = Title::newFromText( $short_src );
|
|
if ( $title && $title->getNamespace() === NS_MODULE ) {
|
|
$title->setFragment( '#mw-ce-l' . $currentline );
|
|
$src = Html::rawElement( 'a',
|
|
array( 'href' => $title->getFullURL( 'action=edit' ) ),
|
|
$src );
|
|
}
|
|
}
|
|
|
|
if ( strval( $info['namewhat'] ) !== '' ) {
|
|
$function = wfMessage( 'scribunto-lua-in-function', $info['name'] );
|
|
in_array( 'content', $msgOptions ) ?
|
|
$function = $function->inContentLanguage()->text() :
|
|
$function = $function->text();
|
|
} elseif ( $info['what'] == 'main' ) {
|
|
$function = wfMessage( 'scribunto-lua-in-main' );
|
|
in_array( 'content', $msgOptions ) ?
|
|
$function = $function->inContentLanguage()->text() :
|
|
$function = $function->text();
|
|
} elseif ( $info['what'] == 'C' || $info['what'] == 'tail' ) {
|
|
$function = '?';
|
|
}
|
|
|
|
$backtraceLine = wfMessage( 'scribunto-lua-backtrace-line', "<strong>$src</strong>", $function );
|
|
in_array( 'content', $msgOptions ) ?
|
|
$backtraceLine = $backtraceLine->inContentLanguage()->text() :
|
|
$backtraceLine = $backtraceLine->text();
|
|
|
|
$s .= "<li>\n\t" . $backtraceLine . "\n</li>\n";
|
|
}
|
|
$s .= '</ol>';
|
|
return $s;
|
|
}
|
|
}
|