mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-11-26 01:05:22 +00:00
6bc11ff615
* Implemented the new parser interface based on a frame object, as described in the design document and wikitech-l. * Added parser tests for the new interface. * Removed {{script:}} parser function * Allow named parameters to {{#invoke:}} * Don't trim the return value * If a function invoked by #invoke returns multiple values, concatenate them into a single string. * If there is an error during parse, show the error message as an HTML comment as well as via JavaScript. This makes parser test construction easier, and probably makes debugging easier also. * Rename mw_internal to mw_php to clarify its role. It is now strictly a private Lua -> PHP interface function table. * Protect mw.setup() against multiple invocation. * Fixed a bug in Scribunto_LuaStandaloneInterpreter::receiveMessage(): large packets caused fread() to return with less than the requested amount of data, which previously caused an exception. It's necessary to check for EOF and to repeat the read to get all data. The receive function on the Lua side does not suffer from this problem. * In the standalone engine, fixed a bug in the interpretation of null return values from PHP callbacks. This should return no values to Lua. * Updated the Lua unit tests to account for the fact that functions are now forced to return strings. * Updated the getfenv and setfenv tests to account for the extra stack level introduced by mw.executeFunction(). Change-Id: If8fdecdfc91ebe7bd4b1dae8489ccbdeb6bbf5ce
435 lines
12 KiB
PHP
435 lines
12 KiB
PHP
<?php
|
|
|
|
abstract class Scribunto_LuaEngine extends ScribuntoEngineBase {
|
|
protected $loaded = false;
|
|
protected $executeModuleFunc, $interpreter;
|
|
protected $mw;
|
|
protected $currentFrame = false;
|
|
protected $expandCache = 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 );
|
|
}
|
|
|
|
/**
|
|
* Initialise the interpreter and the base environment
|
|
*/
|
|
public function load() {
|
|
if( $this->loaded ) {
|
|
return;
|
|
}
|
|
$this->loaded = true;
|
|
|
|
$this->interpreter = $this->newInterpreter();
|
|
$this->mw = $this->loadLibraryFromFile( dirname( __FILE__ ) .'/lualib/mw.lua' );
|
|
|
|
$this->loadLibraryFromFile( dirname( __FILE__ ) .'/lualib/package.lua' );
|
|
|
|
$this->interpreter->registerLibrary( 'mw_php',
|
|
array(
|
|
'loadPackage' => array( $this, 'loadPackage' ),
|
|
'parentFrameExists' => array( $this, 'parentFrameExists' ),
|
|
'getExpandedArgument' => array( $this, 'getExpandedArgument' ),
|
|
'getAllExpandedArguments' => array( $this, 'getAllExpandedArguments' ),
|
|
'expandTemplate' => array( $this, 'expandTemplate' ),
|
|
'preprocess' => array( $this, 'preprocess' ),
|
|
) );
|
|
|
|
$this->interpreter->callFunction( $this->mw['setup'] );
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
}
|
|
|
|
/**
|
|
* 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 mw_php.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 = dirname( __FILE__ ) . "/lualib/$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 mw_php.parentFrameExists()
|
|
*/
|
|
function parentFrameExists() {
|
|
$frame = $this->getFrameById( 'parent' );
|
|
return array( $frame !== false );
|
|
}
|
|
|
|
/**
|
|
* Handler for mw_php.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 mw_php.getAllExpandedArguments()
|
|
*/
|
|
function getAllExpandedArguments( $frameId ) {
|
|
$frame = $this->getFrameById( $frameId );
|
|
if ( $frame === false ) {
|
|
return array();
|
|
}
|
|
return array( $frame->getArguments() );
|
|
}
|
|
|
|
/**
|
|
* Handler for mw_php.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 ) {
|
|
return array();
|
|
}
|
|
|
|
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 ) {
|
|
return array();
|
|
}
|
|
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 mw_php.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' );
|
|
}
|
|
$dom = $this->parser->getPreprocessor()->preprocessToObj( $text, Parser::PTD_FOR_INCLUSION );
|
|
$text = $this->doCachedExpansion( $frame, $dom,
|
|
array(
|
|
'inputText' => $text,
|
|
'args' => $frame->getArguments()
|
|
) );
|
|
return array( $text );
|
|
}
|
|
|
|
function doCachedExpansion( $frame, $dom, $cacheKey ) {
|
|
$hash = md5( serialize( $cacheKey ) );
|
|
if ( !isset( $this->expandCache[$hash] ) ) {
|
|
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;
|
|
|
|
protected function newFunction( $name ) {
|
|
return new Scribunto_LuaFunction( $this, $name, $contents );
|
|
}
|
|
|
|
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;
|
|
|
|
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 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 ) {
|
|
$src = htmlspecialchars( $info['short_src'] );
|
|
if ( $info['currentline'] > 0 ) {
|
|
$src .= ':' . htmlspecialchars( $info['currentline'] );
|
|
|
|
$title = Title::newFromText( $info['short_src'] );
|
|
if ( $title && $title->getNamespace() === NS_MODULE ) {
|
|
$title->setFragment( '#mw-ce-l' . $info['currentline'] );
|
|
$src = Html::rawElement( 'a',
|
|
array( 'href' => $title->getFullURL( 'action=edit' ) ),
|
|
$src );
|
|
}
|
|
}
|
|
|
|
if ( strval( $info['namewhat'] ) !== '' ) {
|
|
$function = wfMsgExt( 'scribunto-lua-in-function', $msgOptions, $info['name'] );
|
|
} elseif ( $info['what'] == 'main' ) {
|
|
$function = wfMsgExt( 'scribunto-lua-in-main', $msgOptions );
|
|
} elseif ( $info['what'] == 'C' || $info['what'] == 'tail' ) {
|
|
$function = '?';
|
|
} else {
|
|
$function = wfMsgExt( 'scribunto-lua-in-function-at',
|
|
$msgOptions, $info['short_src'], $info['linedefined'] );
|
|
}
|
|
$s .= "<li>\n\t" .
|
|
wfMsgExt( 'scribunto-lua-backtrace-line', $msgOptions, "<strong>$src</strong>", $function ) .
|
|
"\n</li>\n";
|
|
}
|
|
$s .= '</ol>';
|
|
return $s;
|
|
}
|
|
}
|