mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Gadgets
synced 2024-11-11 16:49:43 +00:00
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
This commit is contained in:
parent
f52d81a245
commit
519f30355e
111
GadgetHooks.php
111
GadgetHooks.php
|
@ -144,16 +144,12 @@ class GadgetHooks {
|
|||
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 @@ class GadgetHooks {
|
|||
*/
|
||||
$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 @@ class GadgetHooks {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
@ -63,6 +63,47 @@ class Gadget {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -70,7 +111,7 @@ class Gadget {
|
|||
* @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 @@ class Gadget {
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ class Gadget {
|
|||
*/
|
||||
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 @@ class Gadget {
|
|||
}
|
||||
|
||||
/**
|
||||
* @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 @@ class Gadget {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
6
README
6
README
|
@ -35,3 +35,9 @@ See http://www.mediawiki.org/wiki/Extension:Gadgets#Usage
|
|||
* 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.
|
||||
|
|
|
@ -44,6 +44,7 @@ class SpecialGadgets extends SpecialPage {
|
|||
return;
|
||||
}
|
||||
|
||||
$output->disallowUserJs();
|
||||
$lang = $this->getLanguage();
|
||||
$langSuffix = "";
|
||||
if ( $lang->getCode() != $wgContLang->getCode() ) {
|
||||
|
@ -140,21 +141,25 @@ class SpecialGadgets extends SpecialPage {
|
|||
);
|
||||
}
|
||||
|
||||
$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 @@ class SpecialGadgets extends SpecialPage {
|
|||
|
||||
$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 ) )
|
||||
|
|
|
@ -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": [
|
||||
|
@ -72,6 +82,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
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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}}",
|
||||
|
|
126
includes/GadgetDefinitionNamespaceRepo.php
Normal file
126
includes/GadgetDefinitionNamespaceRepo.php
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@ abstract class GadgetRepo {
|
|||
/**
|
||||
* 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 @@ abstract class GadgetRepo {
|
|||
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 @@ abstract class GadgetRepo {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -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 @@ class GadgetResourceLoaderModule extends ResourceLoaderWikiModule {
|
|||
* @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 @@ class GadgetResourceLoaderModule extends ResourceLoaderWikiModule {
|
|||
* @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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,7 +216,7 @@ class MediaWikiGadgetsDefinitionRepo extends GadgetRepo {
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
125
includes/content/GadgetDefinitionContent.php
Normal file
125
includes/content/GadgetDefinitionContent.php
Normal file
|
@ -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() ) )
|
||||
);
|
||||
}
|
||||
}
|
64
includes/content/GadgetDefinitionContentHandler.php
Normal file
64
includes/content/GadgetDefinitionContentHandler.php
Normal file
|
@ -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',
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
45
includes/content/GadgetDefinitionDeletionUpdate.php
Normal file
45
includes/content/GadgetDefinitionDeletionUpdate.php
Normal file
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
37
includes/content/GadgetDefinitionSecondaryDataUpdate.php
Normal file
37
includes/content/GadgetDefinitionSecondaryDataUpdate.php
Normal file
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
89
includes/content/GadgetDefinitionValidator.php
Normal file
89
includes/content/GadgetDefinitionValidator.php
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
74
includes/content/schema.json
Normal file
74
includes/content/schema.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,12 +25,12 @@ class GadgetsTest extends MediaWikiTestCase {
|
|||
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 @@ class GadgetsTest extends MediaWikiTestCase {
|
|||
|
||||
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() );
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue