diff options
author | Yaco <franco@reevo.org> | 2021-10-19 20:30:39 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2021-10-19 20:30:39 -0300 |
commit | 8c201ace3699b4928daf41eb7b4cdcb4565c6f3b (patch) | |
tree | fbd98f026864e9c1919d3ee740b6799ca0c651e2 /www/wiki/extensions/Scribunto/includes/engines/LuaCommon/LuaCommon.php | |
parent | e3880a1c86acaa3bbd05786ad2f5c586e6511a58 (diff) |
adds Scribunto
Diffstat (limited to 'www/wiki/extensions/Scribunto/includes/engines/LuaCommon/LuaCommon.php')
-rw-r--r-- | www/wiki/extensions/Scribunto/includes/engines/LuaCommon/LuaCommon.php | 1063 |
1 files changed, 1063 insertions, 0 deletions
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaCommon/LuaCommon.php b/www/wiki/extensions/Scribunto/includes/engines/LuaCommon/LuaCommon.php new file mode 100644 index 00000000..db38e49d --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaCommon/LuaCommon.php @@ -0,0 +1,1063 @@ +<?php + +use Wikimedia\ScopedCallback; + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +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 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. + * @param string $name + * @return array + */ + 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. + * @param string $name + * @return array + */ + 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() + * + * @param string $frameId + * @return array + */ + function frameExists( $frameId ) { + return [ $frameId === 'empty' || isset( $this->currentFrames[$frameId] ) ]; + } + + /** + * Handler for newChildFrame() + * + * @param string $frameId + * @param string $title + * @param array $args + * @return array + * @throws Scribunto_LuaError + */ + 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() + * + * @param string $frameId + * + * @return array + */ + function getFrameTitle( $frameId ) { + $frame = $this->getFrameById( $frameId ); + return [ $frame->getTitle()->getPrefixedText() ]; + } + + /** + * Handler for setTTL() + * @param int $ttl + */ + function setTTL( $ttl ) { + $args = func_get_args(); + $this->checkNumber( 'setTTL', $args, 0 ); + + $frame = $this->getFrameById( 'current' ); + $frame->setTTL( $ttl ); + } + + /** + * Handler for getExpandedArgument() + * @param string $frameId + * @param string $name + * @return array + */ + 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() + * @param string $frameId + * @return array + */ + function getAllExpandedArguments( $frameId ) { + $frame = $this->getFrameById( $frameId ); + $this->getInterpreter()->pauseUsageTimer(); + return [ $frame->getArguments() ]; + } + + /** + * Handler for expandTemplate() + * @param string $frameId + * @param string $titleText + * @param array $args + * @return array + * @throws Scribunto_LuaError + */ + 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() + * @param string $frameId + * @param string $function + * @param array $args + * @throws MWException + * @throws Scribunto_LuaError + * @return array + */ + 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() + * @param string $frameId + * @param string $text + * @return array + * @throws Scribunto_LuaError + */ + 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 + * + * @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 + * + * @param string $text wikitext + */ + public function addWarning( $text ) { + $this->getParser()->getOutput()->addWarning( $text ); + } + + /** + * Return whether the parser is currently substing + * + * @return array + */ + public function isSubsting() { + // See Parser::braceSubstitution, OT_WIKI is the switch + return [ $this->getParser()->OutputType() === Parser::OT_WIKI ]; + } + + 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; + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +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; + } + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaError extends ScribuntoException { + public $luaMessage, $lineMap = []; + + function __construct( $message, array $options = [] ) { + $this->luaMessage = $message; + $options = $options + [ 'args' => [ $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 = [] ) { + 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; + } +} |