Yurik has uploaded a new change for review. https://gerrit.wikimedia.org/r/162850
Change subject: Added private-wiki-only lua extensions ...................................................................... Added private-wiki-only lua extensions * Several functions that expose MW API and access to request object * Abilitiy to override output to raw format * global var that controls all this $wgEnableSpecialLuaAccess Change-Id: Ifecbbb6e989f1ca510fedeb42cd90d18ea218ff9 --- M ZeroPortal.php A includes/LuaLibrary.lua A includes/LuaLibrary.php M includes/PortalSpecialPage.php 4 files changed, 354 insertions(+), 8 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/ZeroPortal refs/changes/50/162850/1 diff --git a/ZeroPortal.php b/ZeroPortal.php index b9249ad..a75eb5e 100644 --- a/ZeroPortal.php +++ b/ZeroPortal.php @@ -31,20 +31,22 @@ 'url' => 'https://www.mediawiki.org/wiki/Extension:ZeroPortal', ); -$cwd = __DIR__ . DIRECTORY_SEPARATOR; -$wgMessagesDirs['ZeroPortal'] = $cwd . 'i18n'; -$wgExtensionMessagesFiles['ZeroPortal'] = $cwd . 'ZeroPortal.i18n.php'; +$zpDir = __DIR__ . DIRECTORY_SEPARATOR; +$wgMessagesDirs['ZeroPortal'] = $zpDir . 'i18n'; +$wgExtensionMessagesFiles['ZeroPortal'] = $zpDir . 'ZeroPortal.i18n.php'; // autoload extension classes -$cwd .= 'includes' . DIRECTORY_SEPARATOR; +$zpDir .= 'includes' . DIRECTORY_SEPARATOR; foreach ( array( 'ApiZeroPortal', 'ConfigPageHooks', - 'ZeroConfigView', + 'LuaLibrary', 'PortalSpecialPage', + 'ZeroConfigView', ) as $key => $class ) { - $wgAutoloadClasses['ZeroPortal\\' . ( is_string( $key ) ? $key : $class )] = $cwd . $class . '.php'; + $wgAutoloadClasses['ZeroPortal\\' . ( is_string( $key ) ? $key : $class )] = $zpDir . $class . '.php'; } +unset( $zpDir ); $wgResourceModules['zeroportal.config'] = array( 'dependencies' => 'zerobanner.config.styles', // defined in ZeroBanner ext, used for sample banners @@ -56,6 +58,12 @@ $wgAPIModules['zeroportal'] = 'ZeroPortal\ApiZeroPortal'; $wgHooks['BeforePageDisplay'][] = 'ZeroPortal\ConfigPageHooks::onBeforePageDisplay'; +$wgHooks['ScribuntoExternalLibraries'][] = function( $engine, array &$extraLibraries ) { + if( $engine == 'lua' ) { + $extraLibraries['mw.zeroportal'] = 'ZeroPortal\LuaLibrary'; + } + return true; +}; // Set our own view class for the JsonZeroConfig model, and configure local storage $wgJsonConfigModels['JsonZeroConfig']['view'] = 'ZeroPortal\ZeroConfigView'; @@ -66,3 +74,4 @@ $wgExtensionMessagesFiles['ZeroPortalAlias'] = __DIR__ . DIRECTORY_SEPARATOR . 'ZeroPortal.alias.php'; $wgZeroPortalImpersonateUser = false; +$wgEnableSpecialLuaAccess = false; diff --git a/includes/LuaLibrary.lua b/includes/LuaLibrary.lua new file mode 100644 index 0000000..21a7d47 --- /dev/null +++ b/includes/LuaLibrary.lua @@ -0,0 +1,66 @@ +local p = {} +local php + +function p.api( data ) + return php.api( data ) +end + +function p.getAccountsForUser() + return php.getAccountsForUser() +end + +function p.getCheck( name ) + return php.getCheck( name ) +end + +function p.getUsername() + return php.getUsername() +end + +function p.getVal( name, default ) + return php.getVal( name, default ) +end + +function p.isAdmin() + return php.isAdmin() +end + +function p.jsonDecode( value ) + return php.jsonDecode( value ) +end + +function p.jsonEncode( value, escapeHtml ) + return php.jsonEncode( value, escapeHtml ) +end + +function p.register() + return php.register() +end + +function p.setCache( mode, seconds ) + return php.setCache( mode, seconds ) +end + +function p.setResult( result, mime ) + return php.setResult( result, mime ) +end + +function p.wasPosted() + return php.wasPosted() +end + + +function p.setupInterface( options ) + -- Boilerplate + p.setupInterface = nil + php = mw_interface + mw_interface = nil + + -- Register this library in the "mw" global + mw = mw or {} + mw.zeroportal = p + + package.loaded['mw.zeroportal'] = p +end + +return p diff --git a/includes/LuaLibrary.php b/includes/LuaLibrary.php new file mode 100644 index 0000000..e7b18ac --- /dev/null +++ b/includes/LuaLibrary.php @@ -0,0 +1,257 @@ +<?php + +namespace ZeroPortal; + +use ApiMain; +use DerivativeContext; +use FormatJson; +use Scribunto_LuaError; +use Scribunto_LuaLibraryBase; +use IContextSource; +use User; +use ZeroBanner\ZeroConfig; + +class LuaLibrary extends Scribunto_LuaLibraryBase { + + const luaNamespace = 'mw.zeroportal.'; + + /** @var IContextSource */ + private static $context = null; + + /** @var mixed */ + private static $result = null; + + /** @var string */ + private static $mime = 'text/plain'; + + /** @var int */ + private static $cacheMaxAge = 0; + + /** @var string */ + private static $cacheMode = 'private'; + + public function register() { + $functions = array(); + foreach ( array( + 'api', + 'getAccountsForUser', + 'getCheck', + 'getUsername', + 'getVal', + 'isAdmin', + 'jsonDecode', + 'jsonEncode', + 'register', + 'setCache', + 'setResult', + 'wasPosted', + ) as $f ) { + $functions[$f] = array( $this, $f ); + } + $moduleFileName = __DIR__ . DIRECTORY_SEPARATOR . 'LuaLibrary.lua'; + $this->getEngine()->registerInterface( $moduleFileName, $functions, array() ); + } + + /** + * @param IContextSource $context + */ + public static function setContext( $context ) { + self::$context = $context; + } + + /** + * @return IContextSource + * @throws Scribunto_LuaError + */ + public static function getContext() { + if ( !self::$context ) { + throw new Scribunto_LuaError( 'This function is only available in Special:ZeroPortal' ); + } + return self::$context; + } + + /** + * Finishes up the invocation, clean up context, and overrides output if needed + */ + public static function endInvoke() { + $ctx = self::getContext(); + $out = $ctx->getOutput(); + $resp = $ctx->getRequest()->response(); + self::setContext( null ); + + // + // FIXME: Need to review all of these headers + // + + $expiryUnixTime = ( self::$cacheMaxAge == 0 ? 1 : time() + self::$cacheMaxAge ); + $resp->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) ); + + $cacheMode = self::$cacheMode === 'public' ? 'public' : 'private'; + $resp->header( 'Cache-Control: ' . $cacheMode . ', must-revalidate, max-age=0' ); + + if ( self::$result ) { + $out->disable(); + $resp->header( 'Content-Type: ' . self::$mime . '; charset=UTF-8' ); + echo self::$result; + return true; + } else { + return false; + } + } + + /** + * Make an API call + * !!!SECURITY!!! This gives access to the entire (write-enabled) API as an impersonation user (admin)! + * @param array $data MW API parameters + * @return mixed[] + * @throws Scribunto_LuaError + */ + public function api( $data = null ) { + $this->incrementExpensiveFunctionCount(); + + $ctx = new DerivativeContext( self::getContext() ); + + global $wgZeroPortalImpersonateUser; + if ( $wgZeroPortalImpersonateUser ) { + $ctx->setUser( User::newFromName( $wgZeroPortalImpersonateUser ) ); + } + + $req = self::getContext()->getRequest(); + $ctx->setRequest( new \DerivativeRequest( $req, $data, $req->wasPosted() ) ); + + $api = new ApiMain( $ctx, true ); + $api->execute(); + return array( $api->getResult()->getData() ); + } + + /** + * Returns true if the value was passed as a parameter + * @param string $name + * @return bool[] + * @throws Scribunto_LuaError + */ + public function getCheck( $name = null ) { + $this->checkType( self::luaNamespace . __FUNCTION__, 1, $name, 'string' ); + return array( self::getContext()->getRequest()->getCheck( $name ) ); + } + + /** + * Returns a list of Zero config titles that this user has been assigned to as an admin + * @return string[]|false[] + */ + public function getAccountsForUser() { + $configs = array(); + list( $name ) = $this->getUsername(); + if ( $name ) { + ApiZeroPortal::iterateAllConfigs( false, + function ( ZeroConfig $content, $title ) use ( $name, &$configs ) { + if ( $name === true || in_array( $name, $content->admins() ) ) { + $configs[] = $title; + } + return false; + } ); + } + return array( $configs ); + } + + /** + * Return name of the logged in user, or false if anonymous + * @return string[]|bool[] + * @throws Scribunto_LuaError + */ + public function getUsername() { + $user = self::getContext()->getUser(); + if ( $user->isAnon() ) { + return array( false ); + } else { + return array( $user->getName() ); + } + } + + /** + * Get a value from the request (GET or POST). + * !!!SECURITY!!! This gives access to user's cookies + * @param string $name + * @param mixed $default + * @return mixed[] + * @throws Scribunto_LuaError + */ + public function getVal( $name = null, $default = null ) { + $this->checkType( self::luaNamespace . __FUNCTION__, 1, $name, 'string' ); + return array( self::getContext()->getRequest()->getVal( $name, $default ) ); + } + + /** + * Return true if this is an admin user + * @return bool[] + * @throws Scribunto_LuaError + */ + public function isAdmin() { + return array( self::getContext()->getUser()->isAllowed( 'zero-edit' ) ); + } + + /** + * Decode value as a JSON string. This function is enabled for all modules + * @param mixed $value + * @throws Scribunto_LuaError + * @return mixed[] + */ + public function jsonDecode( $value = null ) { + $this->checkType( self::luaNamespace . __FUNCTION__, 1, $value, 'string' ); + return array( FormatJson::decode( $value, true ) ); + } + + /** + * Encode value as a JSON string. This function is enabled for all modules + * @param mixed $value + * @param bool $escapeHtml + * @throws Scribunto_LuaError + * @return string + */ + public function jsonEncode( $value = null, $escapeHtml = null ) { + $this->checkTypeOptional( self::luaNamespace . __FUNCTION__, 2, $escapeHtml, 'boolean', false ); + $result = FormatJson::encode( $value, false, $escapeHtml ? FormatJson::UTF8_OK : FormatJson::ALL_OK ); + if ( $result === false ) { + throw new Scribunto_LuaError( 'Unable to encode value' ); + } + return array( $result ); + } + + /** + * Set raw result of the special page for the given caching type (public, private), and duration + * @param string $mode + * @param int $seconds + * @throws Scribunto_LuaError + */ + public function setCache( $mode = null, $seconds = null ) { + $this->checkType( self::luaNamespace . __FUNCTION__, 1, $mode, 'string' ); + $this->checkType( self::luaNamespace . __FUNCTION__, 2, $seconds, 'number' ); + self::getContext(); // ensure special page is enabled + self::$cacheMode = $mode; + self::$cacheMaxAge = $seconds; + } + + /** + * Set raw result of the special page for a given mime type + * @param mixed $result + * @param string $mime + * @throws Scribunto_LuaError + */ + public function setResult( $result = null, $mime = null ) { + self::getContext(); // ensure special page is enabled + $this->checkType( self::luaNamespace . __FUNCTION__, 1, $result, 'string' ); + $this->checkTypeOptional( self::luaNamespace . __FUNCTION__, 2, $mime, 'string', 'text/plain' ); + self::$result = $result; + self::$mime = $mime; + } + + /** + * Returns true if the present request was reached by a POST operation, false otherwise + * @return bool[] + * @throws Scribunto_LuaError + */ + public function wasPosted() { + return array( self::getContext()->getRequest()->wasPosted() ); + } + +} diff --git a/includes/PortalSpecialPage.php b/includes/PortalSpecialPage.php index d5d5b67..07a1a37 100644 --- a/includes/PortalSpecialPage.php +++ b/includes/PortalSpecialPage.php @@ -29,9 +29,17 @@ public function execute( $par ) { $out = $this->getOutput(); + global $wgEnableSpecialLuaAccess; + if ( $wgEnableSpecialLuaAccess !== true ) { + $out->addWikiText( '$wgEnableSpecialLuaAccess is off' ); + return; + } + + // $out->addModules( 'zeroportal.special.scripts' ); // $out->addModuleStyles( 'zeroportal.special.styles' ); + // todo: delete all parameter handling once we start using lua extension functions $user = $this->getUser(); if ( !$user->isAnon() ) { // Include all zero-config ids if this is an admin @@ -52,9 +60,15 @@ $configs = self::encodeParameter( $configs ); $state = self::encodeParameter( $this->getRequest()->getVal( 's' ) ); $user = self::encodeParameter( $user->isAnon() ? false : $user->getName() ); - $out->addWikiText( "{{#invoke:Portal|main|$configs|$state|$user}}" ); - if ( $this->getRequest()->getCheck( 'raw' ) ) { + + LuaLibrary::setContext( $this->getContext() ); + $out->addWikiText( "{{#invoke:Portal|main|$configs|$state|$user}}" ); + $done = LuaLibrary::endInvoke(); + + + // todo: delete all this once we start using lua extension functions + if ( !$done && $this->getRequest()->getCheck( 'raw' ) ) { // Output $text = html_entity_decode( $out->getHTML(), ENT_QUOTES | ENT_HTML5 ); -- To view, visit https://gerrit.wikimedia.org/r/162850 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ifecbbb6e989f1ca510fedeb42cd90d18ea218ff9 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/ZeroPortal Gerrit-Branch: master Gerrit-Owner: Yurik <yu...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits