summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Scribunto/includes/engines/LuaStandalone')
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php781
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua710
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README39
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch22
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exebin0 -> 409600 bytes
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exebin0 -> 483328 bytes
-rwxr-xr-xwww/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/luabin0 -> 171697 bytes
-rwxr-xr-xwww/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/luabin0 -> 195508 bytes
-rwxr-xr-xwww/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/luabin0 -> 201644 bytes
-rwxr-xr-xwww/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh7
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua8
-rw-r--r--www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt176
12 files changed, 1743 insertions, 0 deletions
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php
new file mode 100644
index 00000000..330a7b9a
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php
@@ -0,0 +1,781 @@
+<?php
+
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps
+class Scribunto_LuaStandaloneEngine extends Scribunto_LuaEngine {
+ protected static $clockTick;
+ public $initialStatus;
+
+ /**
+ * @var Scribunto_LuaStandaloneInterpreter
+ */
+ protected $interpreter;
+
+ public function load() {
+ parent::load();
+ if ( php_uname( 's' ) === 'Linux' ) {
+ $this->initialStatus = $this->interpreter->getStatus();
+ } else {
+ $this->initialStatus = false;
+ }
+ }
+
+ public function getPerformanceCharacteristics() {
+ return [
+ 'phpCallsRequireSerialization' => true,
+ ];
+ }
+
+ function reportLimitData( ParserOutput $output ) {
+ try {
+ $this->load();
+ } catch ( Exception $e ) {
+ return;
+ }
+ if ( $this->initialStatus ) {
+ $status = $this->interpreter->getStatus();
+ $output->setLimitReportData( 'scribunto-limitreport-timeusage',
+ [
+ sprintf( "%.3f", $status['time'] / $this->getClockTick() ),
+ sprintf( "%.3f", $this->options['cpuLimit'] )
+ ]
+ );
+ $output->setLimitReportData( 'scribunto-limitreport-virtmemusage',
+ [
+ $status['vsize'],
+ $this->options['memoryLimit']
+ ]
+ );
+ $output->setLimitReportData( 'scribunto-limitreport-estmemusage',
+ $status['vsize'] - $this->initialStatus['vsize']
+ );
+ }
+ $logs = $this->getLogBuffer();
+ if ( $logs !== '' ) {
+ $output->addModules( 'ext.scribunto.logs' );
+ $output->setLimitReportData( 'scribunto-limitreport-logs', $logs );
+ }
+ }
+
+ function formatLimitData( $key, &$value, &$report, $isHTML, $localize ) {
+ global $wgLang;
+ $lang = $localize ? $wgLang : Language::factory( 'en' );
+ switch ( $key ) {
+ case 'scribunto-limitreport-logs':
+ if ( $isHTML ) {
+ $report .= $this->formatHtmlLogs( $value, $localize );
+ }
+ return false;
+ case 'scribunto-limitreport-virtmemusage':
+ $value = array_map( [ $lang, 'formatSize' ], $value );
+ break;
+ case 'scribunto-limitreport-estmemusage':
+ /** @suppress PhanTypeMismatchArgument */
+ $value = $lang->formatSize( $value );
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * @return mixed
+ */
+ function getClockTick() {
+ if ( self::$clockTick === null ) {
+ Wikimedia\suppressWarnings();
+ self::$clockTick = intval( shell_exec( 'getconf CLK_TCK' ) );
+ Wikimedia\restoreWarnings();
+ if ( !self::$clockTick ) {
+ self::$clockTick = 100;
+ }
+ }
+ return self::$clockTick;
+ }
+
+ /**
+ * @return Scribunto_LuaStandaloneInterpreter
+ */
+ function newInterpreter() {
+ return new Scribunto_LuaStandaloneInterpreter( $this, $this->options + [
+ 'logger' => LoggerFactory::getInstance( 'Scribunto' )
+ ] );
+ }
+
+ public function getSoftwareInfo( array &$software ) {
+ $ver = Scribunto_LuaStandaloneInterpreter::getLuaVersion( $this->options );
+ if ( $ver !== null ) {
+ if ( substr( $ver, 0, 6 ) === 'LuaJIT' ) {
+ $software['[http://luajit.org/ LuaJIT]'] = str_replace( 'LuaJIT ', '', $ver );
+ } else {
+ $software['[http://www.lua.org/ Lua]'] = str_replace( 'Lua ', '', $ver );
+ }
+ }
+ }
+}
+
+// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps
+class Scribunto_LuaStandaloneInterpreter extends Scribunto_LuaInterpreter {
+ protected static $nextInterpreterId = 0;
+
+ /**
+ * @var Scribunto_LuaStandaloneEngine
+ */
+ public $engine;
+
+ /**
+ * @var bool
+ */
+ public $enableDebug;
+
+ /**
+ * @var resource
+ */
+ public $proc;
+
+ /**
+ * @var resource
+ */
+ public $writePipe;
+
+ /**
+ * @var resource
+ */
+ public $readPipe;
+
+ /**
+ * @var ScribuntoException
+ */
+ public $exitError;
+
+ /**
+ * @var int
+ */
+ public $id;
+
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ * @var callable[]
+ */
+ protected $callbacks;
+
+ /**
+ * @param Scribunto_LuaStandaloneEngine $engine
+ * @param array $options
+ * @throws MWException
+ * @throws Scribunto_LuaInterpreterNotFoundError
+ * @throws ScribuntoException
+ */
+ function __construct( $engine, array $options ) {
+ $this->id = self::$nextInterpreterId++;
+
+ if ( $options['errorFile'] === null ) {
+ $options['errorFile'] = wfGetNull();
+ }
+
+ if ( $options['luaPath'] === null ) {
+ $path = false;
+
+ // Note, if you alter these, also alter getLuaVersion() below
+ if ( PHP_OS == 'Linux' ) {
+ if ( PHP_INT_SIZE == 4 ) {
+ $path = 'lua5_1_5_linux_32_generic/lua';
+ } elseif ( PHP_INT_SIZE == 8 ) {
+ $path = 'lua5_1_5_linux_64_generic/lua';
+ }
+ } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) {
+ if ( PHP_INT_SIZE == 4 ) {
+ $path = 'lua5_1_5_Win32_bin/lua5.1.exe';
+ } elseif ( PHP_INT_SIZE == 8 ) {
+ $path = 'lua5_1_5_Win64_bin/lua5.1.exe';
+ }
+ } elseif ( PHP_OS == 'Darwin' ) {
+ $path = 'lua5_1_5_mac_lion_fat_generic/lua';
+ }
+ if ( $path === false ) {
+ throw new Scribunto_LuaInterpreterNotFoundError(
+ 'No Lua interpreter was given in the configuration, ' .
+ 'and no bundled binary exists for this platform.' );
+ }
+ $options['luaPath'] = __DIR__ . "/binaries/$path";
+
+ if ( !is_executable( $options['luaPath'] ) ) {
+ throw new MWException(
+ sprintf( 'The lua binary (%s) is not executable.', $options['luaPath'] )
+ );
+ }
+ }
+
+ $this->engine = $engine;
+ $this->enableDebug = !empty( $options['debug'] );
+ $this->logger = isset( $options['logger'] )
+ ? $options['logger']
+ : new NullLogger();
+
+ $pipes = null;
+ $cmd = wfEscapeShellArg(
+ $options['luaPath'],
+ __DIR__ . '/mw_main.lua',
+ dirname( dirname( __DIR__ ) ),
+ $this->id,
+ PHP_INT_SIZE
+ );
+ if ( php_uname( 's' ) == 'Linux' ) {
+ // Limit memory and CPU
+ $cmd = wfEscapeShellArg(
+ 'exec', # proc_open() passes $cmd to 'sh -c' on Linux, so add an 'exec' to bypass it
+ '/bin/sh',
+ __DIR__ . '/lua_ulimit.sh',
+ $options['cpuLimit'], # soft limit (SIGXCPU)
+ $options['cpuLimit'] + 1, # hard limit
+ intval( $options['memoryLimit'] / 1024 ),
+ $cmd );
+ }
+
+ if ( php_uname( 's' ) == 'Windows NT' ) {
+ // Like the passthru() in older versions of PHP,
+ // PHP's invokation of cmd.exe in proc_open() is broken:
+ // http://news.php.net/php.internals/21796
+ // Unlike passthru(), it is not fixed in any PHP version,
+ // so we use the fix similar to one in wfShellExec()
+ $cmd = '"' . $cmd . '"';
+ }
+
+ $this->logger->debug( __METHOD__.": creating interpreter: $cmd\n" );
+
+ // Check whether proc_open is available before trying to call it (e.g.
+ // PHP's disable_functions may have removed it)
+ if ( !function_exists( 'proc_open' ) ) {
+ throw $this->engine->newException( 'scribunto-luastandalone-proc-error-proc-open' );
+ }
+
+ // Clear the "last error", so if proc_open fails we can know any
+ // warning was generated by that.
+ Wikimedia\suppressWarnings();
+ trigger_error( '' );
+ Wikimedia\restoreWarnings();
+
+ $this->proc = proc_open(
+ $cmd,
+ [
+ [ 'pipe', 'r' ],
+ [ 'pipe', 'w' ],
+ [ 'file', $options['errorFile'], 'a' ]
+ ],
+ $pipes );
+ if ( !$this->proc ) {
+ $err = error_get_last();
+ if ( !empty( $err['message'] ) ) {
+ throw $this->engine->newException( 'scribunto-luastandalone-proc-error-msg',
+ [ 'args' => [ $err['message'] ] ] );
+ } else {
+ throw $this->engine->newException( 'scribunto-luastandalone-proc-error' );
+ }
+ }
+ $this->writePipe = $pipes[0];
+ $this->readPipe = $pipes[1];
+ }
+
+ function __destruct() {
+ $this->terminate();
+ }
+
+ public static function getLuaVersion( array $options ) {
+ if ( $options['luaPath'] === null ) {
+ // We know which versions are distributed, no need to run them.
+ if ( PHP_OS == 'Linux' ) {
+ return 'Lua 5.1.5';
+ } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) {
+ return 'Lua 5.1.4';
+ } elseif ( PHP_OS == 'Darwin' ) {
+ return 'Lua 5.1.5';
+ } else {
+ return null;
+ }
+ }
+
+ // Ask the interpreter what version it is, using the "-v" option.
+ // The output is expected to be one line, something like these:
+ // Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
+ // LuaJIT 2.0.0 -- Copyright (C) 2005-2012 Mike Pall. http://luajit.org/
+ $cmd = wfEscapeShellArg( $options['luaPath'] ) . ' -v';
+ $handle = popen( $cmd, 'r' );
+ if ( $handle ) {
+ $ret = fgets( $handle, 80 );
+ pclose( $handle );
+ if ( $ret && preg_match( '/^Lua(?:JIT)? \S+/', $ret, $m ) ) {
+ return $m[0];
+ }
+ }
+ return null;
+ }
+
+ public function terminate() {
+ if ( $this->proc ) {
+ $this->logger->debug( __METHOD__.": terminating\n" );
+ proc_terminate( $this->proc );
+ proc_close( $this->proc );
+ $this->proc = false;
+ }
+ }
+
+ public function quit() {
+ if ( !$this->proc ) {
+ return;
+ }
+ $this->dispatch( [ 'op' => 'quit' ] );
+ proc_close( $this->proc );
+ }
+
+ public function testquit() {
+ if ( !$this->proc ) {
+ return;
+ }
+ $this->dispatch( [ 'op' => 'testquit' ] );
+ proc_close( $this->proc );
+ }
+
+ /**
+ * @param string $text
+ * @param string $chunkName
+ * @return Scribunto_LuaStandaloneInterpreterFunction
+ */
+ public function loadString( $text, $chunkName ) {
+ $this->cleanupLuaChunks();
+
+ $result = $this->dispatch( [
+ 'op' => 'loadString',
+ 'text' => $text,
+ 'chunkName' => $chunkName
+ ] );
+ return new Scribunto_LuaStandaloneInterpreterFunction( $this->id, $result[1] );
+ }
+
+ public function callFunction( $func /* ... */ ) {
+ if ( !( $func instanceof Scribunto_LuaStandaloneInterpreterFunction ) ) {
+ throw new MWException( __METHOD__.': invalid function type' );
+ }
+ if ( $func->interpreterId !== $this->id ) {
+ throw new MWException( __METHOD__.': function belongs to a different interpreter' );
+ }
+ $args = func_get_args();
+ unset( $args[0] );
+ // $args is now conveniently a 1-based array, as required by the Lua server
+
+ $this->cleanupLuaChunks();
+
+ $result = $this->dispatch( [
+ 'op' => 'call',
+ 'id' => $func->id,
+ 'nargs' => count( $args ),
+ 'args' => $args,
+ ] );
+ // Convert return values to zero-based
+ return array_values( $result );
+ }
+
+ public function wrapPhpFunction( $callable ) {
+ static $uid = 0;
+ $id = "anonymous*" . ++$uid;
+ $this->callbacks[$id] = $callable;
+ $ret = $this->dispatch( [
+ 'op' => 'wrapPhpFunction',
+ 'id' => $id,
+ ] );
+ return $ret[1];
+ }
+
+ public function cleanupLuaChunks() {
+ if ( isset( Scribunto_LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ) ) {
+ unset( Scribunto_LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] );
+ $this->dispatch( [
+ 'op' => 'cleanupChunks',
+ 'ids' => Scribunto_LuaStandaloneInterpreterFunction::$activeChunkIds[$this->id]
+ ] );
+ }
+ }
+
+ public function isLuaFunction( $object ) {
+ return $object instanceof Scribunto_LuaStandaloneInterpreterFunction;
+ }
+
+ public function registerLibrary( $name, array $functions ) {
+ $functionIds = [];
+ foreach ( $functions as $funcName => $callback ) {
+ $id = "$name-$funcName";
+ $this->callbacks[$id] = $callback;
+ $functionIds[$funcName] = $id;
+ }
+ $this->dispatch( [
+ 'op' => 'registerLibrary',
+ 'name' => $name,
+ 'functions' => $functionIds,
+ ] );
+ }
+
+ public function getStatus() {
+ $result = $this->dispatch( [
+ 'op' => 'getStatus',
+ ] );
+ return $result[1];
+ }
+
+ public function pauseUsageTimer() {
+ }
+
+ public function unpauseUsageTimer() {
+ }
+
+ /**
+ * Fill in missing nulls in a list received from Lua
+ *
+ * @param array $array List received from Lua
+ * @param int $count Number of values that should be in the list
+ * @return array Non-sparse array
+ */
+ private static function fixNulls( array $array, $count ) {
+ if ( count( $array ) === $count ) {
+ return $array;
+ } else {
+ return array_replace( array_fill( 1, $count, null ), $array );
+ }
+ }
+
+ protected function handleCall( $message ) {
+ $message['args'] = self::fixNulls( $message['args'], $message['nargs'] );
+ try {
+ $result = $this->callback( $message['id'], $message['args'] );
+ } catch ( Scribunto_LuaError $e ) {
+ return [
+ 'op' => 'error',
+ 'value' => $e->getLuaMessage(),
+ ];
+ }
+
+ // Convert to a 1-based array
+ if ( $result !== null && count( $result ) ) {
+ $result = array_combine( range( 1, count( $result ) ), $result );
+ } else {
+ $result = [];
+ }
+
+ return [
+ 'op' => 'return',
+ 'nvalues' => count( $result ),
+ 'values' => $result
+ ];
+ }
+
+ protected function callback( $id, array $args ) {
+ return call_user_func_array( $this->callbacks[$id], $args );
+ }
+
+ protected function handleError( $message ) {
+ $opts = [];
+ if ( preg_match( '/^(.*?):(\d+): (.*)$/', $message['value'], $m ) ) {
+ $opts['module'] = $m[1];
+ $opts['line'] = $m[2];
+ $message['value'] = $m[3];
+ }
+ if ( isset( $message['trace'] ) ) {
+ $opts['trace'] = array_values( $message['trace'] );
+ }
+ throw $this->engine->newLuaError( $message['value'], $opts );
+ }
+
+ protected function dispatch( $msgToLua ) {
+ $this->sendMessage( $msgToLua );
+ while ( true ) {
+ $msgFromLua = $this->receiveMessage();
+
+ switch ( $msgFromLua['op'] ) {
+ case 'return':
+ return self::fixNulls( $msgFromLua['values'], $msgFromLua['nvalues'] );
+ case 'call':
+ $msgToLua = $this->handleCall( $msgFromLua );
+ $this->sendMessage( $msgToLua );
+ break;
+ case 'error':
+ $this->handleError( $msgFromLua );
+ return; // not reached
+ default:
+ $this->logger->error( __METHOD__ .": invalid response op \"{$msgFromLua['op']}\"\n" );
+ throw $this->engine->newException( 'scribunto-luastandalone-decode-error' );
+ }
+ }
+ }
+
+ protected function sendMessage( $msg ) {
+ $this->debug( "TX ==> {$msg['op']}" );
+ $this->checkValid();
+ // Send the message
+ $encMsg = $this->encodeMessage( $msg );
+ if ( !fwrite( $this->writePipe, $encMsg ) ) {
+ // Write error, probably the process has terminated
+ // If it has, handleIOError() will throw. If not, throw an exception ourselves.
+ $this->handleIOError();
+ throw $this->engine->newException( 'scribunto-luastandalone-write-error' );
+ }
+ }
+
+ protected function receiveMessage() {
+ $this->checkValid();
+ // Read the header
+ $header = fread( $this->readPipe, 16 );
+ if ( strlen( $header ) !== 16 ) {
+ $this->handleIOError();
+ throw $this->engine->newException( 'scribunto-luastandalone-read-error' );
+ }
+ $length = $this->decodeHeader( $header );
+
+ // Read the reply body
+ $body = '';
+ $lengthRemaining = $length;
+ while ( $lengthRemaining ) {
+ $buffer = fread( $this->readPipe, $lengthRemaining );
+ if ( $buffer === false || feof( $this->readPipe ) ) {
+ $this->handleIOError();
+ throw $this->engine->newException( 'scribunto-luastandalone-read-error' );
+ }
+ $body .= $buffer;
+ $lengthRemaining -= strlen( $buffer );
+ }
+ $body = strtr( $body, [
+ '\\r' => "\r",
+ '\\n' => "\n",
+ '\\\\' => '\\',
+ ] );
+ $msg = unserialize( $body );
+ $this->debug( "RX <== {$msg['op']}" );
+ return $msg;
+ }
+
+ protected function encodeMessage( $message ) {
+ $serialized = $this->encodeLuaVar( $message );
+ $length = strlen( $serialized );
+ $check = $length * 2 - 1;
+
+ return sprintf( '%08x%08x%s', $length, $check, $serialized );
+ }
+
+ /**
+ * @param mixed $var
+ * @param int $level
+ *
+ * @return string
+ * @throws MWException
+ */
+ protected function encodeLuaVar( $var, $level = 0 ) {
+ if ( $level > 100 ) {
+ throw new MWException( __METHOD__.': recursion depth limit exceeded' );
+ }
+ $type = gettype( $var );
+ switch ( $type ) {
+ case 'boolean':
+ return $var ? 'true' : 'false';
+ case 'integer':
+ return $var;
+ case 'double':
+ if ( !is_finite( $var ) ) {
+ if ( is_nan( $var ) ) {
+ return '(0/0)';
+ }
+ if ( $var === INF ) {
+ return '(1/0)';
+ }
+ if ( $var === -INF ) {
+ return '(-1/0)';
+ }
+ throw new MWException( __METHOD__.': cannot convert non-finite number' );
+ }
+ return sprintf( '%.17g', $var );
+ case 'string':
+ return '"' .
+ strtr( $var, [
+ '"' => '\\"',
+ '\\' => '\\\\',
+ "\n" => '\\n',
+ "\r" => '\\r',
+ "\000" => '\\000',
+ ] ) .
+ '"';
+ case 'array':
+ $s = '{';
+ foreach ( $var as $key => $element ) {
+ if ( $s !== '{' ) {
+ $s .= ',';
+ }
+
+ // Lua's number type can't represent most integers beyond 2**53, so stringify such keys
+ if ( is_int( $key ) && ( $key > 9007199254740992 || $key < -9007199254740992 ) ) {
+ $key = sprintf( '%d', $key );
+ }
+
+ $s .= '[' . $this->encodeLuaVar( $key, $level + 1 ) . ']' .
+ '=' . $this->encodeLuaVar( $element, $level + 1 );
+ }
+ $s .= '}';
+ return $s;
+ case 'object':
+ if ( !( $var instanceof Scribunto_LuaStandaloneInterpreterFunction ) ) {
+ throw new MWException( __METHOD__.': unable to convert object of type ' .
+ get_class( $var ) );
+ } elseif ( $var->interpreterId !== $this->id ) {
+ throw new MWException(
+ __METHOD__.': unable to convert function belonging to a different interpreter'
+ );
+ } else {
+ return 'chunks[' . intval( $var->id ) . ']';
+ }
+ case 'resource':
+ throw new MWException( __METHOD__.': unable to convert resource' );
+ case 'NULL':
+ return 'nil';
+ default:
+ throw new MWException( __METHOD__.': unable to convert variable of unknown type' );
+ }
+ }
+
+ protected function decodeHeader( $header ) {
+ $length = substr( $header, 0, 8 );
+ $check = substr( $header, 8, 8 );
+ if ( !preg_match( '/^[0-9a-f]+$/', $length ) || !preg_match( '/^[0-9a-f]+$/', $check ) ) {
+ throw $this->engine->newException( 'scribunto-luastandalone-decode-error' );
+ }
+ $length = hexdec( $length );
+ $check = hexdec( $check );
+ if ( $length * 2 - 1 !== $check ) {
+ throw $this->engine->newException( 'scribunto-luastandalone-decode-error' );
+ }
+ return $length;
+ }
+
+ /**
+ * @throws ScribuntoException
+ */
+ protected function checkValid() {
+ if ( !$this->proc ) {
+ $this->logger->error( __METHOD__ . ": process already terminated\n" );
+ if ( $this->exitError ) {
+ throw $this->exitError;
+ } else {
+ throw $this->engine->newException( 'scribunto-luastandalone-gone' );
+ }
+ }
+ }
+
+ /**
+ * @throws ScribuntoException
+ */
+ protected function handleIOError() {
+ $this->checkValid();
+
+ // Terminate, fetch the status, then close. proc_close()'s return
+ // value isn't helpful here because there's no way to differentiate a
+ // signal-kill from a normal exit.
+ proc_terminate( $this->proc );
+ while ( true ) {
+ $status = proc_get_status( $this->proc );
+ if ( $status === false ) {
+ // WTF? Let the caller throw an appropriate error.
+ return;
+ }
+ if ( !$status['running'] ) {
+ break;
+ }
+ usleep( 10000 ); // Give the killed process a chance to be scheduled
+ }
+ proc_close( $this->proc );
+ $this->proc = false;
+
+ // proc_open() sometimes uses a shell, check for shell-style signal reporting.
+ if ( !$status['signaled'] && ( $status['exitcode'] & 0x80 ) === 0x80 ) {
+ $status['signaled'] = true;
+ $status['termsig'] = $status['exitcode'] - 128;
+ }
+
+ if ( $status['signaled'] ) {
+ if ( defined( 'SIGXCPU' ) && $status['termsig'] === SIGXCPU ) {
+ $this->exitError = $this->engine->newException( 'scribunto-common-timeout' );
+ } else {
+ $this->exitError = $this->engine->newException( 'scribunto-luastandalone-signal',
+ [ 'args' => [ $status['termsig'] ] ] );
+ }
+ } else {
+ $this->exitError = $this->engine->newException( 'scribunto-luastandalone-exited',
+ [ 'args' => [ $status['exitcode'] ] ] );
+ }
+ throw $this->exitError;
+ }
+
+ protected function debug( $msg ) {
+ if ( $this->enableDebug ) {
+ $this->logger->debug( "Lua: $msg\n" );
+ }
+ }
+}
+
+// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps
+class Scribunto_LuaStandaloneInterpreterFunction {
+ public static $anyChunksDestroyed = [];
+ public static $activeChunkIds = [];
+
+ /**
+ * @var int
+ */
+ public $interpreterId;
+
+ /**
+ * @var int
+ */
+ public $id;
+
+ /**
+ * @param int $interpreterId
+ * @param int $id
+ */
+ function __construct( $interpreterId, $id ) {
+ $this->interpreterId = $interpreterId;
+ $this->id = $id;
+ $this->incrementRefCount();
+ }
+
+ function __clone() {
+ $this->incrementRefCount();
+ }
+
+ function __wakeup() {
+ $this->incrementRefCount();
+ }
+
+ function __destruct() {
+ $this->decrementRefCount();
+ }
+
+ private function incrementRefCount() {
+ if ( !isset( self::$activeChunkIds[$this->interpreterId] ) ) {
+ self::$activeChunkIds[$this->interpreterId] = [ $this->id => 1 ];
+ } elseif ( !isset( self::$activeChunkIds[$this->interpreterId][$this->id] ) ) {
+ self::$activeChunkIds[$this->interpreterId][$this->id] = 1;
+ } else {
+ self::$activeChunkIds[$this->interpreterId][$this->id]++;
+ }
+ }
+
+ private function decrementRefCount() {
+ if ( isset( self::$activeChunkIds[$this->interpreterId][$this->id] ) ) {
+ if ( --self::$activeChunkIds[$this->interpreterId][$this->id] <= 0 ) {
+ unset( self::$activeChunkIds[$this->interpreterId][$this->id] );
+ self::$anyChunksDestroyed[$this->interpreterId] = true;
+ }
+ } else {
+ self::$anyChunksDestroyed[$this->interpreterId] = true;
+ }
+ }
+}
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua
new file mode 100644
index 00000000..a8227bed
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua
@@ -0,0 +1,710 @@
+MWServer = {}
+
+--- Create a new MWServer object
+function MWServer:new( interpreterId, intSize )
+ interpreterId = tonumber( interpreterId )
+ if not interpreterId then
+ error( "bad argument #1 to 'MWServer:new' (must be a number or convertible to a number)", 2 )
+ end
+ intSize = tonumber( intSize )
+ if intSize ~= 4 and intSize ~= 8 then
+ error( "bad argument #2 to 'MWServer:new' (must be 4 or 8)", 2 )
+ end
+
+ obj = {
+ interpreterId = interpreterId,
+ nextChunkId = 1,
+ chunks = {},
+ xchunks = {},
+ protectedFunctions = {},
+ protectedEnvironments = {},
+ baseEnv = {}
+ }
+ if intSize == 4 then
+ obj.intMax = 2147483648
+ obj.intKeyMax = 2147483648
+ else
+ -- Lua can't represent most larger integers, so they may as well be sent to PHP as floats.
+ obj.intMax = 9007199254740992
+ obj.intKeyMax = 9223372036854775807
+ end
+ setmetatable( obj, self )
+ self.__index = self
+
+ obj:init()
+
+ return obj
+end
+
+--- Initialise a new MWServer object
+function MWServer:init()
+ self.baseEnv = self:newEnvironment()
+ for funcName, func in pairs( self ) do
+ if type(func) == 'function' then
+ self.protectedFunctions[func] = true
+ end
+ end
+ self.protectedEnvironments[_G] = true
+end
+
+--- Serve requests until exit is requested
+function MWServer:execute()
+ self:dispatch( nil )
+ self:debug( 'MWServer:execute: returning' )
+end
+
+-- Convert a multiple-return-value or a ... into a count and a table
+function MWServer:listToCountAndTable( ... )
+ return select( '#', ... ), { ... }
+end
+
+--- Call a PHP function
+-- Raise an error if the PHP handler requests it. May return any number
+-- of values.
+--
+-- @param id The function ID, specified by a registerLibrary message
+-- @param nargs Count of function arguments
+-- @param args The function arguments
+-- @return The return values from the PHP function
+function MWServer:call( id, nargs, args )
+ local result = self:dispatch( {
+ op = 'call',
+ id = id,
+ nargs = nargs,
+ args = args
+ } )
+ if result.op == 'return' then
+ return unpack( result.values, 1, result.nvalues )
+ elseif result.op == 'error' then
+ -- Raise an error in the actual user code that called the function
+ -- The level is 3 since our immediate caller is a closure
+ error( result.value, 3 )
+ else
+ self:internalError( 'MWServer:call: unexpected result op' )
+ end
+end
+
+--- Handle a "call" message from PHP. Call the relevant function.
+--
+-- @param message The message from PHP
+-- @return A response message to send back to PHP
+function MWServer:handleCall( message )
+ if not self.chunks[message.id] then
+ return {
+ op = 'error',
+ value = 'function id ' .. message.id .. ' does not exist'
+ }
+ end
+
+ local n, result = self:listToCountAndTable( xpcall(
+ function ()
+ return self.chunks[message.id]( unpack( message.args, 1, message.nargs ) )
+ end,
+ function ( err )
+ return MWServer:attachTrace( err )
+ end
+ ) )
+
+ if result[1] then
+ -- table.remove( result, 1 ) renumbers from 2 to #result. But #result
+ -- is not necessarily "right" if result contains nils.
+ result = { unpack( result, 2, n ) }
+ return {
+ op = 'return',
+ nvalues = n - 1,
+ values = result
+ }
+ else
+ if result[2].value and result[2].trace then
+ return {
+ op = 'error',
+ value = result[2].value,
+ trace = result[2].trace,
+ }
+ else
+ return {
+ op = 'error',
+ value = result[2]
+ }
+ end
+ end
+end
+
+--- The xpcall() error handler for handleCall(). Modifies the error object
+-- to include a structured backtrace
+--
+-- @param err The error object
+-- @return The new error object
+function MWServer:attachTrace( err )
+ return {
+ value = err,
+ trace = self:getStructuredTrace( 2 )
+ }
+end
+
+--- Handle a "loadString" message from PHP.
+-- Load the function and return a chunk ID.
+--
+-- @param message The message from PHP
+-- @return A response message to send back to PHP
+function MWServer:handleLoadString( message )
+ if string.find( message.text, '\27Lua', 1, true ) then
+ return {
+ op = 'error',
+ value = 'cannot load code with a Lua binary chunk marker escape sequence in it'
+ }
+ end
+ local chunk, errorMsg = loadstring( message.text, message.chunkName )
+ if chunk then
+ setfenv( chunk, self.baseEnv )
+ local id = self:addChunk( chunk )
+ return {
+ op = 'return',
+ nvalues = 1,
+ values = {id}
+ }
+ else
+ return {
+ op = 'error',
+ value = errorMsg
+ }
+ end
+end
+
+--- Add a function value to the list of tracked chunks and return its associated ID.
+-- Adding a chunk allows it to be referred to in messages from PHP.
+--
+-- @param chunk The function value
+-- @return The chunk ID
+function MWServer:addChunk( chunk )
+ local id = self.nextChunkId
+ self.nextChunkId = id + 1
+ self.chunks[id] = chunk
+ self.xchunks[chunk] = id
+ return id
+end
+
+--- Handle a "cleanupChunks" message from PHP.
+-- Remove any chunks no longer referenced by PHP code.
+--
+-- @param message The message from PHP
+-- @return A response message to send back to PHP
+function MWServer:handleCleanupChunks( message )
+ for id, chunk in pairs( self.chunks ) do
+ if not message.ids[id] then
+ self.chunks[id] = nil
+ self.xchunks[chunk] = nil
+ end
+ end
+
+ return {
+ op = 'return',
+ nvalues = 0,
+ values = {}
+ }
+end
+
+--- Handle a "registerLibrary" message from PHP.
+-- Add the relevant functions to the base environment.
+--
+-- @param message The message from PHP
+-- @return The response message
+function MWServer:handleRegisterLibrary( message )
+ local startPos = 1
+ local component
+ if not self.baseEnv[message.name] then
+ self.baseEnv[message.name] = {}
+ end
+ local t = self.baseEnv[message.name]
+
+ for name, id in pairs( message.functions ) do
+ t[name] = function( ... )
+ return self:call( id, self:listToCountAndTable( ... ) )
+ end
+ -- Protect the function against setfenv()
+ self.protectedFunctions[t[name]] = true
+ end
+
+ return {
+ op = 'return',
+ nvalues = 0,
+ values = {}
+ }
+end
+
+--- Handle a "wrapPhpFunction" message from PHP.
+-- Create an anonymous function
+--
+-- @param message The message from PHP
+-- @return The response message
+function MWServer:handleWrapPhpFunction( message )
+ local id = message.id
+ local func = function( ... )
+ return self:call( id, self:listToCountAndTable( ... ) )
+ end
+ -- Protect the function against setfenv()
+ self.protectedFunctions[func] = true
+
+ return {
+ op = 'return',
+ nvalues = 1,
+ values = { func }
+ }
+end
+
+--- Handle a "getStatus" message from PHP
+--
+-- @param message The request message
+-- @return The response message
+function MWServer:handleGetStatus( message )
+ local nullRet = {
+ op = 'return',
+ nvalues = 0,
+ values = {}
+ }
+ local file = io.open( '/proc/self/stat' )
+ if not file then
+ return nullRet
+ end
+ local s = file:read('*a')
+ file:close()
+ local t = {}
+ for token in string.gmatch(s, '[^ ]+') do
+ t[#t + 1] = token
+ end
+ if #t < 22 then
+ return nullRet
+ end
+ return {
+ op = 'return',
+ nvalues = 1,
+ values = {{
+ pid = tonumber(t[1]),
+ time = tonumber(t[14]) + tonumber(t[15]) + tonumber(t[16]) + tonumber(t[17]),
+ vsize = tonumber(t[23]),
+ }}
+ }
+end
+
+--- The main request/response loop
+--
+-- Send a request message and return its matching reply message. Handle any
+-- intervening requests (i.e. re-entrant calls) by dispatching them to the
+-- relevant handler function.
+--
+-- The request message may optionally be omitted, to listen for request messages
+-- without first sending a request of its own. Such a dispatch() call will
+-- continue running until termination is requested by PHP. Typically, PHP does
+-- this with a SIGTERM signal.
+--
+-- @param msgToPhp The message to send to PHP. Optional.
+-- @return The matching response message
+function MWServer:dispatch( msgToPhp )
+ if msgToPhp then
+ self:sendMessage( msgToPhp, 'call' )
+ end
+ while true do
+ local msgFromPhp = self:receiveMessage()
+ local msgToPhp
+ local op = msgFromPhp.op
+ if op == 'return' or op == 'error' then
+ return msgFromPhp
+ elseif op == 'call' then
+ msgToPhp = self:handleCall( msgFromPhp )
+ self:sendMessage( msgToPhp, 'reply' )
+ elseif op == 'loadString' then
+ msgToPhp = self:handleLoadString( msgFromPhp )
+ self:sendMessage( msgToPhp, 'reply' )
+ elseif op == 'registerLibrary' then
+ msgToPhp = self:handleRegisterLibrary( msgFromPhp )
+ self:sendMessage( msgToPhp, 'reply' )
+ elseif op == 'wrapPhpFunction' then
+ msgToPhp = self:handleWrapPhpFunction( msgFromPhp )
+ self:sendMessage( msgToPhp, 'reply' )
+ elseif op == 'cleanupChunks' then
+ msgToPhp = self:handleCleanupChunks( msgFromPhp )
+ self:sendMessage( msgToPhp, 'reply' )
+ elseif op == 'getStatus' then
+ msgToPhp = self:handleGetStatus( msgFromPhp )
+ self:sendMessage( msgToPhp, 'reply' )
+ elseif op == 'quit' then
+ self:debug( 'MWServer:dispatch: quit message received' )
+ os.exit(0)
+ elseif op == 'testquit' then
+ self:debug( 'MWServer:dispatch: testquit message received' )
+ os.exit(42)
+ else
+ self:internalError( "Invalid message operation" )
+ end
+ end
+end
+
+--- Write a message to the debug output stream.
+-- Some day this may be configurable, currently it just unconditionally writes
+-- the message to stderr. The PHP host will redirect those errors to /dev/null
+-- by default, but it can be configured to send them to a file.
+--
+-- @param s The message
+function MWServer:debug( s )
+ if ( type(s) == 'string' ) then
+ io.stderr:write( s .. '\n' )
+ else
+ io.stderr:write( self:serialize( s ) .. '\n' )
+ end
+end
+
+--- Raise an internal error
+-- Write a message to stderr and then exit with a failure status. This should
+-- be called for errors which cannot be allowed to be caught with pcall().
+--
+-- This must be used for protocol errors, or indeed any error from a context
+-- where a dispatch() call lies between the error source and a possible pcall()
+-- handler. If dispatch() were terminated by a regular error() call, the
+-- resulting protocol violation could lead to a deadlock.
+--
+-- @param msg The error message
+function MWServer:internalError( msg )
+ io.stderr:write( debug.traceback( msg ) .. '\n' )
+ os.exit( 1 )
+end
+
+--- Raise an I/O error
+-- Helper function for errors from the io and file modules, which may optionally
+-- return an informative error message as their second return value.
+function MWServer:ioError( header, info )
+ if type( info) == 'string' then
+ self:internalError( header .. ': ' .. info )
+ else
+ self:internalError( header )
+ end
+end
+
+--- Send a message to PHP
+-- @param msg The message table
+-- @param direction 'call' or 'reply'
+function MWServer:sendMessage( msg, direction )
+ if not msg.op then
+ self:internalError( "MWServer:sendMessage: invalid message", 2 )
+ end
+ self:debug('TX ==> ' .. msg.op)
+
+ -- If we're making an outgoing call, let errors go to our caller. If we're
+ -- replying to a call from PHP, catch serialization errors and return them
+ -- to PHP.
+ local encMsg;
+ if direction == 'reply' then
+ local ok
+ ok, encMsg = pcall( self.encodeMessage, self, msg )
+ if not ok then
+ self:debug('Serialization failed: ' .. encMsg)
+ self:debug('TX ==> error')
+ encMsg = self:encodeMessage( { op = 'error', value = encMsg } )
+ end
+ else
+ encMsg = self:encodeMessage( msg )
+ end
+
+ local success, errorMsg = io.stdout:write( encMsg )
+ if not success then
+ self:ioError( 'Write error', errorMsg )
+ end
+ io.stdout:flush()
+end
+
+--- Wait for a message from PHP and then decode and return it as a table
+-- @return The received message
+function MWServer:receiveMessage()
+ -- Read the header
+ local header, errorMsg = io.stdin:read( 16 )
+ if header == nil and errorMsg == nil then
+ -- End of file on stdin, exit gracefully
+ os.exit(0)
+ end
+
+ if not header or #header ~= 16 then
+ self:ioError( 'Read error', errorMsg )
+ end
+ local length = self:decodeHeader( header )
+
+ -- Read the body
+ local body, errorMsg = io.stdin:read( length )
+ if not body then
+ self:ioError( 'Read error', errorMsg )
+ end
+ if #body ~= length then
+ self:ioError( 'Read error', errorMsg )
+ end
+
+ -- Unserialize it
+ msg = self:unserialize( body )
+ self:debug('RX <== ' .. msg.op)
+ if msg.op == 'error' then
+ self:debug( 'Error: ' .. tostring( msg.value ) )
+ end
+ return msg
+end
+
+--- Encode a message for sending to PHP
+function MWServer:encodeMessage( message )
+ local serialized = self:serialize( message )
+ local length = #serialized
+ local check = length * 2 - 1
+ return string.format( '%08x%08x', length, check ) .. serialized
+end
+
+-- Faster to create the table once than for each call to MWServer:serialize()
+local serialize_replacements = {
+ ['\r'] = '\\r',
+ ['\n'] = '\\n',
+ ['\\'] = '\\\\',
+}
+
+--- Convert a value to a string suitable for passing to PHP's unserialize().
+-- Note that the following replacements must be performed before calling
+-- unserialize:
+-- "\\r" => "\r"
+-- "\\n" => "\n"
+-- "\\\\" => "\\"
+--
+-- @param var The value.
+function MWServer:serialize( var )
+ local done = {}
+
+ local function isInteger( var, max )
+ return type(var) == 'number'
+ and math.floor( var ) == var
+ and var >= -max
+ and var < max
+ end
+
+ local function recursiveEncode( var, level )
+ local t = type( var )
+ if t == 'nil' then
+ return 'N;'
+ elseif t == 'number' then
+ if isInteger( var, self.intMax ) then
+ return 'i:' .. string.format( '%d', var ) .. ';'
+ elseif var < math.huge and var > -math.huge then
+ return 'd:' .. string.format( '%.17g', var ) .. ';'
+ elseif var == math.huge then
+ return 'd:INF;'
+ elseif var == -math.huge then
+ return 'd:-INF;'
+ else
+ return 'd:NAN;'
+ end
+ elseif t == 'string' then
+ return 's:' .. string.len( var ) .. ':"' .. var .. '";'
+ elseif t == 'boolean' then
+ if var then
+ return 'b:1;'
+ else
+ return 'b:0;'
+ end
+ elseif t == 'table' then
+ if done[var] then
+ error("Cannot pass circular reference to PHP")
+ end
+ done[var] = true
+ local buf = { '' }
+ local numElements = 0
+ local seen = {}
+ for key, value in pairs(var) do
+ local k = key
+ local t = type( k )
+
+ -- Convert integers in range to look like standard integers.
+ -- Use tostring() for the rest. Reject all other non-strings.
+ if isInteger( k, self.intKeyMax ) then
+ k = string.format( '%d', k )
+ elseif t == 'number' then
+ k = tostring( k );
+ elseif t ~= 'string' then
+ error("Cannot use " .. t .. " as an array key when passing data from Lua to PHP");
+ end
+
+ -- Zend PHP doesn't really care whether integer keys are serialized
+ -- as ints or strings, it converts them correctly on unserialize.
+ -- But HHVM does depend on it, so keep doing it for now.
+ local n = nil
+ if k == '0' or k:match( '^-?[1-9]%d*$' ) then
+ n = tonumber( k )
+ if n == -9223372036854775808 and k ~= '-9223372036854775808' then
+ -- Bad edge rounding
+ n = nil
+ end
+ end
+ if isInteger( n, self.intKeyMax ) then
+ buf[#buf + 1] = 'i:' .. k .. ';'
+ else
+ buf[#buf + 1] = recursiveEncode( k, level + 1 )
+ end
+
+ -- Detect collisions, e.g. { [0] = 'foo', ["0"] = 'bar' }
+ if seen[k] then
+ error( 'Collision for array key ' .. k .. ' when passing data from Lua to PHP' );
+ end
+ seen[k] = true
+
+ buf[#buf + 1] = recursiveEncode( value, level + 1 )
+ numElements = numElements + 1
+ end
+ buf[1] = 'a:' .. numElements .. ':{'
+ buf[#buf + 1] = '}'
+ return table.concat(buf)
+ elseif t == 'function' then
+ local id
+ if self.xchunks[var] then
+ id = self.xchunks[var]
+ else
+ id = self:addChunk(var)
+ end
+ return 'O:42:"Scribunto_LuaStandaloneInterpreterFunction":2:{s:13:"interpreterId";i:' ..
+ self.interpreterId .. ';s:2:"id";i:' .. id .. ';}'
+ elseif t == 'thread' then
+ error("Cannot pass thread to PHP")
+ elseif t == 'userdata' then
+ error("Cannot pass userdata to PHP")
+ else
+ error("Cannot pass unrecognised type to PHP")
+ end
+ end
+
+ return recursiveEncode( var, 0 ):gsub( '[\r\n\\]', serialize_replacements )
+end
+
+--- Convert a Lua expression string to its corresponding value.
+-- Convert any references of the form chunk[id] to the corresponding function
+-- values.
+function MWServer:unserialize( text )
+ local func = loadstring( 'return ' .. text )
+ if not func then
+ self:internalError( "MWServer:unserialize: invalid chunk" )
+ end
+ -- Don't waste JIT cache space by storing every message in it
+ if jit then
+ jit.off( func )
+ end
+ setfenv( func, { chunks = self.chunks } )
+ return func()
+end
+
+--- Decode a message header.
+-- @param header The header string
+-- @return The body length
+function MWServer:decodeHeader( header )
+ local length = string.sub( header, 1, 8 )
+ local check = string.sub( header, 9, 16 )
+ if not string.match( length, '^%x+$' ) or not string.match( check, '^%x+$' ) then
+ self:internalError( "Error decoding message header: " .. length .. '/' .. check )
+ end
+ length = tonumber( length, 16 )
+ check = tonumber( check, 16 )
+ if length * 2 - 1 ~= check then
+ self:internalError( "Error decoding message header" )
+ end
+ return length
+end
+
+--- Get a traceback similar to the one from debug.traceback(), but as a table
+-- rather than formatted as a string
+--
+-- @param The level to start at: 1 for the function that called getStructuredTrace()
+-- @return A table with the backtrace information
+function MWServer:getStructuredTrace( level )
+ level = level + 1
+ local trace = {}
+ while true do
+ local rawInfo = debug.getinfo( level, 'nSl' )
+ if rawInfo == nil then
+ break
+ end
+ local info = {}
+ for i, key in ipairs({'short_src', 'what', 'currentline', 'name', 'namewhat', 'linedefined'}) do
+ info[key] = rawInfo[key]
+ end
+ if string.match( info['short_src'], '/MWServer.lua$' ) then
+ info['short_src'] = 'MWServer.lua'
+ end
+ if string.match( rawInfo['short_src'], '/mw_main.lua$' ) then
+ info['short_src'] = 'mw_main.lua'
+ end
+ table.insert( trace, info )
+ level = level + 1
+ end
+ return trace
+end
+
+--- Create a table to be used as a restricted environment, based on the current
+-- global environment.
+--
+-- @return The environment table
+function MWServer:newEnvironment()
+ local allowedGlobals = {
+ -- base
+ "assert",
+ "error",
+ "getmetatable",
+ "ipairs",
+ "next",
+ "pairs",
+ "pcall",
+ "rawequal",
+ "rawget",
+ "rawset",
+ "select",
+ "setmetatable",
+ "tonumber",
+ "type",
+ "unpack",
+ "xpcall",
+ "_VERSION",
+ -- libs
+ "table",
+ "math"
+ }
+
+ local env = {}
+ for i = 1, #allowedGlobals do
+ env[allowedGlobals[i]] = mw.clone( _G[allowedGlobals[i]] )
+ end
+
+ -- Cloning 'string' doesn't work right, because strings still use the old
+ -- 'string' as the metatable. So just copy it.
+ env.string = string
+
+ env._G = env
+ env.tostring = function( val )
+ return self:tostring( val )
+ end
+ env.string.dump = nil
+ env.setfenv, env.getfenv = mw.makeProtectedEnvFuncs(
+ self.protectedEnvironments, self.protectedFunctions )
+ env.debug = {
+ traceback = debug.traceback
+ }
+ env.os = {
+ date = os.date,
+ difftime = os.difftime,
+ time = os.time,
+ clock = os.clock
+ }
+ return env
+end
+
+--- An implementation of tostring() which does not expose pointers.
+function MWServer:tostring(val)
+ local mt = getmetatable( val )
+ if mt and mt.__tostring then
+ return mt.__tostring(val)
+ end
+ local typeName = type(val)
+ local nonPointerTypes = {number = true, string = true, boolean = true, ['nil'] = true}
+ if nonPointerTypes[typeName] then
+ return tostring(val)
+ else
+ return typeName
+ end
+end
+
+return MWServer
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README
new file mode 100644
index 00000000..1321af9b
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README
@@ -0,0 +1,39 @@
+The lua binaries were compiled from a manually patched 5.1.5 source tree, in order
+to address security vulnerabilities reported in CVE-2014-5461.
+
+The Windows binaries were compiled statically and do not link against the Visual C++
+runtime libraries. They should function on Windows 7 and higher, and Windows Server
+2008 and higher.
+
+The Linux binaries were compiled using "make generic". Lua does not use
+autoconf, rather it encourages users to edit the makefile. The makefile patch
+used to create the Linux binaries is in generic.patch.
+
+Compiling with "make generic" avoids introducing dynamic library dependencies
+other than libc. This makes the binaries work on a far greater variety of Linux
+distributions.
+
+More information on the patch process may be found in the corresponding Phabricator
+issue: https://phabricator.wikimedia.org/T72541
+
+The following copyright and license restrictions apply to these Lua binaries:
+
+Copyright © 2005-2011 Tecgraf/PUC-Rio and the Kepler Project.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch
new file mode 100644
index 00000000..b9262318
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch
@@ -0,0 +1,22 @@
+diff -ru lua-5.1.5~/src/Makefile lua-5.1.5/src/Makefile
+--- lua-5.1.5~/src/Makefile 2012-02-14 07:41:22.000000000 +1100
++++ lua-5.1.5/src/Makefile 2012-04-11 16:13:04.548558332 +1000
+@@ -7,15 +7,15 @@
+ # Your platform. See PLATS for possible values.
+ PLAT= none
+
+-CC= gcc
+-CFLAGS= -O2 -Wall $(MYCFLAGS)
++CC=gcc
++CFLAGS=-fpie -O2 -Wall $(MYCFLAGS)
+ AR= ar rcu
+ RANLIB= ranlib
+ RM= rm -f
+ LIBS= -lm $(MYLIBS)
+
+ MYCFLAGS=
+-MYLDFLAGS=
++MYLDFLAGS=-pie
+ MYLIBS=
+
+ # == END OF USER SETTINGS. NO NEED TO CHANGE ANYTHING BELOW THIS LINE =========
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exe b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exe
new file mode 100644
index 00000000..b75924f1
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exe
Binary files differ
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe
new file mode 100644
index 00000000..2bb611ec
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe
Binary files differ
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/lua
new file mode 100755
index 00000000..82656e64
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/lua
Binary files differ
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua
new file mode 100755
index 00000000..183f1a98
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua
Binary files differ
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/lua
new file mode 100755
index 00000000..5bcf7369
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/lua
Binary files differ
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh
new file mode 100755
index 00000000..a0b48ef0
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+ulimit -St $1
+ulimit -Ht $2
+ulimit -v $3
+eval "exec $4"
+
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua
new file mode 100644
index 00000000..651ab751
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua
@@ -0,0 +1,8 @@
+package.path = arg[1] .. '/engines/LuaStandalone/?.lua;' ..
+ arg[1] .. '/engines/LuaCommon/lualib/?.lua'
+
+require('MWServer')
+require('mwInit')
+server = MWServer:new( arg[2], arg[3] )
+server:execute()
+
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt
new file mode 100644
index 00000000..ceb74d35
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt
@@ -0,0 +1,176 @@
+The Lua standalone engine has a message-based protocol for communicating between
+PHP and Lua.
+
+Messages start with a 16 byte header. The first 8 bytes are the body length in
+hexadecimal. The second 8 bytes are (length * 2 - 1) also in hexadecimal.
+
+For messages passed from PHP to Lua, the body is encoded as a Lua expression.
+The expression may reference a table in a variable called "chunks", which
+contains an array of functions.
+
+Messages passed from Lua to PHP have their body encoded in PHP serialize()
+format, and then "\\", "\r", and "\n" are replaced with "\\\\", "\\r", and
+"\\n" to avoid issues with text-mode file handles. They may include instances
+of function objects which have an "id" member for passing back to Lua as an
+index in the chunk table.
+
+The expressions encoded into the message bodies are associative arrays. The "op"
+member of the array gives the operation to be performed by the message.
+
+Every request message demands exactly one response message in reply. When a
+request message is sent, the responder does not need to send the corresponding
+response message as its next message. It may instead send its own request
+message. In this way, a stack of pending requests can be accumulated. This
+mechanism allows re-entrant and recursive calls.
+
+All numerically-indexed arrays should start from index 1 unless otherwise
+specified. Note that the number of values in an array may not match what Lua's
+'#' operator returns if the array contains nils.
+
+== Request messages sent from PHP to Lua ==
+
+=== loadString ===
+
+Load some executable Lua code (a "chunk") and return the resulting function ID.
+
+Message parameters:
+* op: "loadString"
+* text: The string to load
+* chunkName: The name of the string, for use in error messages
+
+On success, the response message is:
+
+* op: "return"
+* nvalues: 1
+* values: An array with a single element with the ID in it
+
+On failure, the response message is:
+
+* op: "error"
+* value: The error message
+
+=== call ===
+
+Call a Lua function.
+
+Message parameters:
+* op: "call"
+* id: The chunk ID
+* nargs: Number of arguments, including nils
+* args: The argument array
+
+On success, the response message is:
+
+* op: "return"
+* nvalues: Number of return values, including nils
+* values: All return values as an array
+
+On failure, the response message is:
+
+* op: "error"
+* value: The value given to error(), usually an error message
+* trace: A table giving a backtrace of the stack as it was when error() was
+ called, in a similar format to the one used by debug.getinfo(). Element 1 of
+ the table is the function that called error(), element 2 is the function that
+ called that, and so on.
+
+=== registerLibrary ===
+
+Register a set of functions in the sandbox environment.
+
+Message parameters:
+* op: "registerLibrary"
+* name: The global variable name to register. May contain "." characters to
+ specify a global variable subtable.
+* functions: An associative array mapping function name to ID
+
+On success, the response message is:
+
+* op: "return"
+* nvalues: 0
+* values: An empty array
+
+On failure the response message is:
+
+* op: "error"
+* value: The error message
+
+=== getStatus ===
+
+Get status information about the Lua process.
+
+Message parameters:
+* op: "getStatus"
+
+On success, the response message is:
+
+* op: "return"
+* nvalues: 1
+* values: An array with a single element, which is an associative array mapping
+ status key to value. The status keys are:
+** pid: The process identifier
+** time: The amount of user and system time spent by the process, measured in clock ticks
+** vsize: The virtual memory size in bytes
+** rss: The resident set size in bytes
+
+On failure, the response message is:
+
+* op: "return"
+* nvalues: 0
+* values: An empty array
+
+=== cleanupChunks ===
+
+Tell Lua to release any chunks no longer referenced by PHP.
+
+Message parameters:
+* op: "cleanupChunks"
+* ids: Table with keys being the chunk IDs still referenced by PHP, and non-falsey values
+
+The response message is:
+
+* op: "return"
+* nvalues: 0
+* values: An empty array
+
+=== quit ===
+
+Request graceful shutdown.
+
+Message parameters:
+* op: "quit"
+
+No return message will be sent.
+
+=== testquit ===
+
+Request non-graceful shutdown, for testing.
+
+Message parameters:
+* op: "testquit"
+
+No return message will be sent.
+
+== Request messages sent from Lua to PHP ==
+
+=== call ===
+
+Call a PHP function.
+
+Message parameters:
+* op: "call"
+* id: The function ID given by registerLibrary
+* nargs: Number of arguments, including nils
+* args: An array giving the function arguments
+
+On success, the response message is:
+
+* op: "return"
+* nvalues: Number of return values, including nils
+* values: All return values as an array
+
+On failure the response message is:
+
+* op: "error"
+* value: The error message
+