summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Scribunto/includes/common/Hooks.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Scribunto/includes/common/Hooks.php')
-rw-r--r--www/wiki/extensions/Scribunto/includes/common/Hooks.php397
1 files changed, 397 insertions, 0 deletions
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;
+ }
+}