summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Gadgets/includes
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Gadgets/includes
first commit
Diffstat (limited to 'www/wiki/extensions/Gadgets/includes')
-rw-r--r--www/wiki/extensions/Gadgets/includes/Gadget.php315
-rw-r--r--www/wiki/extensions/Gadgets/includes/GadgetDefinitionNamespaceRepo.php167
-rw-r--r--www/wiki/extensions/Gadgets/includes/GadgetHooks.php342
-rw-r--r--www/wiki/extensions/Gadgets/includes/GadgetRepo.php104
-rw-r--r--www/wiki/extensions/Gadgets/includes/GadgetResourceLoaderModule.php93
-rw-r--r--www/wiki/extensions/Gadgets/includes/MediaWikiGadgetsDefinitionRepo.php261
-rw-r--r--www/wiki/extensions/Gadgets/includes/SpecialGadgetUsage.php252
-rw-r--r--www/wiki/extensions/Gadgets/includes/SpecialGadgets.php232
-rw-r--r--www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgetCategories.php110
-rw-r--r--www/wiki/extensions/Gadgets/includes/api/ApiQueryGadgets.php231
-rw-r--r--www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContent.php125
-rw-r--r--www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionContentHandler.php64
-rw-r--r--www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionDeletionUpdate.php43
-rw-r--r--www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionSecondaryDataUpdate.php40
-rw-r--r--www/wiki/extensions/Gadgets/includes/content/GadgetDefinitionValidator.php91
-rw-r--r--www/wiki/extensions/Gadgets/includes/content/schema.json65
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' => '&#160;',
+ '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' => '&#160;',
+ '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 = "&#160; &#160; [$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 = '&#160;&#160;' .
+ $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"
+ }
+ }
+ }
+ }
+}