* 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 "" . $parserError. ""; } } /** * 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 ) ) { global $wgOut; $wgOut->addModules( 'ext.scribunto.edit' ); $editor->editFormTextAfterTools .= '
'; } 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 .= '
'; } 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 EditPage $editor * @param string $text * @param string $error * @param string $summary * @return bool */ public static function validateScript( EditPage $editor, $text, &$error, $summary ) { global $wgOut; $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 = << {$errmsg}
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 ) ) { $wgOut->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 ) { global $wgOut; $title = $article->getTitle(); if ( Scribunto::isDocPage( $title, $forModule ) ) { $wgOut->addHTML( wfMessage( 'scribunto-doc-page-header', $forModule->getPrefixedText() )->parseAsBlock() ); } return true; } }