mediawiki-extensions-Scribunto/engines/LuaCommon/LuaCommon.php
Tim Starling 441943bd9b Do not allow access to setfenv() and getfenv() by default
Optionally remove setfenv and getfenv from the global environment in
which user code runs. This will improve the forwards-compatibility of
user code with Lua 5.2.

Porting to Lua 5.2 would still be a daunting project, of questionable
value, but at least only the internal code would need updating, and not
thousands of on-wiki modules. Compared to the environment changes, the
rest of the Lua 5.2 changes are relatively easy to simulate for
backwards compatibility.

Removed module() from the package module, since it depends on setfenv().
The native version of it is deprecated in Lua 5.2 for that reason.

Change-Id: I978903ca98943ac941833da13fe5027949f6b429
2012-05-31 15:02:04 +02:00

436 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'],
array( 'allowEnvFuncs' => $this->options['allowEnvFuncs'] ) );
}
/**
* 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;
}
}