From 83ec5909d463be00c70f92246401753b595144c0 Mon Sep 17 00:00:00 2001 From: Kunal Mehta Date: Thu, 23 Jun 2016 00:33:35 +0200 Subject: [PATCH] registration: Convert "config" into an object with metadata To add extra metadata for "config" options without constantly adding hacky underscore prefixed keys, convert "config" items into an object, where the value is under a "value" key. "_merge_strategy" is now just "merge_strategy", but still has the underscore prefix for the cache structure, to avoid changing it. Since this is a fully breaking change, it only applies to files with manifest_version: 2 set. A conversion script has been added to assist with automated conversion. Bug: T133626 Change-Id: Id1071fc0647892438e5cd0e3ee621fbdaaa64014 --- docs/extension.schema.json | 21 +- docs/extension.schema.v1.json | 886 +++++++++++++++++++ includes/registration/ExtensionProcessor.php | 35 +- includes/registration/ExtensionRegistry.php | 2 +- maintenance/updateExtensionJsonSchema.php | 69 ++ 5 files changed, 1001 insertions(+), 12 deletions(-) create mode 100644 docs/extension.schema.v1.json create mode 100644 maintenance/updateExtensionJsonSchema.php diff --git a/docs/extension.schema.json b/docs/extension.schema.json index 93ee316487..818814f56c 100644 --- a/docs/extension.schema.json +++ b/docs/extension.schema.json @@ -849,20 +849,21 @@ ], "description": "A function to be called right after MediaWiki processes this file" }, + "config_prefix": { + "type": "string", + "default": "wg", + "description": "Prefix to put in front of configuration settings when exporting them to $GLOBALS" + }, "config": { "type": "object", "description": "Configuration options for this extension", - "properties": { - "_prefix": { - "type": "string", - "default": "wg", - "description": "Prefix to put in front of configuration settings when exporting them to $GLOBALS" - } - }, "patternProperties": { "^[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*$": { "properties": { - "_merge_strategy": { + "value": { + "required": true + }, + "merge_strategy": { "type": "string", "enum": [ "array_merge_recursive", @@ -871,6 +872,10 @@ "array_merge" ], "default": "array_merge" + }, + "description": { + "type": ["string", "array"], + "description": "A description of the config setting, mostly for documentation/developers" } } } diff --git a/docs/extension.schema.v1.json b/docs/extension.schema.v1.json new file mode 100644 index 0000000000..893facfd7a --- /dev/null +++ b/docs/extension.schema.v1.json @@ -0,0 +1,886 @@ +{ + "$schema": "http://json-schema.org/schema#", + "description": "MediaWiki extension.json schema", + "type": "object", + "properties": { + "manifest_version": { + "type": "integer", + "description": "Version of the extension.json schema the extension.json file is in.", + "required": true + }, + "name": { + "type": "string", + "description": "The extension's canonical name.", + "required": true + }, + "namemsg": { + "type": "string", + "description": "i18n message key of the extension's name." + }, + "type": { + "type": "string", + "description": "The extension's type, as an index to $wgExtensionCredits.", + "default": "other" + }, + "author": { + "type": [ + "string", + "array" + ], + "description": "Extension's authors.", + "items": { + "type": "string" + } + }, + "version": { + "type": "string", + "description": "The version of this release of the extension." + }, + "url": { + "type": "string", + "description": "URL to the homepage for the extension.", + "format": "uri" + }, + "description": { + "type": "string", + "description": "Raw description of the extension." + }, + "descriptionmsg": { + "type": "string", + "description": "Message key for a i18n message describing the extension." + }, + "license-name": { + "type": "string", + "description": "Short identifier for the license under which the extension is released.", + "enum": [ + "AFL-1.1", + "AFL-1.2", + "AFL-2.0", + "AFL-2.1", + "AFL-3.0", + "APL-1.0", + "Aladdin", + "ANTLR-PD", + "Apache-1.0", + "Apache-1.1", + "Apache-2.0", + "APSL-1.0", + "APSL-1.1", + "APSL-1.2", + "APSL-2.0", + "Artistic-1.0", + "Artistic-1.0-cl8", + "Artistic-1.0-Perl", + "Artistic-2.0", + "AAL", + "BitTorrent-1.0", + "BitTorrent-1.1", + "BSL-1.0", + "BSD-2-Clause", + "BSD-2-Clause-FreeBSD", + "BSD-2-Clause-NetBSD", + "BSD-3-Clause", + "BSD-3-Clause-Clear", + "BSD-4-Clause", + "BSD-4-Clause-UC", + "CECILL-1.0", + "CECILL-1.1", + "CECILL-2.0", + "CECILL-B", + "CECILL-C", + "ClArtistic", + "CNRI-Python", + "CNRI-Python-GPL-Compatible", + "CPOL-1.02", + "CDDL-1.0", + "CDDL-1.1", + "CPAL-1.0", + "CPL-1.0", + "CATOSL-1.1", + "Condor-1.1", + "CC-BY-1.0", + "CC-BY-2.0", + "CC-BY-2.5", + "CC-BY-3.0", + "CC-BY-ND-1.0", + "CC-BY-ND-2.0", + "CC-BY-ND-2.5", + "CC-BY-ND-3.0", + "CC-BY-NC-1.0", + "CC-BY-NC-2.0", + "CC-BY-NC-2.5", + "CC-BY-NC-3.0", + "CC-BY-NC-ND-1.0", + "CC-BY-NC-ND-2.0", + "CC-BY-NC-ND-2.5", + "CC-BY-NC-ND-3.0", + "CC-BY-NC-SA-1.0", + "CC-BY-NC-SA-2.0", + "CC-BY-NC-SA-2.5", + "CC-BY-NC-SA-3.0", + "CC-BY-SA-1.0", + "CC-BY-SA-2.0", + "CC-BY-SA-2.5", + "CC-BY-SA-3.0", + "CC0-1.0", + "CUA-OPL-1.0", + "D-FSL-1.0", + "WTFPL", + "EPL-1.0", + "eCos-2.0", + "ECL-1.0", + "ECL-2.0", + "EFL-1.0", + "EFL-2.0", + "Entessa", + "ErlPL-1.1", + "EUDatagrid", + "EUPL-1.0", + "EUPL-1.1", + "Fair", + "Frameworx-1.0", + "FTL", + "AGPL-1.0", + "AGPL-3.0", + "GFDL-1.1", + "GFDL-1.2", + "GFDL-1.3", + "GPL-1.0", + "GPL-1.0+", + "GPL-2.0", + "GPL-2.0+", + "GPL-2.0-with-autoconf-exception", + "GPL-2.0-with-bison-exception", + "GPL-2.0-with-classpath-exception", + "GPL-2.0-with-font-exception", + "GPL-2.0-with-GCC-exception", + "GPL-3.0", + "GPL-3.0+", + "GPL-3.0-with-autoconf-exception", + "GPL-3.0-with-GCC-exception", + "LGPL-2.1", + "LGPL-2.1+", + "LGPL-3.0", + "LGPL-3.0+", + "LGPL-2.0", + "LGPL-2.0+", + "gSOAP-1.3b", + "HPND", + "IBM-pibs", + "IPL-1.0", + "Imlib2", + "IJG", + "Intel", + "IPA", + "ISC", + "JSON", + "LPPL-1.3a", + "LPPL-1.0", + "LPPL-1.1", + "LPPL-1.2", + "LPPL-1.3c", + "Libpng", + "LPL-1.02", + "LPL-1.0", + "MS-PL", + "MS-RL", + "MirOS", + "MIT", + "Motosoto", + "MPL-1.0", + "MPL-1.1", + "MPL-2.0", + "MPL-2.0-no-copyleft-exception", + "Multics", + "NASA-1.3", + "Naumen", + "NBPL-1.0", + "NGPL", + "NOSL", + "NPL-1.0", + "NPL-1.1", + "Nokia", + "NPOSL-3.0", + "NTP", + "OCLC-2.0", + "ODbL-1.0", + "PDDL-1.0", + "OGTSL", + "OLDAP-2.2.2", + "OLDAP-1.1", + "OLDAP-1.2", + "OLDAP-1.3", + "OLDAP-1.4", + "OLDAP-2.0", + "OLDAP-2.0.1", + "OLDAP-2.1", + "OLDAP-2.2", + "OLDAP-2.2.1", + "OLDAP-2.3", + "OLDAP-2.4", + "OLDAP-2.5", + "OLDAP-2.6", + "OLDAP-2.7", + "OPL-1.0", + "OSL-1.0", + "OSL-2.0", + "OSL-2.1", + "OSL-3.0", + "OLDAP-2.8", + "OpenSSL", + "PHP-3.0", + "PHP-3.01", + "PostgreSQL", + "Python-2.0", + "QPL-1.0", + "RPSL-1.0", + "RPL-1.1", + "RPL-1.5", + "RHeCos-1.1", + "RSCPL", + "Ruby", + "SAX-PD", + "SGI-B-1.0", + "SGI-B-1.1", + "SGI-B-2.0", + "OFL-1.0", + "OFL-1.1", + "SimPL-2.0", + "Sleepycat", + "SMLNJ", + "SugarCRM-1.1.3", + "SISSL", + "SISSL-1.2", + "SPL-1.0", + "Watcom-1.0", + "NCSA", + "VSL-1.0", + "W3C", + "WXwindows", + "Xnet", + "X11", + "XFree86-1.1", + "YPL-1.0", + "YPL-1.1", + "Zimbra-1.3", + "Zlib", + "ZPL-1.1", + "ZPL-2.0", + "ZPL-2.1", + "Unlicense" + ] + }, + "requires": { + "type": "object", + "description": "Indicates what versions of MediaWiki core are required. This syntax may be extended in the future, for example to check dependencies between other extensions.", + "properties": { + "MediaWiki": { + "type": "string", + "description": "Version constraint string against MediaWiki core." + } + } + }, + "ResourceFileModulePaths": { + "type": "object", + "description": "Default paths to use for all ResourceLoader file modules", + "additionalProperties": false, + "properties": { + "localBasePath": { + "type": "string", + "description": "Base path to prepend to all local paths, relative to current directory" + }, + "remoteExtPath": { + "type": "string", + "description": "Base path to prepend to all remote paths, relative to $wgExtensionAssetsPath" + }, + "remoteSkinPath": { + "type": "string", + "description": "Base path to prepend to all remote paths, relative to $wgStylePath" + } + } + }, + "ResourceModules": { + "type": "object", + "description": "ResourceLoader modules to register", + "patternProperties": { + "^[a-zA-Z0-9-\\.]+$": { + "type": "object", + "anyOf": [ + { + "description": "A ResourceLoaderFileModule definition", + "additionalProperties": false, + "properties": { + "localBasePath": { + "type": "string", + "description": "Base path to prepend to all local paths in $options. Defaults to $IP" + }, + "remoteBasePath": { + "type": "string", + "description": "Base path to prepend to all remote paths in $options. Defaults to $wgScriptPath" + }, + "remoteExtPath": { + "type": "string", + "description": "Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath" + }, + "skipFunction": { + "type": "string", + "description": "Path to a file containing a JavaScript \"skip function\", if desired." + }, + "scripts": { + "type": ["string", "array"], + "description": "Scripts to always include (array of file paths)", + "items": { + "type": "string" + } + }, + "languageScripts": { + "type": "object", + "description": "Scripts to include in specific language contexts (mapping of language code to file path(s))", + "patternProperties": { + "^[a-zA-Z0-9-]{2,}$": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + } + } + } + }, + "skinScripts": { + "type": "object", + "description": "Scripts to include in specific skin contexts (mapping of skin name to script(s)", + "patternProperties": { + ".+": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + } + } + } + }, + "debugScripts": { + "type": ["string", "array"], + "description": "Scripts to include in debug contexts", + "items": { + "type": "string" + } + }, + "loaderScripts": { + "type": ["string", "array"], + "description": "Scripts to include in the startup module", + "items": { + "type": "string" + } + }, + "dependencies": { + "type": ["string", "array"], + "description": "Modules which must be loaded before this module", + "items": { + "type": "string" + } + }, + "styles": { + "type": ["string", "array", "object"], + "description": "Styles to always load", + "items": { + "type": "string" + } + }, + "skinStyles": { + "type": "object", + "description": "Styles to include in specific skin contexts (mapping of skin name to style(s))", + "patternProperties": { + ".+": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + } + } + } + }, + "messages": { + "type": ["string", "array"], + "description": "Messages to always load", + "items": { + "type": "string" + } + }, + "group": { + "type": "string", + "description": "Group which this module should be loaded together with" + }, + "position": { + "type": "string", + "description": "Position on the page to load this module at", + "enum": [ + "bottom", + "top" + ] + }, + "templates": { + "type": ["object", "array"], + "description": "Templates to be loaded for client-side usage" + }, + "targets": { + "type": ["string", "array"], + "description": "ResourceLoader target the module can run on", + "items": { + "type": "string" + } + } + } + }, + { + "description": "A ResourceLoaderWikiModule definition", + "additionalProperties": false, + "properties": { + "class": { + "enum": ["ResourceLoaderWikiModule"] + }, + "group": { + "type": "string", + "description": "Group which this module should be loaded together with" + }, + "position": { + "type": "string", + "description": "Position on the page to load this module at", + "enum": [ + "bottom", + "top" + ] + }, + "targets": { + "type": ["string", "array"], + "description": "ResourceLoader target the module can run on", + "items": { + "type": "string" + } + }, + "scripts": { + "type": "array", + "items": { + "type": "string" + } + }, + "styles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "description": "A ResourceLoaderImageModule definition", + "additionalProperties": false, + "properties": { + "class": { + "enum": ["ResourceLoaderImageModule"] + }, + "data": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "selector": { + "type": "string" + }, + "selectorWithoutVariant": { + "type": "string" + }, + "selectorWithVariant": { + "type": "string" + }, + "variants": { + "type": "object" + }, + "images": { + "type": "object" + }, + "position": { + "enum": [ + "top", + "bottom" + ] + } + } + }, + { + "description": "An arbitrary ResourceLoaderModule definition", + "properties": { + "class": { + "type": "string", + "pattern": "^((?!ResourceLoader(File|Image)Module).)*$" + } + }, + "required": ["class"] + } + ] + } + } + }, + "ResourceModuleSkinStyles": { + "type": "object", + "description": "ResourceLoader modules for custom skin styles" + }, + "ResourceLoaderSources": { + "type": "object", + "description": "ResourceLoader sources to register" + }, + "ResourceLoaderLESSVars": { + "type": "object", + "description": "ResourceLoader LESS variables" + }, + "ConfigRegistry": { + "type": "object", + "description": "Registry of factory functions to create Config objects" + }, + "SessionProviders": { + "type": "object", + "description": "Session providers" + }, + "AuthManagerAutoConfig": { + "type": "object", + "description": "AuthManager auto-configuration", + "additionalProperties": false, + "properties": { + "preauth": { + "type": "object", + "description": "Pre-authentication providers" + }, + "primaryauth": { + "type": "object", + "description": "Primary authentication providers" + }, + "secondaryauth": { + "type": "object", + "description": "Secondary authentication providers" + } + } + }, + "CentralIdLookupProviders": { + "type": "object", + "description": "Central ID lookup providers" + }, + "namespaces": { + "type": "array", + "description": "Method to add extra namespaces", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "constant": { + "type": "string" + }, + "name": { + "type": "string" + }, + "gender": { + "type": "object", + "properties": { + "male": { + "type": "string" + }, + "female": { + "type": "string" + } + } + }, + "subpages": { + "type": "boolean", + "default": false + }, + "content": { + "type": "boolean", + "default": false + }, + "defaultcontentmodel": { + "type": "string" + }, + "protection": { + "type": ["string", "array"], + "description": "Userright(s) required to edit in this namespace" + }, + "capitallinkoverride": { + "type": "boolean", + "description": "Set $wgCapitalLinks on a per-namespace basis" + } + }, + "required": ["id", "constant", "name"] + } + }, + "TrackingCategories": { + "type": "array", + "description": "Tracking category message keys", + "items": { + "type": "string" + } + }, + "DefaultUserOptions": { + "type": "object", + "description": "Default values of user options" + }, + "HiddenPrefs": { + "type": "array", + "description": "Preferences users cannot set", + "items": { + "type": "string" + } + }, + "GroupPermissions": { + "type": "object", + "description": "Default permissions to give to user groups", + "patternProperties": { + "^[a-z]+$": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "type": "boolean" + } + } + } + } + }, + "RevokePermissions": { + "type": "object", + "description": "Default permissions to revoke from user groups", + "patternProperties": { + "^[a-z]+$": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "type": "boolean" + } + } + } + } + }, + "GrantPermissions": { + "type": "object", + "description": "Map of permissions granted to authorized consumers to their bundles, called 'grants'", + "patternProperties": { + "^[a-z]+$": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "type": "boolean" + } + } + } + } + }, + "GrantPermissionGroups": { + "type": "object", + "description": "Map of grants to their UI grouping", + "patternProperties": { + "^[a-z]+$": { + "type": "string" + } + } + }, + "ImplicitGroups": { + "type": "array", + "description": "Implicit groups" + }, + "GroupsAddToSelf": { + "type": "object", + "description": "Groups a user can add to themselves" + }, + "GroupsRemoveFromSelf": { + "type": "object", + "description": "Groups a user can remove from themselves" + }, + "AddGroups": { + "type": "object", + "description": "Groups a user can add to users" + }, + "RemoveGroups": { + "type": "object", + "description": "Groups a user can remove from users" + }, + "AvailableRights": { + "type": "array", + "description": "User rights added by the extension", + "items": { + "type": "string" + } + }, + "ContentHandlers": { + "type": "object", + "description": "Mapping of model ID to class name", + "patternProperties": { + "^[A-Za-z]+$": { + "type": "string" + } + } + }, + "RateLimits": { + "type": "object", + "description": "Rate limits" + }, + "RecentChangesFlags": { + "type": "object", + "description": "Flags (letter symbols) shown on RecentChanges pages" + }, + "MediaHandlers": { + "type": "object", + "description": "Plugins for media file type handling. Each entry in the array maps a MIME type to a PHP class name." + }, + "ExtensionFunctions": { + "type": [ + "array", + "string" + ], + "description": "Function to call after setup has finished", + "items": { + "type": "string" + } + }, + "ExtensionMessagesFiles": { + "type": "object", + "description": "File paths containing PHP internationalization data" + }, + "MessagesDirs": { + "type": "object", + "description": "Directory paths containing JSON internationalization data" + }, + "ExtensionEntryPointListFiles": { + "type": "object" + }, + "SpecialPages": { + "type": "object", + "description": "SpecialPages implemented in this extension (mapping of page name to class name)" + }, + "AutoloadClasses": { + "type": "object" + }, + "Hooks": { + "type": [ "string", "object" ], + "description": "Hooks this extension uses (mapping of hook name to callback)" + }, + "JobClasses": { + "type": "object", + "description": "Job types this extension implements (mapping of job type to class name)" + }, + "LogTypes": { + "type": "array", + "description": "List of new log types this extension uses" + }, + "LogRestrictions": { + "type": "object" + }, + "FilterLogTypes": { + "type": "object" + }, + "ActionFilteredLogs": { + "type": "object", + "description": "List of log types which can be filtered by log actions", + "patternProperties": { + "^[a-z-]+$": { + "type": "object", + "patternProperties": { + "^[a-z-]+$": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "LogNames": { + "type": "object" + }, + "LogHeaders": { + "type": "object" + }, + "LogActions": { + "type": "object" + }, + "LogActionsHandlers": { + "type": "object" + }, + "Actions": { + "type": "object" + }, + "APIModules": { + "type": "object" + }, + "APIFormatModules": { + "type": "object" + }, + "APIMetaModules": { + "type": "object" + }, + "APIPropModules": { + "type": "object" + }, + "APIListModules": { + "type": "object" + }, + "ValidSkinNames": { + "type": "object" + }, + "FeedClasses": { + "type": "object", + "description": "Available feeds objects" + }, + "SkinOOUIThemes": { + "type": "object" + }, + "callback": { + "type": [ + "array", + "string" + ], + "description": "A function to be called right after MediaWiki processes this file" + }, + "config": { + "type": "object", + "description": "Configuration options for this extension", + "properties": { + "_prefix": { + "type": "string", + "default": "wg", + "description": "Prefix to put in front of configuration settings when exporting them to $GLOBALS" + } + }, + "patternProperties": { + "^[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*$": { + "properties": { + "_merge_strategy": { + "type": "string", + "enum": [ + "array_merge_recursive", + "array_plus_2d", + "array_plus", + "array_merge" + ], + "default": "array_merge" + } + } + } + } + }, + "ParserTestFiles": { + "type": "array", + "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it" + }, + "load_composer_autoloader": { + "type": "boolean", + "description": "Load the composer autoloader for this extension, if one is present" + } + } +} diff --git a/includes/registration/ExtensionProcessor.php b/includes/registration/ExtensionProcessor.php index 62f9f9ed44..f4d51c4651 100644 --- a/includes/registration/ExtensionProcessor.php +++ b/includes/registration/ExtensionProcessor.php @@ -107,6 +107,7 @@ class ExtensionProcessor implements Processor { 'MessagesDirs', 'type', 'config', + 'config_prefix', 'ParserTestFiles', 'AutoloadClasses', 'manifest_version', @@ -159,7 +160,12 @@ class ExtensionProcessor implements Processor { * @return array */ public function extractInfo( $path, array $info, $version ) { - $this->extractConfig( $info ); + if ( $version === 2 ) { + $this->extractConfig2( $info ); + } else { + // $version === 1 + $this->extractConfig1( $info ); + } $this->extractHooks( $info ); $dir = dirname( $path ); $this->extractExtensionMessagesFiles( $dir, $info ); @@ -347,12 +353,12 @@ class ExtensionProcessor implements Processor { } /** - * Set configuration settings + * Set configuration settings for manifest_version == 1 * @todo In the future, this should be done via Config interfaces * * @param array $info */ - protected function extractConfig( array $info ) { + protected function extractConfig1( array $info ) { if ( isset( $info['config'] ) ) { if ( isset( $info['config']['_prefix'] ) ) { $prefix = $info['config']['_prefix']; @@ -368,6 +374,29 @@ class ExtensionProcessor implements Processor { } } + /** + * Set configuration settings for manifest_version == 2 + * @todo In the future, this should be done via Config interfaces + * + * @param array $info + */ + protected function extractConfig2( array $info ) { + if ( isset( $info['config_prefix'] ) ) { + $prefix = $info['config_prefix']; + } else { + $prefix = 'wg'; + } + if ( isset( $info['config'] ) ) { + foreach ( $info['config'] as $key => $data ) { + $value = $data['value']; + if ( isset( $value['merge_strategy'] ) ) { + $value[ExtensionRegistry::MERGE_STRATEGY] = $value['merge_strategy']; + } + $this->globals["$prefix$key"] = $value; + } + } + } + protected function extractParserTestFiles( $dir, array $info ) { if ( isset( $info['ParserTestFiles'] ) ) { foreach ( $info['ParserTestFiles'] as $path ) { diff --git a/includes/registration/ExtensionRegistry.php b/includes/registration/ExtensionRegistry.php index dc53ca455a..3bec457c1e 100644 --- a/includes/registration/ExtensionRegistry.php +++ b/includes/registration/ExtensionRegistry.php @@ -19,7 +19,7 @@ class ExtensionRegistry { /** * Version of the highest supported manifest version */ - const MANIFEST_VERSION = 1; + const MANIFEST_VERSION = 2; /** * Version of the oldest supported manifest version diff --git a/maintenance/updateExtensionJsonSchema.php b/maintenance/updateExtensionJsonSchema.php new file mode 100644 index 0000000000..427769f19f --- /dev/null +++ b/maintenance/updateExtensionJsonSchema.php @@ -0,0 +1,69 @@ +addDescription( 'Updates extension.json files to the latest manifest_version' ); + $this->addArg( 'path', 'Location to the extension.json or skin.json you wish to convert', + /* $required = */ true ); + } + + public function execute() { + $filename = $this->getArg( 0 ); + if ( !is_readable( $filename ) ) { + $this->error( "Error: Unable to read $filename", 1 ); + } + + $json = FormatJson::decode( file_get_contents( $filename ), true ); + if ( $json === null ) { + $this->error( "Error: Invalid JSON", 1 ); + } + + if ( !isset( $json['manifest_version'] ) ) { + $json['manifest_version'] = 1; + } + + if ( $json['manifest_version'] == ExtensionRegistry::MANIFEST_VERSION ) { + $this->output( "Already at the latest version: {$json['manifest_version']}\n" ); + return; + } + + while ( $json['manifest_version'] !== ExtensionRegistry::MANIFEST_VERSION ) { + $json['manifest_version'] += 1; + $func = "updateTo{$json['manifest_version']}"; + $this->$func( $json ); + } + + file_put_contents( $filename, FormatJson::encode( $json, "\t", FormatJson::ALL_OK ) . "\n" ); + $this->output( "Updated to {$json['manifest_version']}...\n" ); + } + + protected function updateTo2( &$json ) { + if ( isset( $json['config'] ) ) { + $config = $json['config']; + $json['config'] = []; + if ( isset( $config['_prefix'] ) ) { + $json = wfArrayInsertAfter( $json, [ + 'config_prefix' => $config['_prefix'] + ], 'config' ); + unset( $config['_prefix'] ); + } + + foreach ( $config as $name => $value ) { + if ( $name[0] !== '@' ) { + $json['config'][$name] = [ 'value' => $value ]; + if ( isset( $value[ExtensionRegistry::MERGE_STRATEGY] ) ) { + $json['config'][$name]['merge_strategy'] = $value[ExtensionRegistry::MERGE_STRATEGY]; + unset( $value[ExtensionRegistry::MERGE_STRATEGY] ); + } + } + } + } + } +} + +$maintClass = 'UpdateExtensionJsonSchema'; +require_once RUN_MAINTENANCE_IF_MAIN; -- 2.20.1