Kaldari has submitted this change and it was merged.
Change subject: Implement Gadgets definition namespace repo
......................................................................
Implement Gadgets definition namespace repo
Implements:
* Gadget definition content and content handler
* Basic validation for gadget definition content
* GadgetDefinitionNamespace implementation of GadgetRepo
* DataUpdates upon editing/deletion of Gadget definition pages
* EditFilterMerged hook for improved error messages
* 'GadgetsRepoClass' option to switch GadgetRepo implementation used
* Lazy-load the GadgetResourceLoaderModule class so we don't need to
load each individual gadget object unless its needed
Note that Special:Gadgets's export feature intentionally doesn't work
yet, and will be fixed in a follow up patch.
Bug: T106177
Change-Id: Ib11db5fb0f7b46793bfa956cf1367f1dc1059b1c
---
M GadgetHooks.php
M Gadgets_body.php
M README
M SpecialGadgets.php
M extension.json
M i18n/en.json
M i18n/qqq.json
A includes/GadgetDefinitionNamespaceRepo.php
M includes/GadgetRepo.php
M includes/GadgetResourceLoaderModule.php
M includes/MediaWikiGadgetsDefinitionRepo.php
A includes/content/GadgetDefinitionContent.php
A includes/content/GadgetDefinitionContentHandler.php
A includes/content/GadgetDefinitionDeletionUpdate.php
A includes/content/GadgetDefinitionSecondaryDataUpdate.php
A includes/content/GadgetDefinitionValidator.php
A includes/content/schema.json
M tests/GadgetTest.php
18 files changed, 841 insertions(+), 99 deletions(-)
Approvals:
Kaldari: Looks good to me, approved
jenkins-bot: Verified
diff --git a/GadgetHooks.php b/GadgetHooks.php
index 44f158b..2446c67 100644
--- a/GadgetHooks.php
+++ b/GadgetHooks.php
@@ -144,16 +144,12 @@
public static function registerModules( &$resourceLoader ) {
$repo = GadgetRepo::singleton();
$ids = $repo->getGadgetIds();
- if ( !$ids ) {
- return true;
- }
foreach ( $ids as $id ) {
- $g = $repo->getGadget( $id );
- $module = $g->getModule();
- if ( $module ) {
- $resourceLoader->register( $g->getModuleName(),
$module );
- }
+ $resourceLoader->register( Gadget::getModuleName( $id
), array(
+ 'class' => 'GadgetResourceLoaderModule',
+ 'id' => $id,
+ ) );
}
return true;
@@ -180,11 +176,15 @@
*/
$user = $out->getUser();
foreach ( $ids as $id ) {
- $gadget = $repo->getGadget( $id );
+ try {
+ $gadget = $repo->getGadget( $id );
+ } catch ( InvalidArgumentException $e ) {
+ continue;
+ }
if ( $gadget->isEnabled( $user ) && $gadget->isAllowed(
$user ) ) {
if ( $gadget->hasModule() ) {
- $out->addModuleStyles(
$gadget->getModuleName() );
- $out->addModules(
$gadget->getModuleName() );
+ $out->addModuleStyles(
Gadget::getModuleName( $gadget->getName() ) );
+ $out->addModules(
Gadget::getModuleName( $gadget->getName() ) );
}
if ( $gadget->getLegacyScripts() ) {
@@ -213,6 +213,95 @@
);
}
+
+ /**
+ * Valid gadget definition page after content is modified
+ *
+ * @param IContextSource $context
+ * @param Content $content
+ * @param Status $status
+ * @param string $summary
+ * @throws Exception
+ * @return bool
+ */
+ 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 ) ) {
+ $repo = GadgetRepo::singleton();
+ if ( $repo instanceof GadgetDefinitionNamespaceRepo ) {
+ $repo->purgeGadgetIdsList();
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+
/**
* UnitTestsList hook handler
* @param array $files
diff --git a/Gadgets_body.php b/Gadgets_body.php
index da224da..9e75abd 100644
--- a/Gadgets_body.php
+++ b/Gadgets_body.php
@@ -64,13 +64,54 @@
}
/**
+ * 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 = array(
+ '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'],
+ 'messages' => $data['module']['messages'],
+ 'position' => $data['module']['position'],
+ );
+
+ 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( array( '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(
"ext.gadget.$id" );
+ return strlen( $id ) > 0 && ResourceLoader::isValidModuleName(
Gadget::getModuleName( $id ) );
}
@@ -103,10 +144,11 @@
}
/**
- * @return String: Name of ResourceLoader module for this gadget
+ * @param string $id Name of gadget
+ * @return string Name of ResourceLoader module for the gadget
*/
- public function getModuleName() {
- return "ext.gadget.{$this->name}";
+ public static function getModuleName( $id ) {
+ return "ext.gadget.{$id}";
}
/**
@@ -127,7 +169,7 @@
*/
public function isAllowed( $user ) {
return count( array_intersect( $this->requiredRights,
$user->getRights() ) ) == count( $this->requiredRights )
- && ( !count( $this->requiredSkins ) || in_array(
$user->getOption( 'skin' ), $this->requiredSkins ) );
+ && ( $this->requiredSkins === true || !count(
$this->requiredSkins ) || in_array( $user->getOption( 'skin' ),
$this->requiredSkins ) );
}
/**
@@ -168,14 +210,14 @@
}
/**
- * @return Array: Array of pages with JS not prefixed with namespace
+ * @return Array: Array of pages with JS (including namespace)
*/
public function getScripts() {
return $this->scripts;
}
/**
- * @return Array: Array of pages with CSS not prefixed with namespace
+ * @return Array: Array of pages with CSS (including namespace)
*/
public function getStyles() {
return $this->styles;
@@ -189,34 +231,10 @@
}
/**
- * Returns module for ResourceLoader, see getModuleName() for its name.
- * If our gadget has no scripts or styles suitable for RL, false will
be returned.
- * @return Mixed: GadgetResourceLoaderModule or false
+ * @return array
*/
- public function getModule() {
- $pages = array();
-
- foreach ( $this->styles as $style ) {
- $pages['MediaWiki:' . $style] = array( 'type' =>
'style' );
- }
-
- if ( $this->supportsResourceLoader() ) {
- foreach ( $this->scripts as $script ) {
- $pages['MediaWiki:' . $script] = array( 'type'
=> 'script' );
- }
- }
-
- if ( !count( $pages ) ) {
- return null;
- }
-
- return new GadgetResourceLoaderModule(
- $pages,
- $this->dependencies,
- $this->targets,
- $this->position,
- $this->messages
- );
+ public function getTargets() {
+ return $this->targets;
}
/**
diff --git a/README b/README
index b7bea4f..ac85d70 100644
--- a/README
+++ b/README
@@ -35,3 +35,9 @@
* Gadgets do not apply to Special:Preferences, Special:UserLogin and
Special:ResetPass so users can always disable any broken gadgets they
may have enabled, and malicious gadgets will be unable to steal passwords.
+
+== Configuration settings ==
+* $wgGadgetsRepoClass configures which GadgetRepo implementation will be used
+ to source gadgets from. Currently, "MediaWikiGadgetsDefinitionRepo" is the
+ recommended setting and default. The "GadgetDefinitionNamespaceRepo" is not
+ ready for production usage yet.
diff --git a/SpecialGadgets.php b/SpecialGadgets.php
index 095de46..1428e8f 100644
--- a/SpecialGadgets.php
+++ b/SpecialGadgets.php
@@ -44,6 +44,7 @@
return;
}
+ $output->disallowUserJs();
$lang = $this->getLanguage();
$langSuffix = "";
if ( $lang->getCode() != $wgContLang->getCode() ) {
@@ -140,21 +141,25 @@
);
}
- $skins = array();
- $validskins = Skin::getSkinNames();
- foreach ( $gadget->getRequiredSkins() as
$skinid ) {
- if ( isset( $validskins[$skinid] ) ) {
- $skins[] = $this->msg(
"skinname-$skinid" )->plain();
- } else {
- $skins[] = $skinid;
+ $requiredSkins = $gadget->getRequiredSkins();
+ // $requiredSkins can be an array or true (if
all skins are supported)
+ if ( is_array( $requiredSkins ) ) {
+ $skins = array();
+ $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 ( count( $skins ) ) {
+ $output->addHTML(
+ '<br />' .
+ $this->msg(
'gadgets-required-skins', $lang->commaList( $skins ) )
+ ->numParams(
count( $skins ) )->parse()
+ );
+ }
}
if ( $gadget->isOnByDefault() ) {
@@ -191,7 +196,7 @@
$exportList = "MediaWiki:gadget-$gadget\n";
foreach ( $g->getScriptsAndStyles() as $page ) {
- $exportList .= "MediaWiki:$page\n";
+ $exportList .= "$page\n";
}
$output->addHTML( Html::openElement( 'form', array( 'method' =>
'get', 'action' => $wgScript ) )
diff --git a/extension.json b/extension.json
index 563443e..92576c3 100644
--- a/extension.json
+++ b/extension.json
@@ -25,7 +25,8 @@
"constant": "NS_GADGET_DEFINITION",
"name": "Gadget_definition",
"protection": "gadgets-definition-edit",
- "capitallinkoverride": false
+ "capitallinkoverride": false,
+ "defaultcontentmodel": "GadgetDefinition"
},
{
"id": 2303,
@@ -33,6 +34,9 @@
"name": "Gadget_definition_talk"
}
],
+ "ContentHandlers": {
+ "GadgetDefinition": "GadgetDefinitionContentHandler"
+ },
"AvailableRights": [
"gadgets-edit",
"gadgets-definition-edit"
@@ -63,7 +67,13 @@
"SpecialGadgets": "SpecialGadgets.php",
"SpecialGadgetUsage": "SpecialGadgetUsage.php",
"GadgetRepo": "includes/GadgetRepo.php",
- "MediaWikiGadgetsDefinitionRepo":
"includes/MediaWikiGadgetsDefinitionRepo.php"
+ "GadgetDefinitionNamespaceRepo":
"includes/GadgetDefinitionNamespaceRepo.php",
+ "MediaWikiGadgetsDefinitionRepo":
"includes/MediaWikiGadgetsDefinitionRepo.php",
+ "GadgetDefinitionContent":
"includes/content/GadgetDefinitionContent.php",
+ "GadgetDefinitionContentHandler":
"includes/content/GadgetDefinitionContentHandler.php",
+ "GadgetDefinitionValidator":
"includes/content/GadgetDefinitionValidator.php",
+ "GadgetDefinitionSecondaryDataUpdate":
"includes/content/GadgetDefinitionSecondaryDataUpdate.php",
+ "GadgetDefinitionDeletionUpdate":
"includes/content/GadgetDefinitionDeletionUpdate.php"
},
"Hooks": {
"ArticleSaveComplete": [
@@ -71,6 +81,18 @@
],
"BeforePageDisplay": [
"GadgetHooks::beforePageDisplay"
+ ],
+ "CodeEditorGetPageLanguage": [
+ "GadgetHooks::onCodeEditorGetPageLanguage"
+ ],
+ "ContentHandlerDefaultModelFor": [
+ "GadgetHooks::onContentHandlerDefaultModelFor"
+ ],
+ "EditFilterMergedContent": [
+ "GadgetHooks::onEditFilterMergedContent"
+ ],
+ "PageContentInsertComplete": [
+ "GadgetHooks::onPageContentInsertComplete"
],
"UserGetDefaultOptions": [
"GadgetHooks::userGetDefaultOptions"
@@ -88,5 +110,8 @@
"GadgetHooks::onwgQueryPages"
]
},
+ "config": {
+ "GadgetsRepoClass": "MediaWikiGadgetsDefinitionRepo"
+ },
"manifest_version": 1
}
diff --git a/i18n/en.json b/i18n/en.json
index 4d2a164..ba2e83f 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -26,6 +26,8 @@
"gadgets-not-found": "Gadget \"$1\" not found.",
"gadgets-export-text": "To export the $1 gadget, click on
\"{{int:gadgets-export-download}}\" button, save the downloaded file,\ngo to
Special:Import on destination wiki and upload it. Then add the following to
MediaWiki:Gadgets-definition page:\n<pre>$2</pre>\nYou must have appropriate
permissions on destination wiki (including the right to edit system messages)
and import from file uploads must be enabled.",
"gadgets-export-download": "Download",
+ "gadgets-validate-notset": "The property <code>$1</code> is not set.",
+ "gadgets-validate-wrongtype": "The property <code>$1</code> must be of
type <code>$2</code> instead of <code>$3</code>.",
"apihelp-query+gadgetcategories-description": "Returns a list of gadget
categories.",
"apihelp-query+gadgetcategories-param-prop": "What gadget category
information to get:\n;name:Internal category name.\n;title:Category
title.\n;members:Number of gadgets in category.",
"apihelp-query+gadgetcategories-param-names": "Names of categories to
retrieve.",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 7378bc6..d018e4a 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -38,6 +38,8 @@
"gadgets-not-found": "Used as error message. Parameters:\n* $1 - gadget
name",
"gadgets-export-text": "Used as page description in
[[Special:Gadgets]].\n\nRefers to {{msg-mw|Gadgets-export-download}}.\n\nSee
example: [[Special:Gadgets/export/editbuttons]]\n\nFollowed by the \"Export\"
form.\n\nParameters:\n* $1 - gadget name\n* $2 - gadget definition (code)",
"gadgets-export-download": "Use the verb for this message. Submit
button.\n{{Identical|Download}}",
+ "gadgets-validate-notset": "Error message shown if a a required
property is not set. $1 is the name of the property, e.g. settings.rights .",
+ "gadgets-validate-wrongtype": "Error message shown if a property is set
to the wrong type. * $1 is the name of the property, e.g. settings.rights or
module.messages[3].\n* $2 is the type that this property is expected to have\n*
$3 is the type it actually had",
"apihelp-query+gadgetcategories-description":
"{{doc-apihelp-description|query+gadgetcategories}}",
"apihelp-query+gadgetcategories-param-prop":
"{{doc-apihelp-param|query+gadgetcategories|prop}}",
"apihelp-query+gadgetcategories-param-names":
"{{doc-apihelp-param|query+gadgetcategories|names}}",
diff --git a/includes/GadgetDefinitionNamespaceRepo.php
b/includes/GadgetDefinitionNamespaceRepo.php
new file mode 100644
index 0000000..3d0a423
--- /dev/null
+++ b/includes/GadgetDefinitionNamespaceRepo.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * 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;
+
+ /**
+ * @var string
+ */
+ private $idsKey;
+
+ public function __construct () {
+ $this->idsKey = wfMemcKey( 'gadgets', 'namespace', 'ids' );
+ $this->wanCache = ObjectCache::getMainWANInstance();
+ }
+
+ /**
+ * 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->idsKey );
+ }
+
+ /**
+ * Get a list of gadget ids from cache/database
+ *
+ * @return string[]
+ */
+ public function getGadgetIds() {
+ return $this->wanCache->getWithSetCallback(
+ $this->idsKey,
+ self::CACHE_TTL,
+ function( $oldValue, &$ttl, array &$setOpts ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $setOpts += Database::getCacheSetOptions( $dbr
);
+ return $dbr->selectFieldValues(
+ 'page',
+ 'page_title',
+ array(
+ 'page_namespace' =>
NS_GADGET_DEFINITION
+ ),
+ __METHOD__
+ );
+ },
+ array(
+ 'checkKeys' => array( $this->idsKey ),
+ 'pcTTL' => 5,
+ 'lockTSE' => '30',
+ )
+ );
+ }
+
+ /**
+ * Update the cache for a specific Gadget whenever it is updated
+ *
+ * @param string $id
+ */
+ public function updateGadgetObjectCache( $id ) {
+ $this->wanCache->touchCheckKey( $this->getGadgetCacheKey( $id )
);
+ }
+
+ private function getGadgetCacheKey( $id ) {
+ return wfMemcKey( 'gadgets', 'object', md5( $id ),
Gadget::GADGET_CLASS_VERSION );
+ }
+
+ /**
+ * @param string $id
+ * @throws InvalidArgumentException
+ * @return Gadget
+ */
+ public function getGadget( $id ) {
+ $key = $this->getGadgetCacheKey( $id );
+ $gadget = $this->wanCache->getWithSetCallback(
+ $key,
+ self::CACHE_TTL,
+ function( $old, &$ttl, array &$setOpts ) use ( $id ) {
+ $setOpts += Database::getCacheSetOptions(
wfGetDB( DB_SLAVE ) );
+ $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 );
+ },
+ array(
+ 'checkKeys' => array( $key ),
+ 'pcTTL' => 5,
+ 'lockTSE' => '30',
+ )
+ );
+
+ if ( $gadget === null ) {
+ throw new InvalidArgumentException( "No gadget
registered for '$id'" );
+ }
+
+ return $gadget;
+ }
+}
diff --git a/includes/GadgetRepo.php b/includes/GadgetRepo.php
index 18bf5b5..f4e9f18 100644
--- a/includes/GadgetRepo.php
+++ b/includes/GadgetRepo.php
@@ -10,6 +10,9 @@
/**
* 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();
@@ -31,7 +34,11 @@
public function getStructuredList() {
$list = array();
foreach ( $this->getGadgetIds() as $id ) {
- $gadget = $this->getGadget( $id );
+ try {
+ $gadget = $this->getGadget( $id );
+ } catch ( InvalidArgumentException $e ) {
+ continue;
+ }
$list[$gadget->getCategory()][$gadget->getName()] =
$gadget;
}
@@ -39,15 +46,14 @@
}
/**
- * Get the configured default GadgetRepo. Currently
- * this hardcodes MediaWikiGadgetsDefinitionRepo since
- * that is the only implementation
+ * Get the configured default GadgetRepo.
*
* @return GadgetRepo
*/
public static function singleton() {
if ( self::$instance === null ) {
- self::$instance = new MediaWikiGadgetsDefinitionRepo();
+ global $wgGadgetsRepoClass; // @todo use Config here
+ self::$instance = new $wgGadgetsRepoClass();
}
return self::$instance;
}
diff --git a/includes/GadgetResourceLoaderModule.php
b/includes/GadgetResourceLoaderModule.php
index d4b537d..14156b7 100644
--- a/includes/GadgetResourceLoaderModule.php
+++ b/includes/GadgetResourceLoaderModule.php
@@ -1,40 +1,65 @@
<?php
/**
- * Class representing a list of resources for one gadget
+ * Class representing a list of resources for one gadget, basically a wrapper
+ * around the Gadget class.
*/
class GadgetResourceLoaderModule extends ResourceLoaderWikiModule {
- private $pages, $dependencies, $messages;
+ /**
+ * @var string
+ */
+ private $id;
+
+ /**
+ * @var Gadget
+ */
+ private $gadget;
/**
* Creates an instance of this class
*
- * @param $pages Array: Associative array of pages in
ResourceLoaderWikiModule-compatible
- * format, for example:
- * array(
- * 'MediaWiki:Gadget-foo.js' => array( 'type' => 'script' ),
- * 'MediaWiki:Gadget-foo.css' => array( 'type' => 'style' ),
- * )
- * @param $dependencies Array: Names of resources this module depends on
- * @param $targets Array: List of targets this module support
- * @param $position String: 'bottom' or 'top'
- * @param $messages Array
+ * @param array $options
*/
- public function __construct( $pages, $dependencies, $targets,
$position, $messages ) {
- $this->pages = $pages;
- $this->dependencies = $dependencies;
- $this->targets = $targets;
- $this->position = $position;
- $this->messages = $messages;
+ public function __construct( array $options ) {
+ $this->id = $options['id'];
}
/**
- * Overrides the abstract function from ResourceLoaderWikiModule class
- * @param $context ResourceLoaderContext
- * @return Array: $pages passed to __construct()
+ * @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 ) {
- return $this->pages;
+ $gadget = $this->getGadget();
+ $pages = array();
+
+ foreach ( $gadget->getStyles() as $style ) {
+ $pages[$style] = array( 'type' => 'style' );
+ }
+
+ if ( $gadget->supportsResourceLoader() ) {
+ foreach ( $gadget->getScripts() as $script ) {
+ $pages[$script] = array( 'type' => 'script' );
+ }
+ }
+
+ return $pages;
}
/**
@@ -43,7 +68,7 @@
* @return Array: Names of resources this module depends on
*/
public function getDependencies( ResourceLoaderContext $context = null
) {
- return $this->dependencies;
+ return $this->getGadget()->getDependencies();
}
/**
@@ -51,10 +76,14 @@
* @return String: 'bottom' or 'top'
*/
public function getPosition() {
- return $this->position;
+ return $this->getGadget()->getPosition();
}
public function getMessages() {
- return $this->messages;
+ return $this->getGadget()->getMessages();
+ }
+
+ public function getTargets() {
+ return $this->getGadget()->getTargets();
}
}
diff --git a/includes/MediaWikiGadgetsDefinitionRepo.php
b/includes/MediaWikiGadgetsDefinitionRepo.php
index 5377fad..45277d9 100644
--- a/includes/MediaWikiGadgetsDefinitionRepo.php
+++ b/includes/MediaWikiGadgetsDefinitionRepo.php
@@ -216,7 +216,7 @@
}
foreach ( preg_split( '/\s*\|\s*/', $m[3], -1,
PREG_SPLIT_NO_EMPTY ) as $page ) {
- $page = "Gadget-$page";
+ $page = "MediaWiki:Gadget-$page";
if ( preg_match( '/\.js/', $page ) ) {
$info['scripts'][] = $page;
diff --git a/includes/content/GadgetDefinitionContent.php
b/includes/content/GadgetDefinitionContent.php
new file mode 100644
index 0000000..c72d708
--- /dev/null
+++ b/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(), true,
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 ( array( '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
+ */
+ 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 DataUpdate[]
+ */
+ public function getDeletionUpdates( WikiPage $page, ParserOutput
$parserOutput = null ) {
+ return array_merge(
+ parent::getDeletionUpdates( $page, $parserOutput ),
+ array( new GadgetDefinitionDeletionUpdate(
$page->getTitle()->getText() ) )
+ );
+ }
+
+ /**
+ * @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 ),
+ array( new GadgetDefinitionSecondaryDataUpdate(
$title->getText() ) )
+ );
+ }
+}
diff --git a/includes/content/GadgetDefinitionContentHandler.php
b/includes/content/GadgetDefinitionContentHandler.php
new file mode 100644
index 0000000..49d4f18
--- /dev/null
+++ b/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(), true ) );
+ }
+
+ public function getDefaultMetadata() {
+ return array(
+ 'settings' => array(
+ 'rights' => array(),
+ 'default' => false,
+ 'hidden' => false,
+ 'skins' => array(),
+ 'category' => ''
+ ),
+ 'module' => array(
+ 'scripts' => array(),
+ 'styles' => array(),
+ 'dependencies' => array(),
+ 'messages' => array(),
+ 'position' => 'bottom',
+ ),
+ );
+
+ }
+}
diff --git a/includes/content/GadgetDefinitionDeletionUpdate.php
b/includes/content/GadgetDefinitionDeletionUpdate.php
new file mode 100644
index 0000000..26951bc
--- /dev/null
+++ b/includes/content/GadgetDefinitionDeletionUpdate.php
@@ -0,0 +1,45 @@
+<?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
+ */
+
+/**
+ * DataUpdate to run whenever a page in the Gadget definition
+ * is deleted.
+ */
+class GadgetDefinitionDeletionUpdate extends DataUpdate {
+ /**
+ * Gadget id
+ * @var string
+ */
+ private $id;
+
+ public function __construct( $id ) {
+ $this->id = $id;
+ }
+
+ public function doUpdate() {
+ $repo = GadgetRepo::singleton();
+ if ( $repo instanceof GadgetDefinitionNamespaceRepo ) {
+ $repo->purgeGadgetIdsList();
+ $repo->updateGadgetObjectCache( $this->id );
+ }
+ }
+}
diff --git a/includes/content/GadgetDefinitionSecondaryDataUpdate.php
b/includes/content/GadgetDefinitionSecondaryDataUpdate.php
new file mode 100644
index 0000000..cb1d802
--- /dev/null
+++ b/includes/content/GadgetDefinitionSecondaryDataUpdate.php
@@ -0,0 +1,37 @@
+<?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 GadgetDefinitionSecondaryDataUpdate extends DataUpdate {
+
+ private $id;
+
+ public function __construct( $id ) {
+ $this->id = $id;
+ }
+
+ public function doUpdate() {
+ $repo = GadgetRepo::singleton();
+ if ( $repo instanceof GadgetDefinitionNamespaceRepo ) {
+ $repo->updateGadgetObjectCache( $this->id );
+ }
+ }
+}
diff --git a/includes/content/GadgetDefinitionValidator.php
b/includes/content/GadgetDefinitionValidator.php
new file mode 100644
index 0000000..e17c166
--- /dev/null
+++ b/includes/content/GadgetDefinitionValidator.php
@@ -0,0 +1,89 @@
+<?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' => array( 'type check callback', 'type name' [,
'member type check callback', 'member type name'] )
+ */
+ protected static $propertyValidation = array(
+ 'settings' => array( 'is_array', 'array' ),
+ 'settings.rights' => array( 'is_array', 'array' , 'is_string',
'string' ),
+ 'settings.default' => array( 'is_bool', 'boolean' ),
+ 'settings.hidden' => array( 'is_bool', 'boolean' ),
+ 'settings.skins' => array( array( __CLASS__, 'isArrayOrTrue' ),
'array or true', 'is_string', 'string' ),
+ 'settings.category' => array( 'is_string', 'string' ),
+ 'module' => array( 'is_array', 'array' ),
+ 'module.scripts' => array( 'is_array', 'array', 'is_string',
'string' ),
+ 'module.styles' => array( 'is_array', 'array', 'is_string',
'string' ),
+ 'module.dependencies' => array( 'is_array', 'array',
'is_string', 'string' ),
+ 'module.messages' => array( 'is_array', 'array', 'is_string',
'string' ),
+ 'module.position' => array( '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/includes/content/schema.json b/includes/content/schema.json
new file mode 100644
index 0000000..5d4a10d
--- /dev/null
+++ b/includes/content/schema.json
@@ -0,0 +1,74 @@
+{
+ "$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"
+ },
+ "position": {
+ "type": "string",
+ "description": "Whether this module
should be loaded asynchronously after the page loads (bottom) or synchronously
before the page is rendered (top)",
+ "enum": [
+ "top",
+ "bottom"
+ ],
+ "default": "bottom"
+ }
+ }
+ }
+ }
+}
diff --git a/tests/GadgetTest.php b/tests/GadgetTest.php
index 26d77e5..86ac0fd 100644
--- a/tests/GadgetTest.php
+++ b/tests/GadgetTest.php
@@ -25,12 +25,12 @@
public function testSimpleCases() {
$g = $this->create( '* foo bar| foo.css|foo.js|foo.bar' );
$this->assertEquals( 'foo_bar', $g->getName() );
- $this->assertEquals( 'ext.gadget.foo_bar', $g->getModuleName()
);
- $this->assertEquals( array( 'Gadget-foo.js' ), $g->getScripts()
);
- $this->assertEquals( array( 'Gadget-foo.css' ), $g->getStyles()
);
- $this->assertEquals( array( 'Gadget-foo.js', 'Gadget-foo.css' ),
+ $this->assertEquals( 'ext.gadget.foo_bar',
Gadget::getModuleName( $g->getName() ) );
+ $this->assertEquals( array( 'MediaWiki:Gadget-foo.js' ),
$g->getScripts() );
+ $this->assertEquals( array( 'MediaWiki:Gadget-foo.css' ),
$g->getStyles() );
+ $this->assertEquals( array( 'MediaWiki:Gadget-foo.js',
'MediaWiki:Gadget-foo.css' ),
$g->getScriptsAndStyles() );
- $this->assertEquals( array( 'Gadget-foo.js' ),
$g->getLegacyScripts() );
+ $this->assertEquals( array( 'MediaWiki:Gadget-foo.js' ),
$g->getLegacyScripts() );
$this->assertFalse( $g->supportsResourceLoader() );
$this->assertTrue( $g->hasModule() );
}
@@ -44,7 +44,7 @@
public function testDependencies() {
$g = $this->create( '*
foo[ResourceLoader|dependencies=jquery.ui]|bar.js' );
- $this->assertEquals( array( 'Gadget-bar.js' ), $g->getScripts()
);
+ $this->assertEquals( array( 'MediaWiki:Gadget-bar.js' ),
$g->getScripts() );
$this->assertTrue( $g->supportsResourceLoader() );
$this->assertEquals( array( 'jquery.ui' ),
$g->getDependencies() );
}
--
To view, visit https://gerrit.wikimedia.org/r/228781
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Ib11db5fb0f7b46793bfa956cf1367f1dc1059b1c
Gerrit-PatchSet: 18
Gerrit-Project: mediawiki/extensions/Gadgets
Gerrit-Branch: master
Gerrit-Owner: Legoktm <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Alex Monk <[email protected]>
Gerrit-Reviewer: Kaldari <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits