diff options
Diffstat (limited to 'www/wiki/extensions/Scribunto/includes/common')
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 ); + } +} |