summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Scribunto/includes/common
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Scribunto/includes/common')
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/ApiScribuntoConsole.php150
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/Base.php361
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/Common.php209
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/Hooks.php397
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/ScribuntoContent.php160
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/ScribuntoContentHandler.php52
6 files changed, 1329 insertions, 0 deletions
diff --git a/www/wiki/extensions/Scribunto/includes/common/ApiScribuntoConsole.php b/www/wiki/extensions/Scribunto/includes/common/ApiScribuntoConsole.php
new file mode 100644
index 00000000..22f29668
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/common/ApiScribuntoConsole.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * API module for serving debug console requests on the edit page
+ */
+
+class ApiScribuntoConsole extends ApiBase {
+ const SC_MAX_SIZE = 500000;
+ const SC_SESSION_EXPIRY = 3600;
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $title = Title::newFromText( $params['title'] );
+ if ( !$title ) {
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
+ }
+
+ if ( $params['session'] ) {
+ $sessionId = $params['session'];
+ } else {
+ $sessionId = mt_rand( 0, 0x7fffffff );
+ }
+
+ $cache = ObjectCache::getInstance( CACHE_ANYTHING );
+ $sessionKey = $cache->makeKey( 'scribunto-console', $this->getUser()->getId(), $sessionId );
+ $session = null;
+ $sessionIsNew = false;
+ if ( $params['session'] ) {
+ $session = $cache->get( $sessionKey );
+ }
+ if ( !isset( $session['version'] ) ) {
+ $session = $this->newSession();
+ $sessionIsNew = true;
+ }
+
+ // Create a variable holding the session which will be stored if there
+ // are no errors. If there are errors, we don't want to store the current
+ // question to the state builder array, since that will cause subsequent
+ // requests to fail.
+ $newSession = $session;
+
+ if ( !empty( $params['clear'] ) ) {
+ $newSession['size'] -= strlen( implode( '', $newSession['questions'] ) );
+ $newSession['questions'] = [];
+ $session['questions'] = [];
+ }
+ if ( strlen( $params['question'] ) ) {
+ $newSession['size'] += strlen( $params['question'] );
+ $newSession['questions'][] = $params['question'];
+ }
+ if ( $params['content'] ) {
+ $newSession['size'] += strlen( $params['content'] ) - strlen( $newSession['content'] );
+ $newSession['content'] = $params['content'];
+ }
+
+ if ( $newSession['size'] > self::SC_MAX_SIZE ) {
+ $this->dieWithError( 'scribunto-console-too-large' );
+ }
+ $result = $this->runConsole( [
+ 'title' => $title,
+ 'content' => $newSession['content'],
+ 'prevQuestions' => $session['questions'],
+ 'question' => $params['question'],
+ ] );
+
+ if ( $result['type'] === 'error' ) {
+ // Restore the questions array
+ $newSession['questions'] = $session['questions'];
+ }
+ $cache->set( $sessionKey, $newSession, self::SC_SESSION_EXPIRY );
+ $result['session'] = $sessionId;
+ $result['sessionSize'] = $newSession['size'];
+ $result['sessionMaxSize'] = self::SC_MAX_SIZE;
+ if ( $sessionIsNew ) {
+ $result['sessionIsNew'] = '';
+ }
+ foreach ( $result as $key => $value ) {
+ $this->getResult()->addValue( null, $key, $value );
+ }
+ }
+
+ protected function runConsole( array $params ) {
+ global $wgParser;
+ $options = new ParserOptions;
+ $wgParser->startExternalParse( $params['title'], $options, Parser::OT_HTML, true );
+ $engine = Scribunto::getParserEngine( $wgParser );
+ try {
+ $result = $engine->runConsole( $params );
+ } catch ( ScribuntoException $e ) {
+ $trace = $e->getScriptTraceHtml();
+ $message = $e->getMessage();
+ $html = Html::element( 'p', [], $message );
+ if ( $trace !== false ) {
+ $html .= Html::element( 'p',
+ [],
+ $this->msg( 'scribunto-common-backtrace' )->inContentLanguage()->text()
+ ) . $trace;
+ }
+
+ return [
+ 'type' => 'error',
+ 'html' => $html,
+ 'message' => $message,
+ 'messagename' => $e->getMessageName() ];
+ }
+ return [
+ 'type' => 'normal',
+ 'print' => strval( $result['print'] ),
+ 'return' => strval( $result['return'] )
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ protected function newSession() {
+ return [
+ 'content' => '',
+ 'questions' => [],
+ 'size' => 0,
+ 'version' => 1,
+ ];
+ }
+
+ public function isInternal() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'title' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'content' => [
+ ApiBase::PARAM_TYPE => 'string'
+ ],
+ 'session' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ],
+ 'question' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'clear' => [
+ ApiBase::PARAM_TYPE => 'boolean',
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Scribunto/includes/common/Base.php b/www/wiki/extensions/Scribunto/includes/common/Base.php
new file mode 100644
index 00000000..e6c78111
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/common/Base.php
@@ -0,0 +1,361 @@
+<?php
+
+/**
+ * Wikitext scripting infrastructure for MediaWiki: base classes.
+ * Copyright (C) 2012 Victor Vasiliev <vasilvv@gmail.com> et al
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Base class for all script engines. Includes all code
+ * not related to particular modules, like tracking links between
+ * modules or loading module texts.
+ */
+abstract class ScribuntoEngineBase {
+
+ // Flags for ScribuntoEngineBase::getResourceUsage()
+ const CPU_SECONDS = 1;
+ const MEM_PEAK_BYTES = 2;
+
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var array
+ */
+ protected $options;
+
+ /**
+ * @var ScribuntoModuleBase[]
+ */
+ protected $modules = [];
+
+ /**
+ * @var Parser
+ */
+ protected $parser;
+
+ /**
+ * Creates a new module object within this engine
+ *
+ * @param string $text
+ * @param string|bool $chunkName
+ * @return ScribuntoModuleBase
+ */
+ abstract protected function newModule( $text, $chunkName );
+
+ /**
+ * Run an interactive console request
+ *
+ * @param array $params Associative array. Options are:
+ * - title: The title object for the module being debugged
+ * - content: The text content of the module
+ * - prevQuestions: An array of previous "questions" used to establish the state
+ * - question: The current "question", a string script
+ *
+ * @return array containing:
+ * - print: The resulting print buffer
+ * - return: The resulting return value
+ */
+ abstract function runConsole( array $params );
+
+ /**
+ * Get software information for Special:Version
+ * @param array &$software
+ */
+ abstract public function getSoftwareInfo( array &$software );
+
+ /**
+ * @param array $options Associative array of options:
+ * - parser: A Parser object
+ */
+ public function __construct( array $options ) {
+ $this->options = $options;
+ if ( isset( $options['parser'] ) ) {
+ $this->parser = $options['parser'];
+ }
+ if ( isset( $options['title'] ) ) {
+ $this->title = $options['title'];
+ }
+ }
+
+ public function __destruct() {
+ $this->destroy();
+ }
+
+ public function destroy() {
+ // Break reference cycles
+ $this->parser = null;
+ $this->title = null;
+ $this->modules = null;
+ }
+
+ /**
+ * @param Title $title
+ */
+ public function setTitle( $title ) {
+ $this->title = $title;
+ }
+
+ /**
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Get an element from the configuration array
+ *
+ * @param string $optionName
+ * @return mixed
+ */
+ public function getOption( $optionName ) {
+ return isset( $this->options[$optionName] )
+ ? $this->options[$optionName] : null;
+ }
+
+ /**
+ * @param string $message
+ * @param array $params
+ * @return ScribuntoException
+ */
+ public function newException( $message, array $params = [] ) {
+ return new ScribuntoException( $message, $this->getDefaultExceptionParams() + $params );
+ }
+
+ /**
+ * @return array
+ */
+ public function getDefaultExceptionParams() {
+ $params = [];
+ if ( $this->title ) {
+ $params['title'] = $this->title;
+ }
+ return $params;
+ }
+
+ /**
+ * Load a module from some parser-defined template loading mechanism and
+ * register a parser output dependency.
+ *
+ * Does not initialize the module, i.e. do not expect it to complain if the module
+ * text is garbage or has syntax error. Returns a module or null if it doesn't exist.
+ *
+ * @param Title $title The title of the module
+ * @return ScribuntoModuleBase|null
+ */
+ function fetchModuleFromParser( Title $title ) {
+ $key = $title->getPrefixedDBkey();
+ if ( !array_key_exists( $key, $this->modules ) ) {
+ list( $text, $finalTitle ) = $this->parser->fetchTemplateAndTitle( $title );
+ if ( $text === false ) {
+ $this->modules[$key] = null;
+ return null;
+ }
+
+ $finalKey = $finalTitle->getPrefixedDBkey();
+ if ( !isset( $this->modules[$finalKey] ) ) {
+ $this->modules[$finalKey] = $this->newModule( $text, $finalKey );
+ }
+ // Almost certainly $key === $finalKey, but just in case...
+ $this->modules[$key] = $this->modules[$finalKey];
+ }
+ return $this->modules[$key];
+ }
+
+ /**
+ * Validates the script and returns a Status object containing the syntax
+ * errors for the given code.
+ *
+ * @param string $text
+ * @param string|bool $chunkName
+ * @return Status
+ */
+ function validate( $text, $chunkName = false ) {
+ $module = $this->newModule( $text, $chunkName );
+ return $module->validate();
+ }
+
+ /**
+ * Get CPU and memory usage information, if the script engine
+ * provides it.
+ *
+ * If the script engine is capable of reporting CPU and memory usage
+ * data, it should override this implementation.
+ *
+ * @param int $resource One of ScribuntoEngineBase::CPU_SECONDS
+ * or ScribuntoEngineBase::MEM_PEAK_BYTES.
+ * @return float|int|false Resource usage for the specified resource
+ * or false if not available.
+ */
+ public function getResourceUsage( $resource ) {
+ return false;
+ }
+
+ /**
+ * Get the language for GeSHi syntax highlighter.
+ * @return string|false
+ */
+ function getGeSHiLanguage() {
+ return false;
+ }
+
+ /**
+ * Get the language for Ace code editor.
+ * @return string|false
+ */
+ function getCodeEditorLanguage() {
+ return false;
+ }
+
+ /**
+ * @return Parser
+ */
+ public function getParser() {
+ return $this->parser;
+ }
+
+ /**
+ * Load a list of all libraries supported by this engine
+ *
+ * The return value is an array with keys being the library name seen by
+ * the module and values being either a PHP class name or an array with the
+ * following elements:
+ * - class: (string) Class to load (required)
+ * - deferLoad: (bool) Library should not be loaded at startup; modules
+ * needing the library must request it (e.g. via 'require' in Lua)
+ *
+ * @param string $engine script engine we're using (eg: lua)
+ * @param array $coreLibraries Array of core libraries we support
+ * @return array
+ */
+ protected function getLibraries( $engine, array $coreLibraries = [] ) {
+ $extraLibraries = [];
+ Hooks::run( 'ScribuntoExternalLibraries', [ $engine, &$extraLibraries ] );
+ return $coreLibraries + $extraLibraries;
+ }
+
+ /**
+ * Load a list of all paths libraries can be in for this engine
+ *
+ * @param string $engine script engine we're using (eg: lua)
+ * @param array $coreLibraryPaths Array of library paths to use by default
+ * @return array
+ */
+ protected function getLibraryPaths( $engine, array $coreLibraryPaths = [] ) {
+ $extraLibraryPaths = [];
+ Hooks::run( 'ScribuntoExternalLibraryPaths', [ $engine, &$extraLibraryPaths ] );
+ return array_merge( $coreLibraryPaths, $extraLibraryPaths );
+ }
+
+ /**
+ * Add limit report data to a ParserOutput object
+ *
+ * @param ParserOutput $output ParserOutput object in which to add limit data
+ * @return null
+ */
+ public function reportLimitData( ParserOutput $output ) {
+ }
+
+ /**
+ * Format limit report data
+ *
+ * @param string $key
+ * @param mixed &$value
+ * @param string &$report
+ * @param bool $isHTML
+ * @param bool $localize
+ * @return bool
+ */
+ public function formatLimitData( $key, &$value, &$report, $isHTML, $localize ) {
+ return true;
+ }
+}
+
+/**
+ * Class that represents a module. Responsible for initial module parsing
+ * and maintaining the contents of the module.
+ */
+abstract class ScribuntoModuleBase {
+ /**
+ * @var ScribuntoEngineBase
+ */
+ protected $engine;
+
+ /**
+ * @var string
+ */
+ protected $code;
+
+ /**
+ * @var string|bool
+ */
+ protected $chunkName;
+
+ /**
+ * @param ScribuntoEngineBase $engine
+ * @param string $code
+ * @param string|bool $chunkName
+ */
+ public function __construct( ScribuntoEngineBase $engine, $code, $chunkName ) {
+ $this->engine = $engine;
+ $this->code = $code;
+ $this->chunkName = $chunkName;
+ }
+
+ /**
+ * @return ScribuntoEngineBase
+ */
+ public function getEngine() {
+ return $this->engine;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCode() {
+ return $this->code;
+ }
+
+ /**
+ * @return string|bool
+ */
+ public function getChunkName() {
+ return $this->chunkName;
+ }
+
+ /**
+ * Validates the script and returns a Status object containing the syntax
+ * errors for the given code.
+ *
+ * @return Status
+ */
+ abstract public function validate();
+
+ /**
+ * Invoke the function with the specified name.
+ *
+ * @param string $name
+ * @param PPFrame $frame
+ * @return string
+ */
+ abstract public function invoke( $name, $frame );
+}
diff --git a/www/wiki/extensions/Scribunto/includes/common/Common.php b/www/wiki/extensions/Scribunto/includes/common/Common.php
new file mode 100644
index 00000000..e92e615b
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/common/Common.php
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * Static function collection for general extension support.
+ */
+class Scribunto {
+ const LOCAL = 'local';
+
+ /**
+ * Create a new engine object with specified parameters.
+ *
+ * @param array $options
+ * @return ScribuntoEngineBase
+ */
+ public static function newEngine( $options ) {
+ if ( isset( $options['factory'] ) ) {
+ return call_user_func( $options['factory'], $options );
+ } else {
+ $class = $options['class'];
+ return new $class( $options );
+ }
+ }
+
+ /**
+ * Create a new engine object with default parameters
+ *
+ * @param array $extraOptions Extra options to pass to the constructor,
+ * in addition to the configured options
+ * @throws MWException
+ * @return ScribuntoEngineBase
+ */
+ public static function newDefaultEngine( $extraOptions = [] ) {
+ global $wgScribuntoDefaultEngine, $wgScribuntoEngineConf;
+ if ( !$wgScribuntoDefaultEngine ) {
+ throw new MWException(
+ 'Scribunto extension is enabled but $wgScribuntoDefaultEngine is not set'
+ );
+ }
+
+ if ( !isset( $wgScribuntoEngineConf[$wgScribuntoDefaultEngine] ) ) {
+ throw new MWException( 'Invalid scripting engine is specified in $wgScribuntoDefaultEngine' );
+ }
+ $options = $extraOptions + $wgScribuntoEngineConf[$wgScribuntoDefaultEngine];
+ return self::newEngine( $options );
+ }
+
+ /**
+ * Get an engine instance for the given parser, and cache it in the parser
+ * so that subsequent calls to this function for the same parser will return
+ * the same engine.
+ *
+ * @param Parser $parser
+ * @return ScribuntoEngineBase
+ */
+ public static function getParserEngine( Parser $parser ) {
+ if ( empty( $parser->scribunto_engine ) ) {
+ $parser->scribunto_engine = self::newDefaultEngine( [ 'parser' => $parser ] );
+ $parser->scribunto_engine->setTitle( $parser->getTitle() );
+ }
+ return $parser->scribunto_engine;
+ }
+
+ /**
+ * Check if an engine instance is present in the given parser
+ *
+ * @param Parser $parser
+ * @return bool
+ */
+ public static function isParserEnginePresent( Parser $parser ) {
+ return !empty( $parser->scribunto_engine );
+ }
+
+ /**
+ * Remove the current engine instance from the parser
+ * @param Parser $parser
+ */
+ public static function resetParserEngine( Parser $parser ) {
+ if ( !empty( $parser->scribunto_engine ) ) {
+ $parser->scribunto_engine->destroy();
+ $parser->scribunto_engine = null;
+ }
+ }
+
+ /**
+ * Test whether the page should be considered a documentation page
+ *
+ * @param Title $title
+ * @param Title|null &$forModule Module for which this is a doc page
+ * @return bool
+ */
+ public static function isDocPage( Title $title, Title &$forModule = null ) {
+ $docPage = wfMessage( 'scribunto-doc-page-name' )->inContentLanguage();
+ if ( $docPage->isDisabled() ) {
+ return false;
+ }
+
+ // Canonicalize the input pseudo-title. The unreplaced "$1" shouldn't
+ // cause a problem.
+ $docTitle = Title::newFromText( $docPage->plain() );
+ if ( !$docTitle ) {
+ return false;
+ }
+ $docPage = $docTitle->getPrefixedText();
+
+ // Make it into a regex, and match it against the input title
+ $docPage = str_replace( '\\$1', '(.+)', preg_quote( $docPage, '/' ) );
+ if ( preg_match( "/^$docPage$/", $title->getPrefixedText(), $m ) ) {
+ $forModule = Title::makeTitleSafe( NS_MODULE, $m[1] );
+ return $forModule !== null;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Return the Title for the documentation page
+ *
+ * @param Title $title
+ * @return Title|null
+ */
+ public static function getDocPage( Title $title ) {
+ $docPage = wfMessage( 'scribunto-doc-page-name', $title->getText() )->inContentLanguage();
+ if ( $docPage->isDisabled() ) {
+ return null;
+ }
+
+ return Title::newFromText( $docPage->plain() );
+ }
+}
+
+/**
+ * An exception class which represents an error in the script. This does not
+ * normally abort the request, instead it is caught and shown to the user.
+ */
+class ScribuntoException extends MWException {
+ /**
+ * @var string
+ */
+ public $messageName;
+
+ /**
+ * @var array
+ */
+ public $messageArgs;
+
+ /**
+ * @var array
+ */
+ public $params;
+
+ /**
+ * @param string $messageName
+ * @param array $params
+ */
+ function __construct( $messageName, $params = [] ) {
+ if ( isset( $params['args'] ) ) {
+ $this->messageArgs = $params['args'];
+ } else {
+ $this->messageArgs = [];
+ }
+ if ( isset( $params['module'] ) && isset( $params['line'] ) ) {
+ $codeLocation = false;
+ if ( isset( $params['title'] ) ) {
+ $moduleTitle = Title::newFromText( $params['module'] );
+ if ( $moduleTitle && $moduleTitle->equals( $params['title'] ) ) {
+ $codeLocation = wfMessage( 'scribunto-line', $params['line'] )->inContentLanguage()->text();
+ }
+ }
+ if ( $codeLocation === false ) {
+ $codeLocation = wfMessage(
+ 'scribunto-module-line',
+ $params['module'],
+ $params['line']
+ )->inContentLanguage()->text();
+ }
+ } else {
+ $codeLocation = '[UNKNOWN]';
+ }
+ array_unshift( $this->messageArgs, $codeLocation );
+ $msg = wfMessage( $messageName )->params( $this->messageArgs )->inContentLanguage()->text();
+ parent::__construct( $msg );
+
+ $this->messageName = $messageName;
+ $this->params = $params;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMessageName() {
+ return $this->messageName;
+ }
+
+ public function toStatus() {
+ $args = array_merge( [ $this->messageName ], $this->messageArgs );
+ $status = call_user_func_array( [ 'Status', 'newFatal' ], $args );
+ $status->scribunto_error = $this;
+ return $status;
+ }
+
+ /**
+ * Get the backtrace as HTML, or false if there is none available.
+ * @param array $options
+ * @return bool|string
+ */
+ public function getScriptTraceHtml( $options = [] ) {
+ return false;
+ }
+}
diff --git a/www/wiki/extensions/Scribunto/includes/common/Hooks.php b/www/wiki/extensions/Scribunto/includes/common/Hooks.php
new file mode 100644
index 00000000..9ad63731
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/common/Hooks.php
@@ -0,0 +1,397 @@
+<?php
+/**
+ * Wikitext scripting infrastructure for MediaWiki: hooks.
+ * Copyright (C) 2009-2012 Victor Vasiliev <vasilvv@gmail.com>
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+use MediaWiki\MediaWikiServices;
+use UtfNormal\Validator;
+use Wikimedia\PSquare;
+
+/**
+ * Hooks for the Scribunto extension.
+ */
+class ScribuntoHooks {
+
+ /**
+ * Define content handler constant upon extension registration
+ */
+ public static function onRegistration() {
+ define( 'CONTENT_MODEL_SCRIBUNTO', 'Scribunto' );
+ }
+
+ /**
+ * Get software information for Special:Version
+ *
+ * @param array &$software
+ * @return bool
+ */
+ public static function getSoftwareInfo( array &$software ) {
+ $engine = Scribunto::newDefaultEngine();
+ $engine->setTitle( Title::makeTitle( NS_SPECIAL, 'Version' ) );
+ $engine->getSoftwareInfo( $software );
+ return true;
+ }
+
+ /**
+ * Register parser hooks.
+ *
+ * @param Parser &$parser
+ * @return bool
+ */
+ public static function setupParserHook( Parser &$parser ) {
+ $parser->setFunctionHook( 'invoke', 'ScribuntoHooks::invokeHook', Parser::SFH_OBJECT_ARGS );
+ return true;
+ }
+
+ /**
+ * Called when the interpreter is to be reset.
+ *
+ * @param Parser &$parser
+ * @return bool
+ */
+ public static function clearState( Parser &$parser ) {
+ Scribunto::resetParserEngine( $parser );
+ return true;
+ }
+
+ /**
+ * Called when the parser is cloned
+ *
+ * @param Parser $parser
+ * @return bool
+ */
+ public static function parserCloned( Parser $parser ) {
+ $parser->scribunto_engine = null;
+ return true;
+ }
+
+ /**
+ * Hook function for {{#invoke:module|func}}
+ *
+ * @param Parser &$parser
+ * @param PPFrame $frame
+ * @param array $args
+ * @throws MWException
+ * @throws ScribuntoException
+ * @return string
+ */
+ public static function invokeHook( Parser &$parser, PPFrame $frame, array $args ) {
+ global $wgScribuntoGatherFunctionStats;
+
+ try {
+ if ( count( $args ) < 2 ) {
+ throw new ScribuntoException( 'scribunto-common-nofunction' );
+ }
+ $moduleName = trim( $frame->expand( $args[0] ) );
+ $engine = Scribunto::getParserEngine( $parser );
+
+ $title = Title::makeTitleSafe( NS_MODULE, $moduleName );
+ if ( !$title || !$title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
+ throw new ScribuntoException( 'scribunto-common-nosuchmodule',
+ [ 'args' => [ $moduleName ] ] );
+ }
+ $module = $engine->fetchModuleFromParser( $title );
+ if ( !$module ) {
+ throw new ScribuntoException( 'scribunto-common-nosuchmodule',
+ [ 'args' => [ $moduleName ] ] );
+ }
+ $functionName = trim( $frame->expand( $args[1] ) );
+
+ $bits = $args[1]->splitArg();
+ unset( $args[0] );
+ unset( $args[1] );
+
+ // If $bits['index'] is empty, then the function name was parsed as a
+ // key=value pair (because of an equals sign in it), and since it didn't
+ // have an index, we don't need the index offset.
+ $childFrame = $frame->newChild( $args, $title, $bits['index'] === '' ? 0 : 1 );
+
+ if ( $wgScribuntoGatherFunctionStats ) {
+ $u0 = $engine->getResourceUsage( $engine::CPU_SECONDS );
+ $result = $module->invoke( $functionName, $childFrame );
+ $u1 = $engine->getResourceUsage( $engine::CPU_SECONDS );
+
+ if ( $u1 > $u0 ) {
+ $timingMs = (int)( 1000 * ( $u1 - $u0 ) );
+ // Since the overhead of stats is worst when when #invoke
+ // calls are very short, don't process measurements <= 20ms.
+ if ( $timingMs > 20 ) {
+ self::reportTiming( $moduleName, $functionName, $timingMs );
+ }
+ }
+ } else {
+ $result = $module->invoke( $functionName, $childFrame );
+ }
+
+ return Validator::cleanUp( strval( $result ) );
+ } catch ( ScribuntoException $e ) {
+ $trace = $e->getScriptTraceHtml( [ 'msgOptions' => [ 'content' ] ] );
+ $html = Html::element( 'p', [], $e->getMessage() );
+ if ( $trace !== false ) {
+ $html .= Html::element( 'p',
+ [],
+ wfMessage( 'scribunto-common-backtrace' )->inContentLanguage()->text()
+ ) . $trace;
+ } else {
+ $html .= Html::element( 'p',
+ [],
+ wfMessage( 'scribunto-common-no-details' )->inContentLanguage()->text()
+ );
+ }
+ $out = $parser->getOutput();
+ $errors = $out->getExtensionData( 'ScribuntoErrors' );
+ if ( $errors === null ) {
+ // On first hook use, set up error array and output
+ $errors = [];
+ $parser->addTrackingCategory( 'scribunto-common-error-category' );
+ $out->addModules( 'ext.scribunto.errors' );
+ }
+ $errors[] = $html;
+ $out->setExtensionData( 'ScribuntoErrors', $errors );
+ $out->addJsConfigVars( 'ScribuntoErrors', $errors );
+ $id = 'mw-scribunto-error-' . ( count( $errors ) - 1 );
+ $parserError = htmlspecialchars( $e->getMessage() );
+
+ // #iferror-compatible error element
+ return "<strong class=\"error\"><span class=\"scribunto-error\" id=\"$id\">" .
+ $parserError. "</span></strong>";
+ }
+ }
+
+ /**
+ * Record stats on slow function calls.
+ *
+ * @param string $moduleName
+ * @param string $functionName
+ * @param int $timing Function execution time in milliseconds.
+ */
+ public static function reportTiming( $moduleName, $functionName, $timing ) {
+ global $wgScribuntoGatherFunctionStats, $wgScribuntoSlowFunctionThreshold;
+
+ if ( !$wgScribuntoGatherFunctionStats ) {
+ return;
+ }
+
+ $threshold = $wgScribuntoSlowFunctionThreshold;
+ if ( !( is_float( $threshold ) && $threshold > 0 && $threshold < 1 ) ) {
+ return;
+ }
+
+ static $cache;
+
+ if ( !$cache ) {
+ $cache = ObjectCache::getLocalServerInstance( CACHE_NONE );
+
+ }
+
+ // To control the sampling rate, we keep a compact histogram of
+ // observations in APC, and extract the Nth percentile (specified
+ // via $wgScribuntoSlowFunctionThreshold; defaults to 0.90).
+ // We need APC and \Wikimedia\PSquare to do that.
+ if ( !class_exists( PSquare::class ) || $cache instanceof EmptyBagOStuff ) {
+ return;
+ }
+
+ $cacheVersion = '1';
+ $key = $cache->makeGlobalKey( __METHOD__, $cacheVersion, $threshold );
+
+ // This is a classic "read-update-write" critical section with no
+ // mutual exclusion, but the only consequence is that some samples
+ // will be dropped. We only need enough samples to estimate the
+ // the shape of the data, so that's fine.
+ $ps = $cache->get( $key ) ?: new PSquare( $threshold );
+ $ps->addObservation( $timing );
+ $cache->set( $key, $ps, 60 );
+
+ if ( $ps->getCount() < 1000 || $timing < $ps->getValue() ) {
+ return;
+ }
+
+ static $stats;
+
+ if ( !$stats ) {
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ }
+
+ $metricKey = sprintf( 'scribunto.traces.%s__%s__%s', wfWikiId(), $moduleName, $functionName );
+ $stats->timing( $metricKey, $timing );
+ }
+
+ /**
+ * @param Title $title
+ * @param string &$languageCode
+ * @return bool
+ */
+ public static function getCodeLanguage( Title $title, &$languageCode ) {
+ global $wgScribuntoUseCodeEditor;
+ if ( $wgScribuntoUseCodeEditor && $title->hasContentModel( CONTENT_MODEL_SCRIBUNTO )
+ ) {
+ $engine = Scribunto::newDefaultEngine();
+ if ( $engine->getCodeEditorLanguage() ) {
+ $languageCode = $engine->getCodeEditorLanguage();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Set the Scribunto content handler for modules
+ *
+ * @param Title $title
+ * @param string &$model
+ * @return bool
+ */
+ public static function contentHandlerDefaultModelFor( Title $title, &$model ) {
+ if ( $title->getNamespace() == NS_MODULE && !Scribunto::isDocPage( $title ) ) {
+ $model = CONTENT_MODEL_SCRIBUNTO;
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Adds report of number of evaluations by the single wikitext page.
+ *
+ * @param Parser $parser
+ * @param ParserOutput $output
+ * @return bool
+ */
+ public static function reportLimitData( Parser $parser, ParserOutput $output ) {
+ if ( Scribunto::isParserEnginePresent( $parser ) ) {
+ $engine = Scribunto::getParserEngine( $parser );
+ $engine->reportLimitData( $output );
+ }
+ return true;
+ }
+
+ /**
+ * Formats the limit report data
+ *
+ * @param string $key
+ * @param mixed &$value
+ * @param string &$report
+ * @param bool $isHTML
+ * @param bool $localize
+ * @return bool
+ */
+ public static function formatLimitData( $key, &$value, &$report, $isHTML, $localize ) {
+ $engine = Scribunto::newDefaultEngine();
+ return $engine->formatLimitData( $key, $value, $report, $isHTML, $localize );
+ }
+
+ /**
+ * EditPage::showStandardInputs:options hook
+ *
+ * @param EditPage $editor
+ * @param OutputPage $output
+ * @param int &$tab Current tabindex
+ * @return bool
+ */
+ public static function showStandardInputsOptions( EditPage $editor, OutputPage $output, &$tab ) {
+ if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
+ $output->addModules( 'ext.scribunto.edit' );
+ $editor->editFormTextAfterTools .= '<div id="mw-scribunto-console"></div>';
+ }
+ return true;
+ }
+
+ /**
+ * EditPage::showReadOnlyForm:initial hook
+ *
+ * @param EditPage $editor
+ * @param OutputPage $output
+ * @return bool
+ */
+ public static function showReadOnlyFormInitial( EditPage $editor, OutputPage $output ) {
+ if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
+ $output->addModules( 'ext.scribunto.edit' );
+ $editor->editFormTextAfterContent .= '<div id="mw-scribunto-console"></div>';
+ }
+ return true;
+ }
+
+ /**
+ * EditPageBeforeEditButtons hook
+ *
+ * @param EditPage &$editor
+ * @param array &$buttons Button array
+ * @param int &$tabindex Current tabindex
+ * @return bool
+ */
+ public static function beforeEditButtons( EditPage &$editor, array &$buttons, &$tabindex ) {
+ if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
+ unset( $buttons['preview'] );
+ }
+ return true;
+ }
+
+ /**
+ * @param IContextSource $context
+ * @param Content $content
+ * @param Status $status
+ * @return bool
+ */
+ public static function validateScript( IContextSource $context, Content $content,
+ Status $status
+ ) {
+ $title = $context->getTitle();
+
+ if ( !$content instanceof ScribuntoContent ) {
+ return true;
+ }
+
+ /** @suppress PhanUndeclaredMethod */
+ $validateStatus = $content->validate( $title );
+ if ( $validateStatus->isOK() ) {
+ return true;
+ }
+
+ $status->merge( $validateStatus );
+
+ if ( isset( $validateStatus->scribunto_error->params['module'] ) ) {
+ $module = $validateStatus->scribunto_error->params['module'];
+ $line = $validateStatus->scribunto_error->params['line'];
+ if ( $module === $title->getPrefixedDBkey() && preg_match( '/^\d+$/', $line ) ) {
+ $out = $context->getOutput();
+ $out->addInlineScript( 'window.location.hash = ' . Xml::encodeJsVar( "#mw-ce-l$line" ) );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param Article &$article
+ * @param bool &$outputDone
+ * @param bool &$pcache
+ * @return bool
+ */
+ public static function showDocPageHeader( Article &$article, &$outputDone, &$pcache ) {
+ $title = $article->getTitle();
+ if ( Scribunto::isDocPage( $title, $forModule ) ) {
+ $article->getContext()->getOutput()->addHTML(
+ wfMessage( 'scribunto-doc-page-header', $forModule->getPrefixedText() )->parseAsBlock()
+ );
+ }
+ return true;
+ }
+}
diff --git a/www/wiki/extensions/Scribunto/includes/common/ScribuntoContent.php b/www/wiki/extensions/Scribunto/includes/common/ScribuntoContent.php
new file mode 100644
index 00000000..ff2ec8e4
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/common/ScribuntoContent.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Scribunto Content Model
+ *
+ * @file
+ * @ingroup Extensions
+ * @ingroup Scribunto
+ *
+ * @author Brad Jorsch <bjorsch@wikimedia.org>
+ */
+
+/**
+ * Represents the content of a Scribunto script page
+ */
+class ScribuntoContent extends TextContent {
+
+ function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_SCRIBUNTO );
+ }
+
+ /**
+ * Checks whether the script is valid
+ *
+ * @param Title $title
+ * @return Status
+ */
+ public function validate( Title $title ) {
+ $engine = Scribunto::newDefaultEngine();
+ $engine->setTitle( $title );
+ return $engine->validate( $this->getNativeData(), $title->getPrefixedDBkey() );
+ }
+
+ public function prepareSave( WikiPage $page, $flags, $parentRevId, User $user ) {
+ return $this->validate( $page->getTitle() );
+ }
+
+ /**
+ * Parse the Content object and generate a ParserOutput from the result.
+ *
+ * @param Title $title The page title to use as a context for rendering
+ * @param null|int $revId The revision being rendered (optional)
+ * @param null|ParserOptions $options Any parser options
+ * @param bool $generateHtml Whether to generate HTML (default: true).
+ * @param ParserOutput &$output ParserOutput representing the HTML form of the text.
+ * @return ParserOutput
+ */
+ protected function fillParserOutput(
+ Title $title, $revId, ParserOptions $options, $generateHtml, ParserOutput &$output
+ ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+
+ // Get documentation, if any
+ $output = new ParserOutput();
+ $doc = Scribunto::getDocPage( $title );
+ if ( $doc ) {
+ $msg = wfMessage(
+ $doc->exists() ? 'scribunto-doc-page-show' : 'scribunto-doc-page-does-not-exist',
+ $doc->getPrefixedText()
+ )->inContentLanguage();
+
+ if ( !$msg->isDisabled() ) {
+ // We need the ParserOutput for categories and such, so we
+ // can't use $msg->parse().
+ $docViewLang = $doc->getPageViewLanguage();
+ $dir = $docViewLang->getDir();
+
+ // Code is forced to be ltr, but the documentation can be rtl.
+ // Correct direction class is needed for correct formatting.
+ // The possible classes are
+ // mw-content-ltr or mw-content-rtl
+ $dirClass = "mw-content-$dir";
+
+ $docWikitext = Html::rawElement(
+ 'div',
+ [
+ 'lang' => $docViewLang->getHtmlCode(),
+ 'dir' => $dir,
+ 'class' => $dirClass,
+ ],
+ // Line breaks are needed so that wikitext would be
+ // appropriately isolated for correct parsing. See Bug 60664.
+ "\n" . $msg->plain() . "\n"
+ );
+
+ if ( !$options ) {
+ // NOTE: use canonical options per default to produce cacheable output
+ $options = ContentHandler::getForTitle( $doc )->makeParserOptions( 'canonical' );
+ } else {
+ if ( $options->getTargetLanguage() === null ) {
+ $options->setTargetLanguage( $doc->getPageLanguage() );
+ }
+ }
+
+ $output = $wgParser->parse( $docWikitext, $title, $options, true, true, $revId );
+ }
+
+ // Mark the doc page as a transclusion, so we get purged when it
+ // changes.
+ $output->addTemplate( $doc, $doc->getArticleID(), $doc->getLatestRevID() );
+ }
+
+ // Validate the script, and include an error message and tracking
+ // category if it's invalid
+ $status = $this->validate( $title );
+ if ( !$status->isOK() ) {
+ $output->setText( $output->getRawText() .
+ Html::rawElement( 'div', [ 'class' => 'errorbox' ],
+ $status->getHTML( 'scribunto-error-short', 'scribunto-error-long' )
+ )
+ );
+ $output->addTrackingCategory( 'scribunto-module-with-errors-category', $title );
+ }
+
+ if ( !$generateHtml ) {
+ // We don't need the actual HTML
+ $output->setText( '' );
+ return $output;
+ }
+
+ $engine = Scribunto::newDefaultEngine();
+ $engine->setTitle( $title );
+ if ( $this->highlight( $text, $output, $engine ) ) {
+ return $output;
+ }
+
+ // No GeSHi, or GeSHi can't parse it, use plain <pre>
+ $output->setText( $output->getRawText() .
+ "<pre class='mw-code mw-script' dir='ltr'>\n" .
+ htmlspecialchars( $text ) .
+ "\n</pre>\n"
+ );
+
+ return $output;
+ }
+
+ /**
+ * Adds syntax highlighting to the output (or do not touch it and return false).
+ * @param string $text
+ * @param ParserOutput $output
+ * @param ScribuntoEngineBase $engine
+ * @return bool Success status
+ */
+ protected function highlight( $text, ParserOutput $output, ScribuntoEngineBase $engine ) {
+ global $wgScribuntoUseGeSHi;
+ $language = $engine->getGeSHiLanguage();
+ if ( $wgScribuntoUseGeSHi && class_exists( SyntaxHighlight::class ) && $language ) {
+ $status = SyntaxHighlight::highlight( $text, $language );
+ if ( $status->isGood() ) {
+ // @todo replace addModuleStyles line with the appropriate call on
+ // SyntaxHighlight once one is created
+ $output->addModuleStyles( 'ext.pygments' );
+ $output->setText( $output->getRawText() . $status->getValue() );
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/www/wiki/extensions/Scribunto/includes/common/ScribuntoContentHandler.php b/www/wiki/extensions/Scribunto/includes/common/ScribuntoContentHandler.php
new file mode 100644
index 00000000..ad2c1baa
--- /dev/null
+++ b/www/wiki/extensions/Scribunto/includes/common/ScribuntoContentHandler.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Scribunto Content Handler
+ *
+ * @file
+ * @ingroup Extensions
+ * @ingroup Scribunto
+ *
+ * @author Brad Jorsch <bjorsch@wikimedia.org>
+ */
+
+class ScribuntoContentHandler extends CodeContentHandler {
+
+ /**
+ * @param string $modelId
+ * @param string[] $formats
+ */
+ public function __construct(
+ $modelId = CONTENT_MODEL_SCRIBUNTO, $formats = [ CONTENT_FORMAT_TEXT ]
+ ) {
+ parent::__construct( $modelId, $formats );
+ }
+
+ protected function getContentClass() {
+ return 'ScribuntoContent';
+ }
+
+ /**
+ * @param string $format
+ * @return bool
+ */
+ public function isSupportedFormat( $format ) {
+ // An error in an earlier version of Scribunto means we might see this.
+ if ( $format === 'CONTENT_FORMAT_TEXT' ) {
+ $format = CONTENT_FORMAT_TEXT;
+ }
+ return parent::isSupportedFormat( $format );
+ }
+
+ /**
+ * Only allow this content handler to be used in the Module namespace
+ * @param Title $title
+ * @return bool
+ */
+ public function canBeUsedOn( Title $title ) {
+ if ( $title->getNamespace() !== NS_MODULE ) {
+ return false;
+ }
+
+ return parent::canBeUsedOn( $title );
+ }
+}