mediawiki-extensions-Scribunto/includes/engines/LuaCommon/LuaCommon.php
Brad Jorsch ec103b6966 Scribunto_LuaError: Make ->getMessage() return UTF-8 text
It's easily possible for Lua to raise errors where the string is not
valid UTF-8. When we turn that into a Scribunto_LuaError, we should
normalize it so other things don't break.

Bug: T208689
Change-Id: Idc5514261e99d64222b86877dd0500d425a26988
2018-12-11 14:46:56 -05:00

1076 lines
30 KiB
PHP

<?php
use UtfNormal\Validator;
use Wikimedia\ScopedCallback;
abstract class Scribunto_LuaEngine extends ScribuntoEngineBase {
/**
* Libraries to load. See also the 'ScribuntoExternalLibraries' hook.
* @var array Maps module names to PHP classes or definition arrays
*/
protected static $libraryClasses = [
'mw.site' => 'Scribunto_LuaSiteLibrary',
'mw.uri' => 'Scribunto_LuaUriLibrary',
'mw.ustring' => 'Scribunto_LuaUstringLibrary',
'mw.language' => 'Scribunto_LuaLanguageLibrary',
'mw.message' => 'Scribunto_LuaMessageLibrary',
'mw.title' => 'Scribunto_LuaTitleLibrary',
'mw.text' => 'Scribunto_LuaTextLibrary',
'mw.html' => 'Scribunto_LuaHtmlLibrary',
'mw.hash' => 'Scribunto_LuaHashLibrary',
];
/**
* Paths for modules that may be loaded from Lua. See also the
* 'ScribuntoExternalLibraryPaths' hook.
* @var array Paths
*/
protected static $libraryPaths = [
'.',
'luabit',
'ustring',
];
protected $loaded = false;
/**
* @var Scribunto_LuaInterpreter
*/
protected $interpreter;
/**
* @var array
*/
protected $mw;
/**
* @var array
*/
protected $currentFrames = [];
protected $expandCache = [];
protected $availableLibraries = [];
const MAX_EXPAND_CACHE_SIZE = 100;
/**
* If luasandbox is installed and usable then use it,
* otherwise
*
* @param array $options
* @return Scribunto_LuaEngine
*/
public static function newAutodetectEngine( array $options ) {
global $wgScribuntoEngineConf;
$engine = 'luastandalone';
try {
Scribunto_LuaSandboxInterpreter::checkLuaSandboxVersion();
$engine = 'luasandbox';
} catch ( Scribunto_LuaInterpreterNotFoundError $e ) {
// pass
} catch ( Scribunto_LuaInterpreterBadVersionError $e ) {
// pass
}
unset( $options['factory'] );
return Scribunto::newEngine( $options + $wgScribuntoEngineConf[$engine] );
}
/**
* Create a new interpreter object
* @return Scribunto_LuaInterpreter
*/
abstract protected function newInterpreter();
/**
* @param string $text
* @param string|bool $chunkName
* @return Scribunto_LuaModule
*/
protected function newModule( $text, $chunkName ) {
return new Scribunto_LuaModule( $this, $text, $chunkName );
}
/**
* @param string $message
* @param array $params
* @return Scribunto_LuaError
*/
public function newLuaError( $message, $params = [] ) {
return new Scribunto_LuaError( $message, $this->getDefaultExceptionParams() + $params );
}
public function destroy() {
// Break reference cycles
$this->interpreter = 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;
try {
$this->interpreter = $this->newInterpreter();
$funcs = [
'loadPackage',
'loadPHPLibrary',
'frameExists',
'newChildFrame',
'getExpandedArgument',
'getAllExpandedArguments',
'expandTemplate',
'callParserFunction',
'preprocess',
'incrementExpensiveFunctionCount',
'isSubsting',
'getFrameTitle',
'setTTL',
'addWarning',
];
$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 $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 Scribunto_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' => isset( $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 Scribunto_LuaInterpreter::callFunction()
* @param string $functionName
* @param PPFrame|null $frame
* @return mixed
* @throws ScribuntoException
*/
public function executeModule( $chunk, $functionName, $frame ) {
$resetFrames = null;
if ( !$this->currentFrames || !isset( $this->currentFrames['current'] ) ) {
// Only reset frames if there isn't already current 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 Scribunto_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;
if ( !$cache ) {
$cache = ObjectCache::getLocalServerInstance( 'hash' );
}
$mtime = filemtime( $fileName );
if ( $mtime === false ) {
throw new MWException( 'Lua file does not exist: ' . $fileName );
}
$cacheKey = $cache->makeGlobalKey( __CLASS__, $fileName );
$fileData = $cache->get( $cacheKey );
$code = false;
if ( $fileData ) {
list( $code, $cachedMtime ) = $fileData;
if ( $cachedMtime < $mtime ) {
$code = false;
}
}
if ( !$code ) {
$code = file_get_contents( $fileName );
if ( $code === false ) {
throw new MWException( '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 isset( $ret[0] ) ? $ret[0] : null;
}
public function getGeSHiLanguage() {
return 'lua';
}
public function getCodeEditorLanguage() {
return 'lua';
}
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' => isset( $ret[0] ) ? $ret[0] : null,
'print' => isset( $ret[1] ) ? $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|array $type The allowed type names as given by gettype()
* @param string $msgType The type name used in the error message
* @throws Scribunto_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 Scribunto_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|string $def
* @param bool $loadDeferred
* @throws MWException
* @return array|null
*/
private function instantiatePHPLibrary( $name, $def, $loadDeferred ) {
$def = $this->availableLibraries[$name];
if ( is_string( $def ) ) {
$class = new $def( $this );
} else {
if ( !$loadDeferred && !empty( $def['deferLoad'] ) ) {
return null;
}
if ( isset( $def['class'] ) ) {
$class = new $def['class']( $this );
} else {
throw new MWException( "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 ) {
/** @suppress PhanUndeclaredMethod */
return [ $module->getInitChunk() ];
} else {
return [];
}
}
/**
* Helper function for the implementation of frame methods
*
* @param string $frameId
* @return PPFrame
*
* @throws Scribunto_LuaError
*/
protected function getFrameById( $frameId ) {
if ( $frameId === 'empty' ) {
return $this->getParser()->getPreprocessor()->newFrame();
} elseif ( isset( $this->currentFrames[$frameId] ) ) {
return $this->currentFrames[$frameId];
} else {
throw new Scribunto_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 Scribunto_LuaError
*/
public function newChildFrame( $frameId, $title, array $args ) {
if ( count( $this->currentFrames ) > 100 ) {
throw new Scribunto_LuaError( 'newChild: too many frames' );
}
$frame = $this->getFrameById( $frameId );
if ( $title === false ) {
$title = $frame->getTitle();
} else {
$title = Title::newFromText( $title );
if ( !$title ) {
throw new Scribunto_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 Scribunto_LuaError
*/
public function expandTemplate( $frameId, $titleText, $args ) {
$frame = $this->getFrameById( $frameId );
$title = Title::newFromText( $titleText, NS_TEMPLATE );
if ( !$title ) {
throw new Scribunto_LuaError( "expandTemplate: invalid title \"$titleText\"" );
}
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' );
}
$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 MWException
* @throws Scribunto_LuaError
* @return array
*/
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, 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 Scribunto_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 Scribunto_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 Scribunto_LuaError
*/
public 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' );
}
// 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 Scribunto_LuaError
* @return null
*/
public function incrementExpensiveFunctionCount() {
if ( !$this->getParser()->incrementExpensiveFunctionCount() ) {
throw new Scribunto_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 ) {
$this->getParser()->getOutput()->addWarning( $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()->OutputType() === Parser::OT_WIKI ];
}
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;
}
}
class Scribunto_LuaModule extends ScribuntoModuleBase {
/**
* @var mixed
*/
protected $initChunk;
/**
* @var Scribunto_LuaEngine
*/
protected $engine;
/**
* @param Scribunto_LuaEngine $engine
* @param string $code
* @param string|bool $chunkName
*/
public function __construct( Scribunto_LuaEngine $engine, $code, $chunkName ) {
parent::__construct( $engine, $code, $chunkName );
}
public function validate() {
try {
$this->getInitChunk();
} catch ( ScribuntoException $e ) {
return $e->toStatus();
}
return Status::newGood();
}
/**
* Get the chunk which, when called, will return the export table.
* @return mixed
*/
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.
*
* @param string $name
* @param PPFrame $frame
* @throws ScribuntoException
* @return string|null
*/
public function invoke( $name, $frame ) {
$ret = $this->engine->executeModule( $this->getInitChunk(), $name, $frame );
if ( !isset( $ret ) ) {
throw $this->engine->newException(
'scribunto-common-nosuchfunction', [ 'args' => [ $name ] ]
);
}
if ( !$this->engine->getInterpreter()->isLuaFunction( $ret ) ) {
throw $this->engine->newException(
'scribunto-common-notafunction', [ 'args' => [ $name ] ]
);
}
$result = $this->engine->executeFunctionChunk( $ret, $frame );
if ( isset( $result[0] ) ) {
return $result[0];
} else {
return null;
}
}
}
class Scribunto_LuaError extends ScribuntoException {
public $luaMessage, $lineMap = [];
public function __construct( $message, array $options = [] ) {
$this->luaMessage = $message;
$options = $options + [ 'args' => [ Validator::cleanUp( $message ) ] ];
if ( isset( $options['module'] ) && isset( $options['line'] ) ) {
$msg = 'scribunto-lua-error-location';
} else {
$msg = 'scribunto-lua-error';
}
parent::__construct( $msg, $options );
}
public function getLuaMessage() {
return $this->luaMessage;
}
public 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
*/
public function getScriptTraceHtml( $options = [] ) {
if ( !isset( $this->params['trace'] ) ) {
return false;
}
if ( isset( $options['msgOptions'] ) ) {
$msgOptions = $options['msgOptions'];
} else {
$msgOptions = [];
}
$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->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
$title = $title->createFragmentTarget( 'mw-ce-l' . $currentline );
$src = Html::rawElement( 'a',
[ 'href' => $title->getFullURL( 'action=edit' ) ],
$src );
}
}
if ( strval( $info['namewhat'] ) !== '' ) {
$function = wfMessage( 'scribunto-lua-in-function', wfEscapeWikiText( $info['name'] ) );
in_array( 'content', $msgOptions ) ?
$function = $function->inContentLanguage()->plain() :
$function = $function->plain();
} elseif ( $info['what'] == 'main' ) {
$function = wfMessage( 'scribunto-lua-in-main' );
in_array( 'content', $msgOptions ) ?
$function = $function->inContentLanguage()->plain() :
$function = $function->plain();
} else {
// C function, tail call, or a Lua function where Lua can't
// guess the name
$function = '?';
}
$backtraceLine = wfMessage( 'scribunto-lua-backtrace-line' )
->rawParams( "<strong>$src</strong>" )
->params( $function );
in_array( 'content', $msgOptions ) ?
$backtraceLine = $backtraceLine->inContentLanguage()->parse() :
$backtraceLine = $backtraceLine->parse();
$s .= "<li>$backtraceLine</li>";
}
$s .= '</ol>';
return $s;
}
}