mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Scribunto
synced 2024-11-28 10:10:04 +00:00
48748a6046
Implement Content::prepareSave() to ensure that any content directly passed to WikiPage::doEditContent() that doesn't run edit filters will still be validated. We have to use prepareSave() instead of Content::isValid() because validation depends upon the current Title. Create a ScribuntoContent::validate() convenience function to hold the logic for that and add a todo to use it in the EditFilterMerged hook. Also, remove a parser test that depended upon being able to save invalid modules directly, as what it is testing is no longer possible (unless it pre-dates making valid syntax a requirement). Bug: T145548 Change-Id: Ie57eff36100963f02899d669df7375577f7375e1
457 lines
14 KiB
PHP
457 lines
14 KiB
PHP
<?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
|
|
*/
|
|
|
|
/**
|
|
* Hooks for the Scribunto extension.
|
|
*/
|
|
class ScribuntoHooks {
|
|
/**
|
|
* 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;
|
|
|
|
if ( !defined( get_class( $frame ) . '::SUPPORTS_INDEX_OFFSET' ) ) {
|
|
throw new MWException(
|
|
'Scribunto needs MediaWiki 1.20 or later (Preprocessor::SUPPORTS_INDEX_OFFSET)' );
|
|
}
|
|
|
|
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',
|
|
array( 'args' => array( $moduleName ) ) );
|
|
}
|
|
$module = $engine->fetchModuleFromParser( $title );
|
|
if ( !$module ) {
|
|
throw new ScribuntoException( 'scribunto-common-nosuchmodule',
|
|
array( 'args' => array( $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 UtfNormal::cleanUp( strval( $result ) );
|
|
} catch ( ScribuntoException $e ) {
|
|
$trace = $e->getScriptTraceHtml( array( 'msgOptions' => array( 'content' ) ) );
|
|
$html = Html::element( 'p', array(), $e->getMessage() );
|
|
if ( $trace !== false ) {
|
|
$html .= Html::element( 'p',
|
|
array(),
|
|
wfMessage( 'scribunto-common-backtrace' )->inContentLanguage()->text()
|
|
) . $trace;
|
|
} else {
|
|
$html .= Html::element( 'p',
|
|
array(),
|
|
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 = array();
|
|
$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::newAccelerator( 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 \RunningStat\PSquare to do that.
|
|
if ( !class_exists( '\RunningStat\PSquare' ) || $cache instanceof EmptyBagOStuff ) {
|
|
return;
|
|
}
|
|
|
|
$key = $cache->makeGlobalKey( __METHOD__, $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 \RunningStat\PSquare( $threshold );
|
|
$ps->addObservation( $timing );
|
|
$cache->set( $key, $ps, 60 );
|
|
|
|
if ( $ps->getCount() < 1000 || $timing < $ps->getValue() ) {
|
|
return;
|
|
}
|
|
|
|
static $stats;
|
|
|
|
if ( !$stats ) {
|
|
$stats = RequestContext::getMain()->getStats();
|
|
}
|
|
|
|
$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.
|
|
*
|
|
* @deprecated
|
|
* @param Parser $parser
|
|
* @param string $report
|
|
* @return bool
|
|
*/
|
|
public static function reportLimits( Parser $parser, &$report ) {
|
|
if ( Scribunto::isParserEnginePresent( $parser ) ) {
|
|
$engine = Scribunto::getParserEngine( $parser );
|
|
$report .= $engine->getLimitReport();
|
|
}
|
|
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 ) {
|
|
// Unhook the deprecated hook, since the new one exists.
|
|
global $wgHooks;
|
|
unset( $wgHooks['ParserLimitReport']['scribunto'] );
|
|
|
|
if ( Scribunto::isParserEnginePresent( $parser ) ) {
|
|
$engine = Scribunto::getParserEngine( $parser );
|
|
$engine->reportLimitData( $output );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Formats the limit report data
|
|
*
|
|
* @param string $key
|
|
* @param string &$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 );
|
|
}
|
|
|
|
/**
|
|
* Adds the module namespaces.
|
|
*
|
|
* @param string[] $list
|
|
* @return bool
|
|
*/
|
|
public static function addCanonicalNamespaces( array &$list ) {
|
|
$list[NS_MODULE] = 'Module';
|
|
$list[NS_MODULE_TALK] = 'Module_talk';
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* EditPageBeforeEditChecks hook
|
|
*
|
|
* @param EditPage $editor
|
|
* @param array $checkboxes Checkbox array
|
|
* @param int $tabindex Current tabindex
|
|
* @return bool
|
|
*/
|
|
public static function beforeEditChecks( EditPage &$editor, &$checkboxes, &$tabindex ) {
|
|
if ( $editor->getTitle()->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
|
|
$editor->getArticle()->getContext()->getOutput()->addModules( 'ext.scribunto.edit' );
|
|
$editor->editFormTextAfterTools .= '<div id="mw-scribunto-console"></div>';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* EditPage::showReadOnlyForm:initial hook
|
|
*
|
|
* @param EditPage $editor
|
|
* @param OutputPage $output
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @todo this should use the EditFilterMergedContent hook instead
|
|
* so it can use ScribuntoContent::validate()
|
|
* @param EditPage $editor
|
|
* @param string $text
|
|
* @param string $error
|
|
* @param string $summary
|
|
* @return bool
|
|
*/
|
|
public static function validateScript( EditPage $editor, $text, &$error, $summary ) {
|
|
$title = $editor->getTitle();
|
|
|
|
if ( !$title->hasContentModel( CONTENT_MODEL_SCRIBUNTO ) ) {
|
|
return true;
|
|
}
|
|
|
|
$engine = Scribunto::newDefaultEngine();
|
|
$engine->setTitle( $title );
|
|
$status = $engine->validate( $text, $title->getPrefixedDBkey() );
|
|
if ( $status->isOK() ) {
|
|
return true;
|
|
}
|
|
|
|
$errmsg = $status->getWikiText( 'scribunto-error-short', 'scribunto-error-long' );
|
|
$error = <<<WIKI
|
|
<div class="errorbox">
|
|
{$errmsg}
|
|
</div>
|
|
<br clear="all" />
|
|
WIKI;
|
|
if ( isset( $status->scribunto_error->params['module'] ) ) {
|
|
$module = $status->scribunto_error->params['module'];
|
|
$line = $status->scribunto_error->params['line'];
|
|
if ( $module === $title->getPrefixedDBkey() && preg_match( '/^\d+$/', $line ) ) {
|
|
$out = $editor->getArticle()->getContext()->getOutput();
|
|
$out->addInlineScript( 'window.location.hash = ' . Xml::encodeJsVar( "#mw-ce-l$line" ) );
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param array $files
|
|
* @return bool
|
|
*/
|
|
public static function unitTestsList( array &$files ) {
|
|
$tests = array(
|
|
'engines/LuaStandalone/LuaStandaloneInterpreterTest.php',
|
|
'engines/LuaStandalone/StandaloneTest.php',
|
|
'engines/LuaSandbox/LuaSandboxInterpreterTest.php',
|
|
'engines/LuaSandbox/SandboxTest.php',
|
|
'engines/LuaCommon/LuaEnvironmentComparisonTest.php',
|
|
'engines/LuaCommon/CommonTest.php',
|
|
'engines/LuaCommon/SiteLibraryTest.php',
|
|
'engines/LuaCommon/UriLibraryTest.php',
|
|
'engines/LuaCommon/UstringLibraryTest.php',
|
|
'engines/LuaCommon/MessageLibraryTest.php',
|
|
'engines/LuaCommon/TitleLibraryTest.php',
|
|
'engines/LuaCommon/TextLibraryTest.php',
|
|
'engines/LuaCommon/HtmlLibraryTest.php',
|
|
'engines/LuaCommon/HashLibraryTest.php',
|
|
'engines/LuaCommon/LanguageLibraryTest.php',
|
|
'engines/LuaCommon/UstringLibraryPureLuaTest.php',
|
|
'engines/LuaCommon/LibraryUtilTest.php',
|
|
);
|
|
foreach ( $tests as $test ) {
|
|
$files[] = __DIR__ . '/../tests/' . $test;
|
|
}
|
|
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;
|
|
}
|
|
}
|