diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Gadgets/includes |
first commit
Diffstat (limited to 'www/wiki/extensions/Gadgets/includes')
16 files changed, 2535 insertions, 0 deletions
diff --git a/www/wiki/extensions/Gadgets/includes/Gadget.php b/www/wiki/extensions/Gadgets/includes/Gadget.php new file mode 100644 index 00000000..245458a9 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/Gadget.php @@ -0,0 +1,315 @@ +<?php +/** + * Gadgets extension - lets users select custom javascript gadgets + * + * For more info see https://www.mediawiki.org/wiki/Extension:Gadgets + * + * @file + * @ingroup Extensions + * @author Daniel Kinzler, brightbyte.de + * @copyright © 2007 Daniel Kinzler + * @license GPL-2.0-or-later + */ + +/** + * Wrapper for one gadget. + */ +class Gadget { + /** + * Increment this when changing class structure + */ + const GADGET_CLASS_VERSION = 9; + + const CACHE_TTL = 86400; + + private $scripts = [], + $styles = [], + $dependencies = [], + $peers = [], + $messages = [], + $name, + $definition, + $resourceLoaded = false, + $requiredRights = [], + $requiredSkins = [], + $targets = [ 'desktop' ], + $onByDefault = false, + $hidden = false, + $type = '', + $category; + + public function __construct( array $options ) { + foreach ( $options as $member => $option ) { + switch ( $member ) { + case 'scripts': + case 'styles': + case 'dependencies': + case 'peers': + case 'messages': + case 'name': + case 'definition': + case 'resourceLoaded': + case 'requiredRights': + case 'requiredSkins': + case 'targets': + case 'onByDefault': + case 'type': + case 'hidden': + case 'category': + $this->{$member} = $option; + break; + default: + throw new InvalidArgumentException( "Unrecognized '$member' parameter" ); + } + } + } + + /** + * Create a object based on the metadata in a GadgetDefinitionContent object + * + * @param string $id + * @param GadgetDefinitionContent $content + * @return Gadget + */ + public static function newFromDefinitionContent( $id, GadgetDefinitionContent $content ) { + $data = $content->getAssocArray(); + $prefixGadgetNs = function ( $page ) { + return 'Gadget:' . $page; + }; + $info = [ + 'name' => $id, + 'resourceLoaded' => true, + 'requiredRights' => $data['settings']['rights'], + 'onByDefault' => $data['settings']['default'], + 'hidden' => $data['settings']['hidden'], + 'requiredSkins' => $data['settings']['skins'], + 'category' => $data['settings']['category'], + 'scripts' => array_map( $prefixGadgetNs, $data['module']['scripts'] ), + 'styles' => array_map( $prefixGadgetNs, $data['module']['styles'] ), + 'dependencies' => $data['module']['dependencies'], + 'peers' => $data['module']['peers'], + 'messages' => $data['module']['messages'], + 'type' => $data['module']['type'], + ]; + + return new self( $info ); + } + + /** + * Get a placeholder object to use if a gadget doesn't exist + * + * @param string $id name + * @return Gadget + */ + public static function newEmptyGadget( $id ) { + return new self( [ 'name' => $id ] ); + } + + /** + * Whether the provided gadget id is valid + * + * @param string $id + * @return bool + */ + public static function isValidGadgetID( $id ) { + return strlen( $id ) > 0 && ResourceLoader::isValidModuleName( self::getModuleName( $id ) ); + } + + /** + * @return string Gadget name + */ + public function getName() { + return $this->name; + } + + /** + * @return string Gadget description parsed into HTML + */ + public function getDescription() { + return wfMessage( "gadget-{$this->getName()}" )->parse(); + } + + /** + * @return string Wikitext of gadget description + */ + public function getRawDescription() { + return wfMessage( "gadget-{$this->getName()}" )->plain(); + } + + /** + * @return string Name of category (aka section) our gadget belongs to. Empty string if none. + */ + public function getCategory() { + return $this->category; + } + + /** + * @param string $id Name of gadget + * @return string Name of ResourceLoader module for the gadget + */ + public static function getModuleName( $id ) { + return "ext.gadget.{$id}"; + } + + /** + * Checks whether this gadget is enabled for given user + * + * @param User $user user to check against + * @return bool + */ + public function isEnabled( $user ) { + return (bool)$user->getOption( "gadget-{$this->name}", $this->onByDefault ); + } + + /** + * Checks whether given user has permissions to use this gadget + * + * @param User $user user to check against + * @return bool + */ + public function isAllowed( $user ) { + return count( array_intersect( $this->requiredRights, $user->getRights() ) ) == + count( $this->requiredRights ) + && ( $this->requiredSkins === true + || !count( $this->requiredSkins ) + || in_array( $user->getOption( 'skin' ), $this->requiredSkins ) + ); + } + + /** + * @return bool Whether this gadget is on by default for everyone + * (but can be disabled in preferences) + */ + public function isOnByDefault() { + return $this->onByDefault; + } + + /** + * @return bool + */ + public function isHidden() { + return $this->hidden; + } + + /** + * @return bool Whether all of this gadget's JS components support ResourceLoader + */ + public function supportsResourceLoader() { + return $this->resourceLoaded; + } + + /** + * @return bool Whether this gadget has resources that can be loaded via ResourceLoaderb + */ + public function hasModule() { + return count( $this->styles ) + + ( $this->supportsResourceLoader() ? count( $this->scripts ) : 0 ) + > 0; + } + + /** + * @return string Definition for this gadget from MediaWiki:gadgets-definition + */ + public function getDefinition() { + return $this->definition; + } + + /** + * @return array Array of pages with JS (including namespace) + */ + public function getScripts() { + return $this->scripts; + } + + /** + * @return array Array of pages with CSS (including namespace) + */ + public function getStyles() { + return $this->styles; + } + + /** + * @return array Array of all of this gadget's resources + */ + public function getScriptsAndStyles() { + return array_merge( $this->scripts, $this->styles ); + } + + /** + * @return array + */ + public function getTargets() { + return $this->targets; + } + + /** + * Returns list of scripts that don't support ResourceLoader + * @return Array + */ + public function getLegacyScripts() { + if ( $this->supportsResourceLoader() ) { + return []; + } + return $this->scripts; + } + + /** + * Returns names of resources this gadget depends on + * @return Array + */ + public function getDependencies() { + return $this->dependencies; + } + + /** + * Get list of extra modules that should be loaded when this gadget is enabled + * + * Primary use case is to allow a Gadget that includes JavaScript to also load + * a (usually, hidden) styles-type module to be applied to the page. Dependencies + * don't work for this use case as those would not be part of page rendering. + * + * @return Array + */ + public function getPeers() { + return $this->peers; + } + + /** + * @return array + */ + public function getMessages() { + return $this->messages; + } + + /** + * Returns array of permissions required by this gadget + * @return Array + */ + public function getRequiredRights() { + return $this->requiredRights; + } + + /** + * Returns array of skins where this gadget works + * @return Array + */ + public function getRequiredSkins() { + return $this->requiredSkins; + } + + /** + * Returns the load type of this Gadget's ResourceLoader module + * @return string 'styles' or 'general' + */ + public function getType() { + if ( $this->type === 'styles' || $this->type === 'general' ) { + return $this->type; + } + // Similar to ResourceLoaderWikiModule default + if ( $this->styles && !$this->scripts && !$this->dependencies ) { + return 'styles'; + } else { + return 'general'; + } + } +} diff --git a/www/wiki/extensions/Gadgets/includes/GadgetDefinitionNamespaceRepo.php b/www/wiki/extensions/Gadgets/includes/GadgetDefinitionNamespaceRepo.php new file mode 100644 index 00000000..fdecee80 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/GadgetDefinitionNamespaceRepo.php @@ -0,0 +1,167 @@ +<?php + +use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\Database; + +/** + * GadgetRepo implementation where each gadget has a page in + * the Gadget definition namespace, and scripts and styles are + * located in the Gadget namespace. + */ +class GadgetDefinitionNamespaceRepo extends GadgetRepo { + /** + * How long in seconds the list of gadget ids and + * individual gadgets should be cached for (1 day) + */ + const CACHE_TTL = 86400; + + /** + * @var WANObjectCache + */ + private $wanCache; + + public function __construct() { + $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + } + + /** + * Get a list of gadget ids from cache/database + * + * @return string[] + */ + public function getGadgetIds() { + $key = $this->getGadgetIdsKey(); + + return $this->wanCache->getWithSetCallback( + $key, + self::CACHE_TTL, + function ( $oldValue, &$ttl, array &$setOpts ) { + $dbr = wfGetDB( DB_REPLICA ); + $setOpts += Database::getCacheSetOptions( $dbr ); + + return $dbr->selectFieldValues( + 'page', + 'page_title', + [ 'page_namespace' => NS_GADGET_DEFINITION ], + __METHOD__ + ); + }, + [ + 'checkKeys' => [ $key ], + 'pcTTL' => WANObjectCache::TTL_PROC_SHORT, + 'lockTSE' => 30 + ] + ); + } + + /** + * @inheritDoc + */ + public function handlePageUpdate( LinkTarget $target ) { + if ( $target->inNamespace( NS_GADGET_DEFINITION ) ) { + $this->purgeGadgetEntry( $target->getText() ); + } + } + + /** + * @inheritDoc + */ + public function handlePageCreation( LinkTarget $target ) { + if ( $target->inNamespace( NS_GADGET_DEFINITION ) ) { + $this->purgeGadgetIdsList(); + } + } + + /** + * @inheritDoc + */ + public function handlePageDeletion( LinkTarget $target ) { + if ( $target->inNamespace( NS_GADGET_DEFINITION ) ) { + $this->purgeGadgetIdsList(); + $this->purgeGadgetEntry( $target->getText() ); + } + } + + /** + * Purge the list of gadget ids when a page is deleted or if a new page is created + */ + public function purgeGadgetIdsList() { + $this->wanCache->touchCheckKey( $this->getGadgetIdsKey() ); + } + + /** + * @param string $id + * @throws InvalidArgumentException + * @return Gadget + */ + public function getGadget( $id ) { + $key = $this->getGadgetCacheKey( $id ); + $gadget = $this->wanCache->getWithSetCallback( + $key, + self::CACHE_TTL, + /** + * @suppress PhanTypeMismatchArgument + */ + function ( $old, &$ttl, array &$setOpts ) use ( $id ) { + $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); + $title = Title::makeTitleSafe( NS_GADGET_DEFINITION, $id ); + if ( !$title ) { + $ttl = WANObjectCache::TTL_UNCACHEABLE; + return null; + } + + $rev = Revision::newFromTitle( $title ); + if ( !$rev ) { + $ttl = WANObjectCache::TTL_UNCACHEABLE; + return null; + } + + $content = $rev->getContent(); + if ( !$content instanceof GadgetDefinitionContent ) { + // Uhm... + $ttl = WANObjectCache::TTL_UNCACHEABLE; + return null; + } + + return Gadget::newFromDefinitionContent( $id, $content ); + }, + [ + 'checkKeys' => [ $key ], + 'pcTTL' => WANObjectCache::TTL_PROC_SHORT, + 'lockTSE' => 30 + ] + ); + + if ( $gadget === null ) { + throw new InvalidArgumentException( "No gadget registered for '$id'" ); + } + + return $gadget; + } + + /** + * Update the cache for a specific Gadget whenever it is updated + * + * @param string $id + */ + public function purgeGadgetEntry( $id ) { + $this->wanCache->touchCheckKey( $this->getGadgetCacheKey( $id ) ); + } + + /** + * @return string + */ + private function getGadgetIdsKey() { + return $this->wanCache->makeKey( 'gadgets', 'namespace', 'ids' ); + } + + /** + * @param string $id + * @return string + */ + private function getGadgetCacheKey( $id ) { + return $this->wanCache->makeKey( + 'gadgets', 'object', md5( $id ), Gadget::GADGET_CLASS_VERSION ); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/GadgetHooks.php b/www/wiki/extensions/Gadgets/includes/GadgetHooks.php new file mode 100644 index 00000000..e493baf7 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/GadgetHooks.php @@ -0,0 +1,342 @@ +<?php + +/** + * Copyright © 2007 Daniel Kinzler + * + * 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 + * + * @file + */ +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\WrappedString; + +class GadgetHooks { + /** + * PageContentSaveComplete hook handler. + * + * @param WikiPage $wikiPage + * @param User $user + * @param Content $content New page content + * @return bool + */ + public static function onPageContentSaveComplete( WikiPage $wikiPage, $user, $content ) { + // update cache if MediaWiki:Gadgets-definition was edited + GadgetRepo::singleton()->handlePageUpdate( $wikiPage->getTitle() ); + return true; + } + + /** + * UserGetDefaultOptions hook handler + * @param array &$defaultOptions Array of default preference keys and values + * @return bool + */ + public static function userGetDefaultOptions( &$defaultOptions ) { + $gadgets = GadgetRepo::singleton()->getStructuredList(); + if ( !$gadgets ) { + return true; + } + + /** + * @var $gadget Gadget + */ + foreach ( $gadgets as $thisSection ) { + foreach ( $thisSection as $gadgetId => $gadget ) { + if ( $gadget->isOnByDefault() ) { + $defaultOptions['gadget-' . $gadgetId] = 1; + } + } + } + + return true; + } + + /** + * GetPreferences hook handler. + * @param User $user + * @param array &$preferences Preference descriptions + * @return bool + */ + public static function getPreferences( $user, &$preferences ) { + $gadgets = GadgetRepo::singleton()->getStructuredList(); + if ( !$gadgets ) { + return true; + } + + $options = []; + $default = []; + foreach ( $gadgets as $section => $thisSection ) { + $available = []; + + /** + * @var $gadget Gadget + */ + foreach ( $thisSection as $gadget ) { + if ( !$gadget->isHidden() && $gadget->isAllowed( $user ) ) { + $gname = $gadget->getName(); + # bug 30182: dir="auto" because it's often not translated + $desc = '<span dir="auto">' . $gadget->getDescription() . '</span>'; + $available[$desc] = $gname; + if ( $gadget->isEnabled( $user ) ) { + $default[] = $gname; + } + } + } + + if ( $section !== '' ) { + $section = wfMessage( "gadget-section-$section" )->parse(); + + if ( count( $available ) ) { + $options[$section] = $available; + } + } else { + $options = array_merge( $options, $available ); + } + } + + $preferences['gadgets-intro'] = + [ + 'type' => 'info', + 'label' => ' ', + 'default' => Xml::tags( 'tr', [], + Xml::tags( 'td', [ 'colspan' => 2 ], + wfMessage( 'gadgets-prefstext' )->parseAsBlock() ) ), + 'section' => 'gadgets', + 'raw' => 1, + 'rawrow' => 1, + 'noglobal' => true, + ]; + + $preferences['gadgets'] = + [ + 'type' => 'multiselect', + 'options' => $options, + 'section' => 'gadgets', + 'label' => ' ', + 'prefix' => 'gadget-', + 'default' => $default, + 'noglobal' => true, + ]; + + return true; + } + + /** + * ResourceLoaderRegisterModules hook handler. + * @param ResourceLoader &$resourceLoader + * @return bool + */ + public static function registerModules( &$resourceLoader ) { + $repo = GadgetRepo::singleton(); + $ids = $repo->getGadgetIds(); + + foreach ( $ids as $id ) { + $resourceLoader->register( Gadget::getModuleName( $id ), [ + 'class' => 'GadgetResourceLoaderModule', + 'id' => $id, + ] ); + } + + return true; + } + + /** + * BeforePageDisplay hook handler. + * @param OutputPage $out + * @return bool + */ + public static function beforePageDisplay( $out ) { + $repo = GadgetRepo::singleton(); + $ids = $repo->getGadgetIds(); + if ( !$ids ) { + return true; + } + + $lb = new LinkBatch(); + $lb->setCaller( __METHOD__ ); + $enabledLegacyGadgets = []; + + /** + * @var $gadget Gadget + */ + $user = $out->getUser(); + foreach ( $ids as $id ) { + try { + $gadget = $repo->getGadget( $id ); + } catch ( InvalidArgumentException $e ) { + continue; + } + $peers = []; + foreach ( $gadget->getPeers() as $peerName ) { + try { + $peers[] = $repo->getGadget( $peerName ); + } catch ( InvalidArgumentException $e ) { + // Ignore + // @todo: Emit warning for invalid peer? + } + } + if ( $gadget->isEnabled( $user ) && $gadget->isAllowed( $user ) ) { + if ( $gadget->hasModule() ) { + if ( $gadget->getType() === 'styles' ) { + $out->addModuleStyles( Gadget::getModuleName( $gadget->getName() ) ); + } else { + $out->addModules( Gadget::getModuleName( $gadget->getName() ) ); + // Load peer modules + foreach ( $peers as $peer ) { + if ( $peer->getType() === 'styles' ) { + $out->addModuleStyles( Gadget::getModuleName( $peer->getName() ) ); + } + // Else, if not type=styles: Use dependencies instead. + // Note: No need for recursion as styles modules don't support + // either of 'dependencies' and 'peers'. + } + } + } + + if ( $gadget->getLegacyScripts() ) { + $enabledLegacyGadgets[] = $id; + } + } + } + + $strings = []; + foreach ( $enabledLegacyGadgets as $id ) { + $strings[] = self::makeLegacyWarning( $id ); + } + $out->addHTML( WrappedString::join( "\n", $strings ) ); + + return true; + } + + private static function makeLegacyWarning( $id ) { + $special = SpecialPage::getTitleFor( 'Gadgets' ); + + return ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.log.warn', [ + "Gadget \"$id\" was not loaded. Please migrate it to use ResourceLoader. " . + 'See <' . $special->getCanonicalURL() . '>.' + ] ) + ); + } + + /** + * Valid gadget definition page after content is modified + * + * @param IContextSource $context + * @param Content $content + * @param Status $status + * @param string $summary + * @throws Exception + * @return bool + * @suppress PhanUndeclaredMethod + */ + public static function onEditFilterMergedContent( $context, $content, $status, $summary ) { + $title = $context->getTitle(); + + if ( !$title->inNamespace( NS_GADGET_DEFINITION ) ) { + return true; + } + + if ( !$content instanceof GadgetDefinitionContent ) { + // This should not be possible? + throw new Exception( + "Tried to save non-GadgetDefinitionContent to {$title->getPrefixedText()}" + ); + } + + $status = $content->validate(); + if ( !$status->isGood() ) { + $status->merge( $status ); + return false; + } + + return true; + } + + /** + * After a new page is created in the Gadget definition namespace, + * invalidate the list of gadget ids + * + * @param WikiPage $page + */ + public static function onPageContentInsertComplete( WikiPage $page ) { + if ( $page->getTitle()->inNamespace( NS_GADGET_DEFINITION ) ) { + GadgetRepo::singleton()->handlePageCreation( $page->getTitle() ); + } + } + + /** + * Mark the Title as having a content model of javascript or css for pages + * in the Gadget namespace based on their file extension + * + * @param Title $title + * @param string &$model + * @return bool + */ + public static function onContentHandlerDefaultModelFor( Title $title, &$model ) { + if ( $title->inNamespace( NS_GADGET ) ) { + preg_match( '!\.(css|js)$!u', $title->getText(), $ext ); + $ext = isset( $ext[1] ) ? $ext[1] : ''; + switch ( $ext ) { + case 'js': + $model = 'javascript'; + return false; + case 'css': + $model = 'css'; + return false; + } + } + + return true; + } + + /** + * Set the CodeEditor language for Gadget definition pages. It already + * knows the language for Gadget: namespace pages. + * + * @param Title $title + * @param string &$lang + * @return bool + */ + public static function onCodeEditorGetPageLanguage( Title $title, &$lang ) { + if ( $title->hasContentModel( 'GadgetDefinition' ) ) { + $lang = 'json'; + return false; + } + + return true; + } + + /** + * Add the GadgetUsage special page to the list of QueryPages. + * @param array &$queryPages + * @return bool + */ + public static function onwgQueryPages( &$queryPages ) { + $queryPages[] = [ 'SpecialGadgetUsage', 'GadgetUsage' ]; + return true; + } + + /** + * Prevent gadget preferences from being deleted. + * @link https://www.mediawiki.org/wiki/Manual:Hooks/DeleteUnknownPreferences + * @suppress PhanParamTooMany + * @param string[] &$where Array of where clause conditions to add to. + * @param IDatabase $db + */ + public static function onDeleteUnknownPreferences( &$where, IDatabase $db ) { + $where[] = 'up_property NOT' . $db->buildLike( 'gadget-', $db->anyString() ); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/GadgetRepo.php b/www/wiki/extensions/Gadgets/includes/GadgetRepo.php new file mode 100644 index 00000000..11996f8e --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/GadgetRepo.php @@ -0,0 +1,104 @@ +<?php + +use MediaWiki\Linker\LinkTarget; + +abstract class GadgetRepo { + + /** + * @var GadgetRepo|null + */ + private static $instance; + + /** + * Get the ids of the gadgets provided by this repository + * + * It's possible this could be out of sync with what + * getGadget() will return due to caching + * + * @return string[] + */ + abstract public function getGadgetIds(); + + /** + * Get the Gadget object for a given gadget id + * + * @param string $id + * @throws InvalidArgumentException + * @return Gadget + */ + abstract public function getGadget( $id ); + + /** + * Given that the provided page was updated, invalidate + * caches if necessary + * + * @param LinkTarget $target + * + * @return void + */ + public function handlePageUpdate( LinkTarget $target ) { + } + + /** + * Given that the provided page was created, invalidate + * caches if necessary + * + * @param LinkTarget $target + * + * @return void + */ + public function handlePageCreation( LinkTarget $target ) { + } + + /** + * Given that the provided page was updated, invalidate + * caches if necessary + * + * @param LinkTarget $target + * + * @return void + */ + public function handlePageDeletion( LinkTarget $target ) { + } + + /** + * Get a list of gadgets sorted by category + * + * @return array [ 'category' => [ 'name' => $gadget ] ] + */ + public function getStructuredList() { + $list = []; + foreach ( $this->getGadgetIds() as $id ) { + try { + $gadget = $this->getGadget( $id ); + } catch ( InvalidArgumentException $e ) { + continue; + } + $list[$gadget->getCategory()][$gadget->getName()] = $gadget; + } + + return $list; + } + + /** + * Get the configured default GadgetRepo. + * + * @return GadgetRepo + */ + public static function singleton() { + if ( self::$instance === null ) { + global $wgGadgetsRepoClass; // @todo use Config here + self::$instance = new $wgGadgetsRepoClass(); + } + return self::$instance; + } + + /** + * Should only be used by unit tests + * + * @param GadgetRepo|null $repo + */ + public static function setSingleton( $repo = null ) { + self::$instance = $repo; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/GadgetResourceLoaderModule.php b/www/wiki/extensions/Gadgets/includes/GadgetResourceLoaderModule.php new file mode 100644 index 00000000..13d16cdd --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/GadgetResourceLoaderModule.php @@ -0,0 +1,93 @@ +<?php + +/** + * Class representing a list of resources for one gadget, basically a wrapper + * around the Gadget class. + */ +class GadgetResourceLoaderModule extends ResourceLoaderWikiModule { + /** + * @var string + */ + private $id; + + /** + * @var Gadget + */ + private $gadget; + + /** + * @param array $options + */ + public function __construct( array $options ) { + $this->id = $options['id']; + } + + /** + * @return Gadget instance this module is about + */ + private function getGadget() { + if ( !$this->gadget ) { + try { + $this->gadget = GadgetRepo::singleton()->getGadget( $this->id ); + } catch ( InvalidArgumentException $e ) { + // Fallback to a placeholder object... + $this->gadget = Gadget::newEmptyGadget( $this->id ); + } + } + + return $this->gadget; + } + + /** + * Overrides the function from ResourceLoaderWikiModule class + * @param ResourceLoaderContext $context + * @return array + */ + protected function getPages( ResourceLoaderContext $context ) { + $gadget = $this->getGadget(); + $pages = []; + + foreach ( $gadget->getStyles() as $style ) { + $pages[$style] = [ 'type' => 'style' ]; + } + + if ( $gadget->supportsResourceLoader() ) { + foreach ( $gadget->getScripts() as $script ) { + $pages[$script] = [ 'type' => 'script' ]; + } + } + + return $pages; + } + + /** + * Overrides ResourceLoaderModule::getDependencies() + * @param ResourceLoaderContext $context + * @return string[] Names of resources this module depends on + */ + public function getDependencies( ResourceLoaderContext $context = null ) { + return $this->getGadget()->getDependencies(); + } + + /** + * Overrides ResourceLoaderWikiModule::getType() + * @return string ResourceLoaderModule::LOAD_STYLES or ResourceLoaderModule::LOAD_GENERAL + */ + public function getType() { + return $this->getGadget()->getType() === 'styles' + ? ResourceLoaderModule::LOAD_STYLES + : ResourceLoaderModule::LOAD_GENERAL; + } + + public function getMessages() { + return $this->getGadget()->getMessages(); + } + + public function getTargets() { + return $this->getGadget()->getTargets(); + } + + public function getGroup() { + return 'site'; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/MediaWikiGadgetsDefinitionRepo.php b/www/wiki/extensions/Gadgets/includes/MediaWikiGadgetsDefinitionRepo.php new file mode 100644 index 00000000..84c84b6c --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/MediaWikiGadgetsDefinitionRepo.php @@ -0,0 +1,261 @@ +<?php + +use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\Database; + +/** + * Gadgets repo powered by MediaWiki:Gadgets-definition + */ +class MediaWikiGadgetsDefinitionRepo extends GadgetRepo { + const CACHE_VERSION = 2; + + private $definitionCache; + + /** + * @param string $id + * + * @return Gadget + * @throws InvalidArgumentException + */ + public function getGadget( $id ) { + $gadgets = $this->loadGadgets(); + if ( !isset( $gadgets[$id] ) ) { + throw new InvalidArgumentException( "No gadget registered for '$id'" ); + } + + return $gadgets[$id]; + } + + public function getGadgetIds() { + $gadgets = $this->loadGadgets(); + if ( $gadgets ) { + return array_keys( $gadgets ); + } else { + return []; + } + } + + public function handlePageUpdate( LinkTarget $target ) { + if ( $target->getNamespace() == NS_MEDIAWIKI && $target->getText() == 'Gadgets-definition' ) { + $this->purgeDefinitionCache(); + } + } + + /** + * Purge the definitions cache, for example if MediaWiki:Gadgets-definition + * was edited. + */ + private function purgeDefinitionCache() { + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $cache->touchCheckKey( $this->getCheckKey() ); + } + + private function getCheckKey() { + return wfMemcKey( 'gadgets-definition', Gadget::GADGET_CLASS_VERSION, self::CACHE_VERSION ); + } + + /** + * Loads list of gadgets and returns it as associative array of sections with gadgets + * e.g. [ 'sectionnname1' => [ $gadget1, $gadget2 ], + * 'sectionnname2' => [ $gadget3 ] ]; + * @return array|bool Gadget array or false on failure + */ + protected function loadGadgets() { + if ( $this->definitionCache !== null ) { + return $this->definitionCache; // process cache hit + } + + // Ideally $t1Cache is APC, and $wanCache is memcached + $t1Cache = ObjectCache::getLocalServerInstance( 'hash' ); + $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + + $key = $this->getCheckKey(); + + // (a) Check the tier 1 cache + $value = $t1Cache->get( $key ); + // Check if it passes a blind TTL check (avoids I/O) + if ( $value && ( microtime( true ) - $value['time'] ) < 10 ) { + $this->definitionCache = $value['gadgets']; // process cache + return $this->definitionCache; + } + // Cache generated after the "check" time should be up-to-date + $ckTime = $wanCache->getCheckKeyTime( $key ) + WANObjectCache::HOLDOFF_TTL; + if ( $value && $value['time'] > $ckTime ) { + $this->definitionCache = $value['gadgets']; // process cache + return $this->definitionCache; + } + + // (b) Fetch value from WAN cache or regenerate if needed. + // This is hit occasionally and more so when the list changes. + $us = $this; + $value = $wanCache->getWithSetCallback( + $key, + Gadget::CACHE_TTL, + function ( $old, &$ttl, &$setOpts ) use ( $us ) { + $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); + + $now = microtime( true ); + $gadgets = $us->fetchStructuredList(); + if ( $gadgets === false ) { + $ttl = WANObjectCache::TTL_UNCACHEABLE; + } + + return [ 'gadgets' => $gadgets, 'time' => $now ]; + }, + [ 'checkKeys' => [ $key ], 'lockTSE' => 300 ] + ); + + // Update the tier 1 cache as needed + if ( $value['gadgets'] !== false && $value['time'] > $ckTime ) { + // Set a modest TTL to keep the WAN key in cache + $t1Cache->set( $key, $value, mt_rand( 300, 600 ) ); + } + + $this->definitionCache = $value['gadgets']; + + return $this->definitionCache; + } + + /** + * Fetch list of gadgets and returns it as associative array of sections with gadgets + * e.g. [ $name => $gadget1, etc. ] + * @param string $forceNewText Injected text of MediaWiki:gadgets-definition [optional] + * @return array|bool + */ + public function fetchStructuredList( $forceNewText = null ) { + if ( $forceNewText === null ) { + // T157210: avoid using wfMessage() to avoid staleness due to cache layering + $title = Title::makeTitle( NS_MEDIAWIKI, 'Gadgets-definition' ); + $rev = Revision::newFromTitle( $title ); + if ( !$rev || !$rev->getContent() || $rev->getContent()->isEmpty() ) { + return false; // don't cache + } + + $g = $rev->getContent()->getNativeData(); + } else { + $g = $forceNewText; + } + + $gadgets = $this->listFromDefinition( $g ); + if ( !count( $gadgets ) ) { + return false; // don't cache; Bug 37228 + } + + $source = $forceNewText !== null ? 'input text' : 'MediaWiki:Gadgets-definition'; + wfDebug( __METHOD__ . ": $source parsed, cache entry should be updated\n" ); + + return $gadgets; + } + + /** + * Generates a structured list of Gadget objects from a definition + * + * @param string $definition + * @return Gadget[] List of Gadget objects indexed by the gadget's name. + */ + private function listFromDefinition( $definition ) { + $definition = preg_replace( '/<!--.*?-->/s', '', $definition ); + $lines = preg_split( '/(\r\n|\r|\n)+/', $definition ); + + $gadgets = []; + $section = ''; + + foreach ( $lines as $line ) { + $m = []; + if ( preg_match( '/^==+ *([^*:\s|]+?)\s*==+\s*$/', $line, $m ) ) { + $section = $m[1]; + } else { + $gadget = $this->newFromDefinition( $line, $section ); + if ( $gadget ) { + $gadgets[$gadget->getName()] = $gadget; + } + } + } + + return $gadgets; + } + + /** + * Creates an instance of this class from definition in MediaWiki:Gadgets-definition + * @param string $definition Gadget definition + * @param string $category + * @return Gadget|bool Instance of Gadget class or false if $definition is invalid + */ + public function newFromDefinition( $definition, $category ) { + $m = []; + if ( !preg_match( + '/^\*+ *([a-zA-Z](?:[-_:.\w\d ]*[a-zA-Z0-9])?)(\s*\[.*?\])?\s*((\|[^|]*)+)\s*$/', + $definition, + $m + ) ) { + return false; + } + // NOTE: the gadget name is used as part of the name of a form field, + // and must follow the rules defined in https://www.w3.org/TR/html4/types.html#type-cdata + // Also, title-normalization applies. + $info = [ 'category' => $category ]; + $info['name'] = trim( str_replace( ' ', '_', $m[1] ) ); + // If the name is too long, then RL will throw an MWException when + // we try to register the module + if ( !Gadget::isValidGadgetID( $info['name'] ) ) { + return false; + } + $info['definition'] = $definition; + $options = trim( $m[2], ' []' ); + + foreach ( preg_split( '/\s*\|\s*/', $options, -1, PREG_SPLIT_NO_EMPTY ) as $option ) { + $arr = preg_split( '/\s*=\s*/', $option, 2 ); + $option = $arr[0]; + if ( isset( $arr[1] ) ) { + $params = explode( ',', $arr[1] ); + $params = array_map( 'trim', $params ); + } else { + $params = []; + } + + switch ( $option ) { + case 'ResourceLoader': + $info['resourceLoaded'] = true; + break; + case 'dependencies': + $info['dependencies'] = $params; + break; + case 'peers': + $info['peers'] = $params; + break; + case 'rights': + $info['requiredRights'] = $params; + break; + case 'hidden': + $info['hidden'] = true; + break; + case 'skins': + $info['requiredSkins'] = $params; + break; + case 'default': + $info['onByDefault'] = true; + break; + case 'targets': + $info['targets'] = $params; + break; + case 'type': + // Single value, not a list + $info['type'] = isset( $params[0] ) ? $params[0] : ''; + break; + } + } + + foreach ( preg_split( '/\s*\|\s*/', $m[3], -1, PREG_SPLIT_NO_EMPTY ) as $page ) { + $page = "MediaWiki:Gadget-$page"; + + if ( preg_match( '/\.js/', $page ) ) { + $info['scripts'][] = $page; + } elseif ( preg_match( '/\.css/', $page ) ) { + $info['styles'][] = $page; + } + } + + return new Gadget( $info ); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/SpecialGadgetUsage.php b/www/wiki/extensions/Gadgets/includes/SpecialGadgetUsage.php new file mode 100644 index 00000000..0601d654 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/SpecialGadgetUsage.php @@ -0,0 +1,252 @@ +<?php +/** + * Implements Special:GadgetUsage + * + * Copyright © 2015 Niharika Kohli + * + * 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 + * + * @file + * @ingroup SpecialPage + * @author Niharika Kohli <niharika@wikimedia.org> + */ + +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\IResultWrapper; + +/** + * Special:GadgetUsage - Lists all the gadgets on the wiki along with number of users. + * @ingroup SpecialPage + */ +class SpecialGadgetUsage extends QueryPage { + function __construct( $name = 'GadgetUsage' ) { + parent::__construct( $name ); + $this->limit = 1000; // Show all gadgets + $this->shownavigation = false; + $this->activeUsers = $this->getConfig()->get( 'SpecialGadgetUsageActiveUsers' ); + } + + /** + * Flag for holding the value of config variable SpecialGadgetUsageActiveUsers + * + * @var bool $activeUsers + */ + public $activeUsers; + + public function isExpensive() { + return true; + } + + /** + * Define the database query that is used to generate the stats table. + * This uses 1 of 2 possible queries, depending on $wgSpecialGadgetUsageActiveUsers. + * + * The simple query is essentially: + * SELECT up_property, SUM(up_value) + * FROM user_properties + * WHERE up_property LIKE 'gadget-%' + * GROUP BY up_property; + * + * The more expensive query is: + * SELECT up_property, SUM(up_value), count(qcc_title) + * FROM user_properties + * LEFT JOIN user ON up_user = user_id + * LEFT JOIN querycachetwo ON user_name = qcc_title AND qcc_type = 'activeusers' AND up_value = 1 + * WHERE up_property LIKE 'gadget-%' + * GROUP BY up_property; + * @return array + */ + public function getQueryInfo() { + $dbr = wfGetDB( DB_REPLICA ); + if ( !$this->activeUsers ) { + return [ + 'tables' => [ 'user_properties' ], + 'fields' => [ + 'title' => 'up_property', + 'value' => 'SUM( up_value )', + 'namespace' => NS_GADGET + ], + 'conds' => [ + 'up_property' . $dbr->buildLike( 'gadget-', $dbr->anyString() ) + ], + 'options' => [ + 'GROUP BY' => [ 'up_property' ] + ] + ]; + } else { + return [ + 'tables' => [ 'user_properties', 'user', 'querycachetwo' ], + 'fields' => [ + 'title' => 'up_property', + 'value' => 'SUM( up_value )', + // Need to pick fields existing in the querycache table so that the results are cachable + 'namespace' => 'COUNT( qcc_title )' + ], + 'conds' => [ + 'up_property' . $dbr->buildLike( 'gadget-', $dbr->anyString() ) + ], + 'options' => [ + 'GROUP BY' => [ 'up_property' ] + ], + 'join_conds' => [ + 'user' => [ + 'LEFT JOIN', [ + 'up_user = user_id' + ] + ], + 'querycachetwo' => [ + 'LEFT JOIN', [ + 'user_name = qcc_title', + 'qcc_type = "activeusers"', + 'up_value = 1' + ] + ] + ] + ]; + } + } + + public function getOrderFields() { + return [ 'value' ]; + } + + /** + * Output the start of the table + * Including opening <table>, and first <tr> with column headers. + */ + protected function outputTableStart() { + $html = Html::openElement( 'table', [ 'class' => [ 'sortable', 'wikitable' ] ] ); + $html .= Html::openElement( 'tr', [] ); + $headers = [ 'gadgetusage-gadget', 'gadgetusage-usercount' ]; + if ( $this->activeUsers ) { + $headers[] = 'gadgetusage-activeusers'; + } + foreach ( $headers as $h ) { + if ( $h == 'gadgetusage-gadget' ) { + $html .= Html::element( 'th', [], $this->msg( $h )->text() ); + } else { + $html .= Html::element( 'th', [ 'data-sort-type' => 'number' ], + $this->msg( $h )->text() ); + } + } + $html .= Html::closeElement( 'tr' ); + $this->getOutput()->addHTML( $html ); + } + + /** + * @param Skin $skin + * @param object $result Result row + * @return string|bool String of HTML + */ + public function formatResult( $skin, $result ) { + $gadgetTitle = substr( $result->title, 7 ); + $gadgetUserCount = $this->getLanguage()->formatNum( $result->value ); + if ( $gadgetTitle ) { + $html = Html::openElement( 'tr', [] ); + $html .= Html::element( 'td', [], $gadgetTitle ); + $html .= Html::element( 'td', [], $gadgetUserCount ); + if ( $this->activeUsers == true ) { + $activeUserCount = $this->getLanguage()->formatNum( $result->namespace ); + $html .= Html::element( 'td', [], $activeUserCount ); + } + $html .= Html::closeElement( 'tr' ); + return $html; + } + return false; + } + + /** + * Get a list of default gadgets + * @param GadgetRepo $gadgetRepo + * @param array $gadgetIds list of gagdet ids registered in the wiki + * @return array + */ + protected function getDefaultGadgets( $gadgetRepo, $gadgetIds ) { + $gadgetsList = []; + foreach ( $gadgetIds as $g ) { + $gadget = $gadgetRepo->getGadget( $g ); + if ( $gadget->isOnByDefault() ) { + $gadgetsList[] = $gadget->getName(); + } + } + asort( $gadgetsList, SORT_STRING | SORT_FLAG_CASE ); + return $gadgetsList; + } + + /** + * Format and output report results using the given information plus + * OutputPage + * + * @param OutputPage $out OutputPage to print to + * @param Skin $skin User skin to use + * @param IDatabase $dbr Database (read) connection to use + * @param IResultWrapper $res Result pointer + * @param int $num Number of available result rows + * @param int $offset Paging offset + */ + protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { + $gadgetRepo = GadgetRepo::singleton(); + $gadgetIds = $gadgetRepo->getGadgetIds(); + $defaultGadgets = $this->getDefaultGadgets( $gadgetRepo, $gadgetIds ); + if ( $this->activeUsers ) { + $out->addHtml( + $this->msg( 'gadgetusage-intro' ) + ->numParams( $this->getConfig()->get( 'ActiveUserDays' ) )->parseAsBlock() + ); + } else { + $out->addHtml( + $this->msg( 'gadgetusage-intro-noactive' )->parseAsBlock() + ); + } + if ( $num > 0 ) { + $this->outputTableStart(); + // Append default gadgets to the table with 'default' in the total and active user fields + foreach ( $defaultGadgets as $default ) { + $html = Html::openElement( 'tr', [] ); + $html .= Html::element( 'td', [], $default ); + $html .= Html::element( 'td', [], $this->msg( 'gadgetusage-default' )->text() ); + if ( $this->activeUsers ) { + $html .= Html::element( 'td', [], $this->msg( 'gadgetusage-default' )->text() ); + } + $html .= Html::closeElement( 'tr' ); + $out->addHTML( $html ); + } + foreach ( $res as $row ) { + // Remove the 'gadget-' part of the result string and compare if it's present + // in $defaultGadgets, if not we format it and add it to the output + if ( !in_array( substr( $row->title, 7 ), $defaultGadgets ) ) { + // Only pick gadgets which are in the list $gadgetIds to make sure they exist + if ( in_array( substr( $row->title, 7 ), $gadgetIds ) ) { + $line = $this->formatResult( $skin, $row ); + if ( $line ) { + $out->addHTML( $line ); + } + } + } + } + // Close table element + $out->addHtml( Html::closeElement( 'table' ) ); + } else { + $out->addHtml( + $this->msg( 'gadgetusage-noresults' )->parseAsBlock() + ); + } + } + + protected function getGroupName() { + return 'wiki'; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/SpecialGadgets.php b/www/wiki/extensions/Gadgets/includes/SpecialGadgets.php new file mode 100644 index 00000000..1d374cbf --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/SpecialGadgets.php @@ -0,0 +1,232 @@ +<?php +/** + * Special:Gadgets, provides a preview of MediaWiki:Gadgets. + * + * @file + * @ingroup SpecialPage + * @author Daniel Kinzler, brightbyte.de + * @copyright © 2007 Daniel Kinzler + * @license GPL-2.0-or-later + */ + +class SpecialGadgets extends SpecialPage { + public function __construct() { + parent::__construct( 'Gadgets', '', true ); + } + + /** + * Main execution function + * @param string $par Parameters passed to the page + */ + public function execute( $par ) { + $parts = explode( '/', $par ); + + if ( count( $parts ) == 2 && $parts[0] == 'export' ) { + $this->showExportForm( $parts[1] ); + } else { + $this->showMainForm(); + } + } + + private function makeAnchor( $gadgetName ) { + return 'gadget-' . Sanitizer::escapeId( $gadgetName, [ 'noninitial' ] ); + } + + /** + * Displays form showing the list of installed gadgets + */ + public function showMainForm() { + global $wgContLang; + + $output = $this->getOutput(); + $this->setHeaders(); + $output->setPageTitle( $this->msg( 'gadgets-title' ) ); + $output->addWikiMsg( 'gadgets-pagetext' ); + + $gadgets = GadgetRepo::singleton()->getStructuredList(); + if ( !$gadgets ) { + return; + } + + $output->disallowUserJs(); + $lang = $this->getLanguage(); + $langSuffix = ""; + if ( $lang->getCode() != $wgContLang->getCode() ) { + $langSuffix = "/" . $lang->getCode(); + } + + $listOpen = false; + + $editInterfaceMessage = $this->getUser()->isAllowed( 'editinterface' ) + ? 'edit' + : 'viewsource'; + + $linkRenderer = $this->getLinkRenderer(); + foreach ( $gadgets as $section => $entries ) { + if ( $section !== false && $section !== '' ) { + $t = Title::makeTitleSafe( NS_MEDIAWIKI, "Gadget-section-$section$langSuffix" ); + $lnkTarget = $t + ? $linkRenderer->makeLink( $t, $this->msg( $editInterfaceMessage )->text(), + [], [ 'action' => 'edit' ] ) + : htmlspecialchars( $section ); + $lnk = "    [$lnkTarget]"; + + $ttext = $this->msg( "gadget-section-$section" )->parse(); + + if ( $listOpen ) { + $output->addHTML( Xml::closeElement( 'ul' ) . "\n" ); + $listOpen = false; + } + + $output->addHTML( Html::rawElement( 'h2', [], $ttext . $lnk ) . "\n" ); + } + + /** + * @var $gadget Gadget + */ + foreach ( $entries as $gadget ) { + $name = $gadget->getName(); + $t = Title::makeTitleSafe( NS_MEDIAWIKI, "Gadget-{$name}$langSuffix" ); + if ( !$t ) { + continue; + } + + $links = []; + $links[] = $linkRenderer->makeLink( + $t, + $this->msg( $editInterfaceMessage )->text(), + [], + [ 'action' => 'edit' ] + ); + $links[] = $linkRenderer->makeLink( + $this->getPageTitle( "export/{$name}" ), + $this->msg( 'gadgets-export' )->text() + ); + + $ttext = $this->msg( "gadget-{$name}" )->parse(); + + if ( !$listOpen ) { + $listOpen = true; + $output->addHTML( Xml::openElement( 'ul' ) ); + } + + $actions = '  ' . + $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $links ) )->escaped(); + $output->addHTML( + Xml::openElement( 'li', [ 'id' => $this->makeAnchor( $name ) ] ) . + $ttext . $actions . "<br />" . + $this->msg( 'gadgets-uses' )->escaped() . + $this->msg( 'colon-separator' )->escaped() + ); + + $lnk = []; + foreach ( $gadget->getPeers() as $peer ) { + $lnk[] = Html::element( + 'a', + [ 'href' => '#' . $this->makeAnchor( $peer ) ], + $peer + ); + } + foreach ( $gadget->getScriptsAndStyles() as $codePage ) { + $t = Title::newFromText( $codePage ); + + if ( !$t ) { + continue; + } + + $lnk[] = $linkRenderer->makeLink( $t, $t->getText() ); + } + $output->addHTML( $lang->commaList( $lnk ) ); + if ( $gadget->getLegacyScripts() ) { + $output->addHTML( '<br />' . Html::rawElement( + 'span', + [ 'class' => 'mw-gadget-legacy errorbox' ], + $this->msg( 'gadgets-legacy' )->parse() + ) ); + } + + $rights = []; + foreach ( $gadget->getRequiredRights() as $right ) { + $rights[] = '* ' . $this->msg( "right-$right" )->plain(); + } + if ( count( $rights ) ) { + $output->addHTML( '<br />' . + $this->msg( 'gadgets-required-rights', implode( "\n", $rights ), count( $rights ) )->parse() + ); + } + + $requiredSkins = $gadget->getRequiredSkins(); + // $requiredSkins can be an array or true (if all skins are supported) + if ( is_array( $requiredSkins ) ) { + $skins = []; + $validskins = Skin::getSkinNames(); + foreach ( $requiredSkins as $skinid ) { + if ( isset( $validskins[$skinid] ) ) { + $skins[] = $this->msg( "skinname-$skinid" )->plain(); + } else { + $skins[] = $skinid; + } + } + if ( count( $skins ) ) { + $output->addHTML( + '<br />' . + $this->msg( 'gadgets-required-skins', $lang->commaList( $skins ) ) + ->numParams( count( $skins ) )->parse() + ); + } + } + + if ( $gadget->isOnByDefault() ) { + $output->addHTML( '<br />' . $this->msg( 'gadgets-default' )->parse() ); + } + + $output->addHTML( Xml::closeElement( 'li' ) . "\n" ); + } + } + + if ( $listOpen ) { + $output->addHTML( Xml::closeElement( 'ul' ) . "\n" ); + } + } + + /** + * Exports a gadget with its dependencies in a serialized form + * @param string $gadget Name of gadget to export + */ + public function showExportForm( $gadget ) { + global $wgScript; + + $output = $this->getOutput(); + try { + $g = GadgetRepo::singleton()->getGadget( $gadget ); + } catch ( InvalidArgumentException $e ) { + $output->showErrorPage( 'error', 'gadgets-not-found', [ $gadget ] ); + return; + } + + $this->setHeaders(); + $output->setPageTitle( $this->msg( 'gadgets-export-title' ) ); + $output->addWikiMsg( 'gadgets-export-text', $gadget, $g->getDefinition() ); + + $exportList = "MediaWiki:gadget-$gadget\n"; + foreach ( $g->getScriptsAndStyles() as $page ) { + $exportList .= "$page\n"; + } + + $htmlForm = HTMLForm::factory( 'ooui', [], $this->getContext() ); + $htmlForm + ->addHiddenField( 'title', SpecialPage::getTitleFor( 'Export' )->getPrefixedDBKey() ) + ->addHiddenField( 'pages', $exportList ) + ->addHiddenField( 'wpDownload', '1' ) + ->addHiddenField( 'templates', '1' ) + ->setAction( $wgScript ) + ->setMethod( 'get' ) + ->setSubmitText( $this->msg( 'gadgets-export-download' )->text() ) + ->prepareForm() + ->displayForm( false ); + } + + protected function getGroupName() { + return 'wiki'; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgetCategories.php b/www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgetCategories.php new file mode 100644 index 00000000..489f6650 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgetCategories.php @@ -0,0 +1,110 @@ +<?php +/** + * Created on 16 April 2011 + * API for Gadgets extension + * + * 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 + */ + +class ApiQueryGadgetCategories extends ApiQueryBase { + /** + * @var array + */ + private $props; + + /** + * @var array|bool + */ + private $neededNames; + + public function __construct( ApiQuery $queryModule, $moduleName ) { + parent::__construct( $queryModule, $moduleName, 'gc' ); + } + + public function execute() { + $params = $this->extractRequestParams(); + $this->props = array_flip( $params['prop'] ); + $this->neededNames = isset( $params['names'] ) + ? array_flip( $params['names'] ) + : false; + + $this->getMain()->setCacheMode( 'public' ); + + $this->getList(); + } + + private function getList() { + $data = []; + $result = $this->getResult(); + $gadgets = GadgetRepo::singleton()->getStructuredList(); + + if ( $gadgets ) { + foreach ( $gadgets as $category => $list ) { + if ( !$this->neededNames || isset( $this->neededNames[$category] ) ) { + $row = []; + if ( isset( $this->props['name'] ) ) { + $row['name'] = $category; + } + + if ( $category !== "" ) { + if ( isset( $this->props['title'] ) ) { + $row['desc'] = $this->msg( "gadget-section-$category" )->parse(); + } + } + + if ( isset( $this->props['members'] ) ) { + $row['members'] = count( $list ); + } + + $data[] = $row; + } + } + } + $result->setIndexedTagName( $data, 'category' ); + $result->addValue( 'query', $this->getModuleName(), $data ); + } + + public function getAllowedParams() { + return [ + 'prop' => [ + ApiBase::PARAM_DFLT => 'name', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => [ + 'name', + 'title', + 'members', + ], + ], + 'names' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_ISMULTI => true, + ], + ]; + } + + /** + * @see ApiBase::getExamplesMessages() + * @return array + */ + protected function getExamplesMessages() { + return [ + 'action=query&list=gadgetcategories' + => 'apihelp-query+gadgetcategories-example-1', + 'action=query&list=gadgetcategories&gcnames=foo|bar&gcprop=name|title|members' + => 'apihelp-query+gadgetcategories-example-2', + ]; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgets.php b/www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgets.php new file mode 100644 index 00000000..618858da --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgets.php @@ -0,0 +1,231 @@ +<?php +/** + * Created on 15 April 2011 + * API for Gadgets extension + * + * 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 + */ + +class ApiQueryGadgets extends ApiQueryBase { + /** + * @var array + */ + private $props; + + /** + * @var array|bool + */ + private $categories; + + /** + * @var array|bool + */ + private $neededIds; + + /** + * @var bool + */ + private $listAllowed; + + /** + * @var bool + */ + private $listEnabled; + + public function __construct( ApiQuery $queryModule, $moduleName ) { + parent::__construct( $queryModule, $moduleName, 'ga' ); + } + + public function execute() { + $params = $this->extractRequestParams(); + $this->props = array_flip( $params['prop'] ); + $this->categories = isset( $params['categories'] ) + ? array_flip( $params['categories'] ) + : false; + $this->neededIds = isset( $params['ids'] ) + ? array_flip( $params['ids'] ) + : false; + $this->listAllowed = isset( $params['allowedonly'] ) && $params['allowedonly']; + $this->listEnabled = isset( $params['enabledonly'] ) && $params['enabledonly']; + + $this->getMain()->setCacheMode( $this->listAllowed || $this->listEnabled + ? 'anon-public-user-private' : 'public' ); + + $this->applyList( $this->getList() ); + } + + /** + * @return array + */ + private function getList() { + $gadgets = GadgetRepo::singleton()->getStructuredList(); + + if ( $gadgets === false ) { + return []; + } + + $result = []; + foreach ( $gadgets as $category => $list ) { + if ( $this->categories && !isset( $this->categories[$category] ) ) { + continue; + } + + foreach ( $list as $g ) { + if ( $this->isNeeded( $g ) ) { + $result[] = $g; + } + } + } + return $result; + } + + /** + * @param array $gadgets + */ + private function applyList( $gadgets ) { + $data = []; + $result = $this->getResult(); + + /** + * @var $g Gadget + */ + foreach ( $gadgets as $g ) { + $row = []; + if ( isset( $this->props['id'] ) ) { + $row['id'] = $g->getName(); + } + + if ( isset( $this->props['metadata'] ) ) { + $row['metadata'] = $this->fakeMetadata( $g ); + $this->setIndexedTagNameForMetadata( $row['metadata'] ); + } + + if ( isset( $this->props['desc'] ) ) { + $row['desc'] = $g->getDescription(); + } + + $data[] = $row; + } + + $result->setIndexedTagName( $data, 'gadget' ); + $result->addValue( 'query', $this->getModuleName(), $data ); + } + + /** + * @param Gadget $gadget + * + * @return bool + */ + private function isNeeded( Gadget $gadget ) { + $user = $this->getUser(); + + return ( $this->neededIds === false || isset( $this->neededIds[$gadget->getName()] ) ) + && ( !$this->listAllowed || $gadget->isAllowed( $user ) ) + && ( !$this->listEnabled || $gadget->isEnabled( $user ) ); + } + + /** + * @param Gadget $g + * @return array + */ + private function fakeMetadata( Gadget $g ) { + return [ + 'settings' => [ + 'rights' => $g->getRequiredRights(), + 'skins' => $g->getRequiredSkins(), + 'default' => $g->isOnByDefault(), + 'hidden' => $g->isHidden(), + 'shared' => false, + 'category' => $g->getCategory(), + 'legacyscripts' => (bool)$g->getLegacyScripts(), + ], + 'module' => [ + 'scripts' => $g->getScripts(), + 'styles' => $g->getStyles(), + 'dependencies' => $g->getDependencies(), + 'peers' => $g->getPeers(), + 'messages' => $g->getMessages(), + ] + ]; + } + + private function setIndexedTagNameForMetadata( &$metadata ) { + static $tagNames = [ + 'rights' => 'right', + 'skins' => 'skin', + 'scripts' => 'script', + 'styles' => 'style', + 'dependencies' => 'dependency', + 'peers' => 'peer', + 'messages' => 'message', + ]; + + $result = $this->getResult(); + foreach ( $metadata as $data ) { + foreach ( $data as $key => $value ) { + if ( is_array( $value ) ) { + $tag = isset( $tagNames[$key] ) ? $tagNames[$key] : $key; + $result->setIndexedTagName( $value, $tag ); + } + } + } + } + + public function getAllowedParams() { + return [ + 'prop' => [ + ApiBase::PARAM_DFLT => 'id|metadata', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => [ + 'id', + 'metadata', + 'desc', + ], + ], + 'categories' => [ + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'string', + ], + 'ids' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_ISMULTI => true, + ], + 'allowedonly' => false, + 'enabledonly' => false, + ]; + } + + /** + * @see ApiBase::getExamplesMessages() + * @return array + */ + protected function getExamplesMessages() { + $params = $this->getAllowedParams(); + $allProps = implode( '|', $params['prop'][ApiBase::PARAM_TYPE] ); + return [ + 'action=query&list=gadgets&gaprop=id|desc' + => 'apihelp-query+gadgets-example-1', + "action=query&list=gadgets&gaprop=$allProps" + => 'apihelp-query+gadgets-example-2', + 'action=query&list=gadgets&gacategories=foo' + => 'apihelp-query+gadgets-example-3', + 'action=query&list=gadgets&gaids=foo|bar&gaprop=id|desc|metadata' + => 'apihelp-query+gadgets-example-4', + 'action=query&list=gadgets&gaenabledonly' + => 'apihelp-query+gadgets-example-5', + ]; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContent.php b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContent.php new file mode 100644 index 00000000..0a7b2d9d --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContent.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright 2014 + * + * 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 + * + * @file + */ + +class GadgetDefinitionContent extends JsonContent { + + public function __construct( $text ) { + parent::__construct( $text, 'GadgetDefinition' ); + } + + public function isValid() { + // parent::isValid() is called in validate() + return $this->validate()->isOK(); + } + + /** + * Pretty-print JSON. + * + * If called before validation, it may return JSON "null". + * + * @return string + */ + public function beautifyJSON() { + // @todo we should normalize entries in module.scripts and module.styles + return FormatJson::encode( $this->getAssocArray(), "\t", FormatJson::UTF8_OK ); + } + + /** + * Register some links + * + * @param Title $title + * @param int $revId + * @param ParserOptions $options + * @param bool $generateHtml + * @param ParserOutput &$output + */ + protected function fillParserOutput( Title $title, $revId, + ParserOptions $options, $generateHtml, ParserOutput &$output + ) { + parent::fillParserOutput( $title, $revId, $options, $generateHtml, $output ); + $assoc = $this->getAssocArray(); + foreach ( [ 'scripts', 'styles' ] as $type ) { + foreach ( $assoc['module'][$type] as $page ) { + $title = Title::makeTitleSafe( NS_GADGET, $page ); + if ( $title ) { + $output->addLink( $title ); + } + } + } + } + + /** + * @return Status + */ + public function validate() { + if ( !parent::isValid() ) { + return $this->getData(); + } + + $validator = new GadgetDefinitionValidator(); + return $validator->validate( $this->getAssocArray() ); + } + + /** + * Get the JSON content as an associative array with + * all fields filled out, populating defaults as necessary. + * + * @return array + * @suppress PhanUndeclaredMethod + */ + public function getAssocArray() { + $info = wfObjectToArray( $this->getData()->getValue() ); + /** @var GadgetDefinitionContentHandler $handler */ + $handler = $this->getContentHandler(); + $info = wfArrayPlus2d( $info, $handler->getDefaultMetadata() ); + + return $info; + } + + /** + * @param WikiPage $page + * @param ParserOutput $parserOutput + * @return DeferrableUpdate[] + */ + public function getDeletionUpdates( WikiPage $page, ParserOutput $parserOutput = null ) { + return array_merge( + parent::getDeletionUpdates( $page, $parserOutput ), + [ new GadgetDefinitionDeletionUpdate( $page->getTitle() ) ] + ); + } + + /** + * @param Title $title + * @param Content $old + * @param bool $recursive + * @param ParserOutput $parserOutput + * @return DataUpdate[] + */ + public function getSecondaryDataUpdates( Title $title, Content $old = null, + $recursive = true, ParserOutput $parserOutput = null + ) { + return array_merge( + parent::getSecondaryDataUpdates( $title, $old, $recursive, $parserOutput ), + [ new GadgetDefinitionSecondaryDataUpdate( $title ) ] + ); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContentHandler.php b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContentHandler.php new file mode 100644 index 00000000..59276219 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContentHandler.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright 2014 + * + * 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 + * + * @file + */ + +class GadgetDefinitionContentHandler extends JsonContentHandler { + public function __construct() { + parent::__construct( 'GadgetDefinition' ); + } + + /** + * @param Title $title + * @return bool + */ + public function canBeUsedOn( Title $title ) { + return $title->inNamespace( NS_GADGET_DEFINITION ); + } + + protected function getContentClass() { + return 'GadgetDefinitionContent'; + } + + public function makeEmptyContent() { + $class = $this->getContentClass(); + return new $class( FormatJson::encode( $this->getDefaultMetadata(), "\t" ) ); + } + + public function getDefaultMetadata() { + return [ + 'settings' => [ + 'rights' => [], + 'default' => false, + 'hidden' => false, + 'skins' => [], + 'category' => '' + ], + 'module' => [ + 'scripts' => [], + 'styles' => [], + 'peers' => [], + 'dependencies' => [], + 'messages' => [], + 'type' => '', + ], + ]; + } +} diff --git a/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionDeletionUpdate.php b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionDeletionUpdate.php new file mode 100644 index 00000000..97c8d8e8 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionDeletionUpdate.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright 2014 + * + * 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 + * + * @file + */ + +use MediaWiki\Linker\LinkTarget; + +/** + * DataUpdate to run whenever a page in the Gadget definition + * is deleted. + */ +class GadgetDefinitionDeletionUpdate extends DataUpdate { + /** + * Page that was deleted + * @var LinkTarget + */ + private $target; + + public function __construct( LinkTarget $target ) { + $this->target = $target; + } + + public function doUpdate() { + GadgetRepo::singleton()->handlePageDeletion( $this->target ); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionSecondaryDataUpdate.php b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionSecondaryDataUpdate.php new file mode 100644 index 00000000..8a663933 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionSecondaryDataUpdate.php @@ -0,0 +1,40 @@ +<?php + +/** + * Copyright 2014 + * + * 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 + * + * @file + */ + +use MediaWiki\Linker\LinkTarget; + +class GadgetDefinitionSecondaryDataUpdate extends DataUpdate { + + /** + * @var LinkTarget + */ + private $target; + + public function __construct( LinkTarget $target ) { + $this->target = $target; + } + + public function doUpdate() { + GadgetRepo::singleton()->handlePageUpdate( $this->target ); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionValidator.php b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionValidator.php new file mode 100644 index 00000000..2be75783 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionValidator.php @@ -0,0 +1,91 @@ +<?php + +/** + * Class responsible for validating Gadget definition contents + * + * @todo maybe this should use a formal JSON schema validator or something + */ +class GadgetDefinitionValidator { + /** + * Validation metadata. + * 'foo.bar.baz' => [ 'type check callback', + * 'type name' [, 'member type check callback', 'member type name'] ] + */ + protected static $propertyValidation = [ + 'settings' => [ 'is_array', 'array' ], + 'settings.rights' => [ 'is_array', 'array' , 'is_string', 'string' ], + 'settings.default' => [ 'is_bool', 'boolean' ], + 'settings.hidden' => [ 'is_bool', 'boolean' ], + 'settings.skins' => [ [ __CLASS__, 'isArrayOrTrue' ], 'array or true', 'is_string', 'string' ], + 'settings.category' => [ 'is_string', 'string' ], + 'module' => [ 'is_array', 'array' ], + 'module.scripts' => [ 'is_array', 'array', 'is_string', 'string' ], + 'module.styles' => [ 'is_array', 'array', 'is_string', 'string' ], + 'module.dependencies' => [ 'is_array', 'array', 'is_string', 'string' ], + 'module.peers' => [ 'is_array', 'array', 'is_string', 'string' ], + 'module.messages' => [ 'is_array', 'array', 'is_string', 'string' ], + 'module.type' => [ 'is_string', 'string' ], + ]; + + /** + * @param mixed $value + * @return bool + */ + public static function isArrayOrTrue( $value ) { + return is_array( $value ) || $value === true; + } + + /** + * Check the validity of the given properties array + * @param array $properties Return value of FormatJson::decode( $blob, true ) + * @param bool $tolerateMissing If true, don't complain about missing keys + * @return Status object with error message if applicable + */ + public function validate( array $properties, $tolerateMissing = false ) { + foreach ( self::$propertyValidation as $property => $validation ) { + $path = explode( '.', $property ); + $val = $properties; + + // Walk down and verify that the path from the root to this property exists + foreach ( $path as $p ) { + if ( !array_key_exists( $p, $val ) ) { + if ( $tolerateMissing ) { + // Skip validation of this property altogether + continue 2; + } else { + return Status::newFatal( 'gadgets-validate-notset', $property ); + } + } + $val = $val[$p]; + } + + // Do the actual validation of this property + $func = $validation[0]; + if ( !call_user_func( $func, $val ) ) { + return Status::newFatal( + 'gadgets-validate-wrongtype', + $property, + $validation[1], + gettype( $val ) + ); + } + + if ( isset( $validation[2] ) && is_array( $val ) ) { + // Descend into the array and check the type of each element + $func = $validation[2]; + foreach ( $val as $i => $v ) { + if ( !call_user_func( $func, $v ) ) { + return Status::newFatal( + 'gadgets-validate-wrongtype', + "{$property}[{$i}]", + $validation[3], + gettype( $v ) + ); + } + } + } + } + + return Status::newGood(); + } +} diff --git a/www/wiki/extensions/Gadgets/includes/content/schema.json b/www/wiki/extensions/Gadgets/includes/content/schema.json new file mode 100644 index 00000000..e326be46 --- /dev/null +++ b/www/wiki/extensions/Gadgets/includes/content/schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/schema#", + "description": "Gadget definition schema", + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "rights": { + "description": "The rights required to be able to enable/load this gadget", + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "description": "Whether this gadget is enabled by default", + "type": "boolean", + "default": false + }, + "hidden": { + "description": "Whether this gadget is hidden from preferences", + "type": "boolean", + "default": false + }, + "skins": { + "description": "Skins supported by this gadget; empty or true if all skins are supported", + "type": [ "array", "boolean" ], + "items": { + "type": "string" + } + }, + "category": { + "description": "Key of the category this gadget belongs to", + "type": "string", + "default": "" + } + } + }, + "module": { + "type": "object", + "additionalProperties": false, + "properties": { + "scripts": { + "type": "array", + "description": "List of JavaScript pages included in this gadget" + }, + "styles": { + "type": "array", + "description": "List of CSS pages included in this gadget" + }, + "dependencies": { + "type": "array", + "description": "ResourceLoader modules this gadget depends upon" + }, + "messages": { + "type": "array", + "description": "Messages this gadget depends upon" + } + } + } + } +} |