3 use UtfNormal\Validator;
4 use Wikimedia\ScopedCallback;
12 'mw.site' =>
'Scribunto_LuaSiteLibrary',
13 'mw.uri' =>
'Scribunto_LuaUriLibrary',
14 'mw.ustring' =>
'Scribunto_LuaUstringLibrary',
15 'mw.language' =>
'Scribunto_LuaLanguageLibrary',
16 'mw.message' =>
'Scribunto_LuaMessageLibrary',
17 'mw.title' =>
'Scribunto_LuaTitleLibrary',
18 'mw.text' =>
'Scribunto_LuaTextLibrary',
19 'mw.html' =>
'Scribunto_LuaHtmlLibrary',
20 'mw.hash' =>
'Scribunto_LuaHashLibrary',
63 global $wgScribuntoEngineConf;
64 $engine =
'luastandalone';
67 $engine =
'luasandbox';
105 $this->interpreter =
null;
107 $this->expandCache =
null;
115 if ( $this->loaded ) {
118 $this->loaded =
true;
128 'getExpandedArgument',
129 'getAllExpandedArguments',
131 'callParserFunction',
133 'incrementExpensiveFunctionCount',
141 foreach ( $funcs as $name ) {
142 $lib[$name] = [ $this, $name ];
147 [
'allowEnvFuncs' => $this->options[
'allowEnvFuncs'] ] );
149 $this->availableLibraries = $this->
getLibraries(
'lua', self::$libraryClasses );
150 foreach ( $this->availableLibraries as $name => $def ) {
153 }
catch ( Exception $ex ) {
154 $this->loaded =
false;
155 $this->interpreter =
null;
176 $this->interpreter->registerLibrary(
'mw_interface', $interfaceFuncs );
179 if ( !empty( $package[
'setupInterface'] ) ) {
180 $this->interpreter->callFunction( $package[
'setupInterface'], $setupOptions );
190 return __DIR__ .
'/lualib';
202 if ( !preg_match(
'<^(?:[a-zA-Z]:)?' . preg_quote( DIRECTORY_SEPARATOR ) .
'>', $fileName ) ) {
203 $fileName =
"{$this->getLuaLibDir()}/{$fileName}";
237 $frame = $this->
getParser()->getPreprocessor()->newFrame();
242 $this->currentFrames = [
244 'parent' => $frame->parent ??
null,
246 $this->expandCache = [];
248 return new ScopedCallback(
function () use ( $oldFrames, $oldExpandCache ) {
249 $this->currentFrames = $oldFrames;
250 $this->expandCache = $oldExpandCache;
264 if ( !$this->currentFrames || !isset( $this->currentFrames[
'current'] ) ) {
271 $this->mw[
'executeModule'], $chunk, $functionName
278 'scribunto-lua-notarrayreturn', [
'args' => [ $retval[1] ] ]
295 $this->mw[
'executeFunction'],
304 if ( !$this->loaded ) {
308 $log = $this->
getInterpreter()->callFunction( $this->mw[
'getLogBuffer'] );
323 $keyMsg =
wfMessage(
'scribunto-limitreport-logs' );
325 $keyMsg->inLanguage(
'en' )->useDatabase(
false );
327 return Html::openElement(
'tr' ) .
328 Html::rawElement(
'th', [
'colspan' => 2 ], $keyMsg->parse() ) .
329 Html::closeElement(
'tr' ) .
330 Html::openElement(
'tr' ) .
331 Html::openElement(
'td', [
'colspan' => 2 ] ) .
332 Html::openElement(
'div', [
'class' =>
'mw-collapsible mw-collapsed' ] ) .
333 Html::element(
'pre', [
'class' =>
'scribunto-limitreport-logs' ], $logs ) .
334 Html::closeElement(
'div' ) .
335 Html::closeElement(
'td' ) .
336 Html::closeElement(
'tr' );
351 $mtime = filemtime( $fileName );
352 if ( $mtime ===
false ) {
353 throw new MWException(
'Lua file does not exist: ' . $fileName );
356 $cacheKey =
$cache->makeGlobalKey( __CLASS__, $fileName );
357 $fileData =
$cache->get( $cacheKey );
361 list( $code, $cachedMtime ) = $fileData;
362 if ( $cachedMtime < $mtime ) {
367 $code = file_get_contents( $fileName );
368 if ( $code ===
false ) {
369 throw new MWException(
'Lua file does not exist: ' . $fileName );
371 $cache->set( $cacheKey, [ $code, $mtime ], 60 * 5 );
374 # Prepending an "@" to the chunk name makes Lua think it is a filename
375 $module = $this->
getInterpreter()->loadString( $code,
'@' . basename( $fileName ) );
377 return $ret[0] ??
null;
400 $code =
"return function (__init, exe)\n" .
401 "if not exe then exe = function(...) return true, ... end end\n" .
402 "local p = select(2, exe(__init) )\n" .
403 "__init, exe = nil, nil\n" .
404 "local print = mw.log\n";
405 foreach ( $params[
'prevQuestions'] as $q ) {
406 if ( substr( $q, 0, 1 ) ===
'=' ) {
407 $code .=
"print(" . substr( $q, 1 ) .
")";
413 $code .=
"mw.clearLogBuffer()\n";
414 if ( substr( $params[
'question'], 0, 1 ) ===
'=' ) {
416 $code .=
"local ret = mw.allToString(" . substr( $params[
'question'], 1 ) .
")\n" .
417 "return ret, mw.getLogBuffer()\n";
419 $code .= $params[
'question'] .
"\n" .
420 "return nil, mw.getLogBuffer()\n";
424 if ( $params[
'title']->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
426 $params[
'content'], $params[
'title']->getPrefixedDBkey() );
427 $contentInit = $contentModule->getInitChunk();
428 $contentExe = $this->mw[
'executeModule'];
430 $contentInit = $params[
'content'];
436 wfMessage(
'scribunto-console-current-src' )->text()
438 $consoleInit = $consoleModule->getInitChunk();
439 $ret = $this->
getInterpreter()->callFunction( $this->mw[
'executeModule'], $consoleInit,
false );
441 $ret = $this->
getInterpreter()->callFunction( $func, $contentInit, $contentExe );
444 'return' => $ret[0] ??
null,
445 'print' => $ret[1] ??
'',
460 if ( !is_array(
$type ) ) {
463 if ( !isset(
$args[$index0] ) || !in_array( gettype(
$args[$index0] ),
$type,
true ) ) {
464 $index1 = $index0 + 1;
465 throw new Scribunto_LuaError(
"bad argument #$index1 to '$funcName' ($msgType expected)" );
477 $this->
checkType( $funcName,
$args, $index0,
'string',
'string' );
488 $this->
checkType( $funcName,
$args, $index0, [
'integer',
'double' ],
'number' );
500 $def = $this->availableLibraries[$name];
501 if ( is_string( $def ) ) {
502 $class =
new $def( $this );
504 if ( !$loadDeferred && !empty( $def[
'deferLoad'] ) ) {
507 if ( isset( $def[
'class'] ) ) {
508 $class =
new $def[
'class']( $this );
510 throw new MWException(
"No class for library \"$name\"" );
513 return $class->register();
526 $args = func_get_args();
530 if ( isset( $this->availableLibraries[$name] ) ) {
547 $args = func_get_args();
550 # This is what Lua does for its built-in loaders
551 $luaName = str_replace(
'.',
'/', $name ) .
'.lua';
553 foreach ( $paths as
$path ) {
555 if ( !file_exists( $fileName ) ) {
558 $code = file_get_contents( $fileName );
559 $init = $this->interpreter->loadString( $code,
"@$luaName" );
571 return [ $module->getInitChunk() ];
586 if ( $frameId ===
'empty' ) {
587 return $this->
getParser()->getPreprocessor()->newFrame();
588 } elseif ( isset( $this->currentFrames[$frameId] ) ) {
589 return $this->currentFrames[$frameId];
603 return [ $frameId ===
'empty' || isset( $this->currentFrames[$frameId] ) ];
617 if ( count( $this->currentFrames ) > 100 ) {
623 $title = $frame->getTitle();
630 $args = $this->
getParser()->getPreprocessor()->newPartNodeArray( $args );
632 $newFrameId =
'frame' . count( $this->currentFrames );
633 $this->currentFrames[$newFrameId] = $newFrame;
634 return [ $newFrameId ];
647 return [ $frame->getTitle()->getPrefixedText() ];
656 $args = func_get_args();
660 $frame->setTTL( $ttl );
671 $args = func_get_args();
676 $result = $frame->getArgument( $name );
677 if ( $result ===
false ) {
693 return [ $frame->getArguments() ];
712 if ( $frame->depth >= $this->parser->mOptions->getMaxTemplateDepth() ) {
719 list( $dom, $finalTitle ) = $this->parser->getTemplateDom(
$title );
720 if ( $dom ===
false ) {
721 throw new Scribunto_LuaError(
"expandTemplate: template \"$titleText\" does not exist" );
723 if ( !$frame->loopCheck( $finalTitle ) ) {
727 $fargs = $this->
getParser()->getPreprocessor()->newPartNodeArray(
$args );
728 $newFrame = $frame->newChild( $fargs, $finalTitle );
731 'frameId' => $frameId,
732 'template' => $finalTitle->getPrefixedDBkey(),
751 # Make zero-based, without screwing up named args
754 # Sort, since we can't rely on the order coming in from Lua
755 uksort(
$args,
function ( $a, $b ) {
756 if ( is_int( $a ) !== is_int( $b ) ) {
757 return is_int( $a ) ? -1 : 1;
759 if ( is_int( $a ) ) {
762 return strcmp( $a, $b );
766 $colonPos = strpos( $function,
':' );
767 if ( $colonPos !==
false ) {
768 array_unshift(
$args, trim( substr( $function, $colonPos + 1 ) ) );
769 $function = substr( $function, 0, $colonPos );
771 if ( !isset(
$args[0] ) ) {
772 # It's impossible to call a parser function from wikitext without
773 # supplying an arg 0. Insist that one be provided via Lua, too.
775 '(the parameter that comes after the colon in wikitext) ' .
780 $result = $this->parser->callParserFunction( $frame, $function,
$args );
781 if ( !$result[
'found'] ) {
782 throw new Scribunto_LuaError(
"callParserFunction: function \"$function\" was not found" );
785 # Set defaults for various flags
788 'isChildObj' =>
false,
789 'isLocalObj' =>
false,
794 $text = $result[
'text'];
795 if ( $result[
'isChildObj'] ) {
796 $fargs = $this->
getParser()->getPreprocessor()->newPartNodeArray(
$args );
797 $newFrame = $frame->newChild( $fargs, $result[
'title'] );
798 if ( $result[
'nowiki'] ) {
801 $text = $newFrame->expand( $text );
804 if ( $result[
'isLocalObj'] && $result[
'nowiki'] ) {
806 $result[
'isLocalObj'] =
false;
809 # Replace raw HTML by a placeholder
810 if ( $result[
'isHTML'] ) {
811 $text = $this->parser->insertStripItem( $text );
812 } elseif ( $result[
'nowiki'] ) {
813 # Escape nowiki-style return values
818 if ( $result[
'isLocalObj'] ) {
819 $text = $frame->expand( $text );
834 $args = func_get_args();
846 $frame->getArguments();
851 'frameId' => $frameId,
878 $this->
getParser()->getOutput()->addWarning( $text );
894 if ( isset( $this->expandCache[$hash] ) ) {
895 return $this->expandCache[$hash];
898 if ( is_scalar( $input ) ) {
899 $input = str_replace( [
"\r\n",
"\r" ],
"\n", $input );
900 $dom = $this->parser->getPreprocessor()->preprocessToObj(
901 $input, $frame->depth ? Parser::PTD_FOR_INCLUSION : 0 );
905 $ret = $frame->expand( $dom );
906 if ( !$frame->isVolatile() ) {
908 reset( $this->expandCache );
909 $oldHash = key( $this->expandCache );
910 unset( $this->expandCache[$oldHash] );
912 $this->expandCache[$hash] = $ret;
952 if ( !$this->initChunk ) {
953 $this->initChunk = $this->engine->getInterpreter()->loadString(
956 '=' . $this->chunkName );
969 public function invoke( $name, $frame ) {
970 $ret = $this->engine->executeModule( $this->
getInitChunk(), $name, $frame );
972 if ( !isset( $ret ) ) {
973 throw $this->engine->newException(
974 'scribunto-common-nosuchfunction', [
'args' => [ $name ] ]
977 if ( !$this->engine->getInterpreter()->isLuaFunction( $ret ) ) {
978 throw $this->engine->newException(
979 'scribunto-common-notafunction', [
'args' => [ $name ] ]
983 $result = $this->engine->executeFunctionChunk( $ret, $frame );
984 if ( isset( $result[0] ) ) {
996 $this->luaMessage = $message;
997 $options = $options + [
'args' => [ Validator::cleanUp( $message ) ] ];
998 if ( isset( $options[
'module'] ) && isset( $options[
'line'] ) ) {
999 $msg =
'scribunto-lua-error-location';
1001 $msg =
'scribunto-lua-error';
1004 parent::__construct( $msg, $options );
1012 $this->lineMap = $map;
1021 if ( !isset( $this->params[
'trace'] ) ) {
1024 if ( isset( $options[
'msgOptions'] ) ) {
1025 $msgOptions = $options[
'msgOptions'];
1030 $s =
'<ol class="scribunto-trace">';
1031 foreach ( $this->params[
'trace'] as $info ) {
1032 $short_src = $srcdefined = $info[
'short_src'];
1033 $currentline = $info[
'currentline'];
1035 $src = htmlspecialchars( $short_src );
1036 if ( $currentline > 0 ) {
1037 $src .=
':' . htmlspecialchars( $currentline );
1040 if (
$title &&
$title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
1041 $title =
$title->createFragmentTarget(
'mw-ce-l' . $currentline );
1042 $src = Html::rawElement(
'a',
1043 [
'href' =>
$title->getFullURL(
'action=edit' ) ],
1048 if ( strval( $info[
'namewhat'] ) !==
'' ) {
1050 in_array(
'content', $msgOptions ) ?
1051 $function = $functionMsg->inContentLanguage()->plain() :
1052 $function = $functionMsg->plain();
1053 } elseif ( $info[
'what'] ==
'main' ) {
1054 $functionMsg =
wfMessage(
'scribunto-lua-in-main' );
1055 in_array(
'content', $msgOptions ) ?
1056 $function = $functionMsg->inContentLanguage()->plain() :
1057 $function = $functionMsg->plain();
1064 $backtraceLineMsg =
wfMessage(
'scribunto-lua-backtrace-line' )
1065 ->rawParams(
"<strong>$src</strong>" )
1066 ->params( $function );
1067 in_array(
'content', $msgOptions ) ?
1068 $backtraceLine = $backtraceLineMsg->inContentLanguage()->parse() :
1069 $backtraceLine = $backtraceLineMsg->parse();
1071 $s .=
"<li>$backtraceLine</li>";