summaryrefslogtreecommitdiff
path: root/www/wiki/includes/registration
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/registration
first commit
Diffstat (limited to 'www/wiki/includes/registration')
-rw-r--r--www/wiki/includes/registration/ExtensionDependencyError.php81
-rw-r--r--www/wiki/includes/registration/ExtensionJsonValidationError.php22
-rw-r--r--www/wiki/includes/registration/ExtensionJsonValidator.php119
-rw-r--r--www/wiki/includes/registration/ExtensionProcessor.php549
-rw-r--r--www/wiki/includes/registration/ExtensionRegistry.php424
-rw-r--r--www/wiki/includes/registration/Processor.php53
-rw-r--r--www/wiki/includes/registration/VersionChecker.php236
7 files changed, 1484 insertions, 0 deletions
diff --git a/www/wiki/includes/registration/ExtensionDependencyError.php b/www/wiki/includes/registration/ExtensionDependencyError.php
new file mode 100644
index 00000000..d380d077
--- /dev/null
+++ b/www/wiki/includes/registration/ExtensionDependencyError.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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.
+ *
+ */
+
+/**
+ * @since 1.31
+ */
+class ExtensionDependencyError extends Exception {
+
+ /**
+ * @var string[]
+ */
+ public $missingExtensions = [];
+
+ /**
+ * @var string[]
+ */
+ public $missingSkins = [];
+
+ /**
+ * @var string[]
+ */
+ public $incompatibleExtensions = [];
+
+ /**
+ * @var string[]
+ */
+ public $incompatibleSkins = [];
+
+ /**
+ * @var bool
+ */
+ public $incompatibleCore = false;
+
+ /**
+ * @param array $errors Each error has a 'msg' and 'type' key at minimum
+ */
+ public function __construct( array $errors ) {
+ $msg = '';
+ foreach ( $errors as $info ) {
+ $msg .= $info['msg'] . "\n";
+ switch ( $info['type'] ) {
+ case 'incompatible-core':
+ $this->incompatibleCore = true;
+ break;
+ case 'missing-skins':
+ $this->missingSkins[] = $info['missing'];
+ break;
+ case 'missing-extensions':
+ $this->missingExtensions[] = $info['missing'];
+ break;
+ case 'incompatible-skins':
+ $this->incompatibleSkins[] = $info['incompatible'];
+ break;
+ case 'incompatible-extensions':
+ $this->incompatibleExtensions[] = $info['incompatible'];
+ break;
+ // default: continue
+ }
+ }
+
+ parent::__construct( $msg );
+ }
+
+}
diff --git a/www/wiki/includes/registration/ExtensionJsonValidationError.php b/www/wiki/includes/registration/ExtensionJsonValidationError.php
new file mode 100644
index 00000000..897d2840
--- /dev/null
+++ b/www/wiki/includes/registration/ExtensionJsonValidationError.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * 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 ExtensionJsonValidationError extends Exception {
+}
diff --git a/www/wiki/includes/registration/ExtensionJsonValidator.php b/www/wiki/includes/registration/ExtensionJsonValidator.php
new file mode 100644
index 00000000..7e3afaa8
--- /dev/null
+++ b/www/wiki/includes/registration/ExtensionJsonValidator.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Composer\Spdx\SpdxLicenses;
+use JsonSchema\Validator;
+
+/**
+ * @since 1.29
+ */
+class ExtensionJsonValidator {
+
+ /**
+ * @var callable
+ */
+ private $missingDepCallback;
+
+ /**
+ * @param callable $missingDepCallback
+ */
+ public function __construct( callable $missingDepCallback ) {
+ $this->missingDepCallback = $missingDepCallback;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @return bool
+ */
+ public function checkDependencies() {
+ if ( !class_exists( Validator::class ) ) {
+ call_user_func( $this->missingDepCallback,
+ 'The JsonSchema library cannot be found, please install it through composer.'
+ );
+ return false;
+ } elseif ( !class_exists( SpdxLicenses::class ) ) {
+ call_user_func( $this->missingDepCallback,
+ 'The spdx-licenses library cannot be found, please install it through composer.'
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $path file to validate
+ * @return bool true if passes validation
+ * @throws ExtensionJsonValidationError on any failure
+ */
+ public function validate( $path ) {
+ $data = json_decode( file_get_contents( $path ) );
+ if ( !is_object( $data ) ) {
+ throw new ExtensionJsonValidationError( "$path is not valid JSON" );
+ }
+
+ if ( !isset( $data->manifest_version ) ) {
+ throw new ExtensionJsonValidationError(
+ "$path does not have manifest_version set." );
+ }
+
+ $version = $data->manifest_version;
+ $schemaPath = __DIR__ . "/../../docs/extension.schema.v$version.json";
+
+ // Not too old
+ if ( $version < ExtensionRegistry::OLDEST_MANIFEST_VERSION ) {
+ throw new ExtensionJsonValidationError(
+ "$path is using a non-supported schema version"
+ );
+ } elseif ( $version > ExtensionRegistry::MANIFEST_VERSION ) {
+ throw new ExtensionJsonValidationError(
+ "$path is using a non-supported schema version"
+ );
+ }
+
+ $licenseError = false;
+ // Check if it's a string, if not, schema validation will display an error
+ if ( isset( $data->{'license-name'} ) && is_string( $data->{'license-name'} ) ) {
+ $licenses = new SpdxLicenses();
+ $valid = $licenses->validate( $data->{'license-name'} );
+ if ( !$valid ) {
+ $licenseError = '[license-name] Invalid SPDX license identifier, '
+ . 'see <https://spdx.org/licenses/>';
+ }
+ }
+
+ $validator = new Validator;
+ $validator->check( $data, (object)[ '$ref' => 'file://' . $schemaPath ] );
+ if ( $validator->isValid() && !$licenseError ) {
+ // All good.
+ return true;
+ } else {
+ $out = "$path did not pass validation.\n";
+ foreach ( $validator->getErrors() as $error ) {
+ $out .= "[{$error['property']}] {$error['message']}\n";
+ }
+ if ( $licenseError ) {
+ $out .= "$licenseError\n";
+ }
+ throw new ExtensionJsonValidationError( $out );
+ }
+ }
+}
diff --git a/www/wiki/includes/registration/ExtensionProcessor.php b/www/wiki/includes/registration/ExtensionProcessor.php
new file mode 100644
index 00000000..14d4a17d
--- /dev/null
+++ b/www/wiki/includes/registration/ExtensionProcessor.php
@@ -0,0 +1,549 @@
+<?php
+
+class ExtensionProcessor implements Processor {
+
+ /**
+ * Keys that should be set to $GLOBALS
+ *
+ * @var array
+ */
+ protected static $globalSettings = [
+ 'ActionFilteredLogs',
+ 'Actions',
+ 'AddGroups',
+ 'APIFormatModules',
+ 'APIListModules',
+ 'APIMetaModules',
+ 'APIModules',
+ 'APIPropModules',
+ 'AuthManagerAutoConfig',
+ 'AvailableRights',
+ 'CentralIdLookupProviders',
+ 'ChangeCredentialsBlacklist',
+ 'ConfigRegistry',
+ 'ContentHandlers',
+ 'DefaultUserOptions',
+ 'ExtensionEntryPointListFiles',
+ 'ExtensionFunctions',
+ 'FeedClasses',
+ 'FileExtensions',
+ 'FilterLogTypes',
+ 'GrantPermissionGroups',
+ 'GrantPermissions',
+ 'GroupPermissions',
+ 'GroupsAddToSelf',
+ 'GroupsRemoveFromSelf',
+ 'HiddenPrefs',
+ 'ImplicitGroups',
+ 'JobClasses',
+ 'LogActions',
+ 'LogActionsHandlers',
+ 'LogHeaders',
+ 'LogNames',
+ 'LogRestrictions',
+ 'LogTypes',
+ 'MediaHandlers',
+ 'PasswordPolicy',
+ 'RateLimits',
+ 'RecentChangesFlags',
+ 'RemoveCredentialsBlacklist',
+ 'RemoveGroups',
+ 'ResourceLoaderLESSVars',
+ 'ResourceLoaderSources',
+ 'RevokePermissions',
+ 'SessionProviders',
+ 'SpecialPages',
+ 'ValidSkinNames',
+ ];
+
+ /**
+ * Top-level attributes that come from MW core
+ *
+ * @var string[]
+ */
+ protected static $coreAttributes = [
+ 'SkinOOUIThemes',
+ 'TrackingCategories',
+ ];
+
+ /**
+ * Mapping of global settings to their specific merge strategies.
+ *
+ * @see ExtensionRegistry::exportExtractedData
+ * @see getExtractedInfo
+ * @var array
+ */
+ protected static $mergeStrategies = [
+ 'wgAuthManagerAutoConfig' => 'array_plus_2d',
+ 'wgCapitalLinkOverrides' => 'array_plus',
+ 'wgExtensionCredits' => 'array_merge_recursive',
+ 'wgExtraGenderNamespaces' => 'array_plus',
+ 'wgGrantPermissions' => 'array_plus_2d',
+ 'wgGroupPermissions' => 'array_plus_2d',
+ 'wgHooks' => 'array_merge_recursive',
+ 'wgNamespaceContentModels' => 'array_plus',
+ 'wgNamespaceProtection' => 'array_plus',
+ 'wgNamespacesWithSubpages' => 'array_plus',
+ 'wgPasswordPolicy' => 'array_merge_recursive',
+ 'wgRateLimits' => 'array_plus_2d',
+ 'wgRevokePermissions' => 'array_plus_2d',
+ ];
+
+ /**
+ * Keys that are part of the extension credits
+ *
+ * @var array
+ */
+ protected static $creditsAttributes = [
+ 'name',
+ 'namemsg',
+ 'author',
+ 'version',
+ 'url',
+ 'description',
+ 'descriptionmsg',
+ 'license-name',
+ ];
+
+ /**
+ * Things that are not 'attributes', but are not in
+ * $globalSettings or $creditsAttributes.
+ *
+ * @var array
+ */
+ protected static $notAttributes = [
+ 'callback',
+ 'Hooks',
+ 'namespaces',
+ 'ResourceFileModulePaths',
+ 'ResourceModules',
+ 'ResourceModuleSkinStyles',
+ 'ExtensionMessagesFiles',
+ 'MessagesDirs',
+ 'type',
+ 'config',
+ 'config_prefix',
+ 'ServiceWiringFiles',
+ 'ParserTestFiles',
+ 'AutoloadClasses',
+ 'manifest_version',
+ 'load_composer_autoloader',
+ ];
+
+ /**
+ * Stuff that is going to be set to $GLOBALS
+ *
+ * Some keys are pre-set to arrays so we can += to them
+ *
+ * @var array
+ */
+ protected $globals = [
+ 'wgExtensionMessagesFiles' => [],
+ 'wgMessagesDirs' => [],
+ ];
+
+ /**
+ * Things that should be define()'d
+ *
+ * @var array
+ */
+ protected $defines = [];
+
+ /**
+ * Things to be called once registration of these extensions are done
+ * keyed by the name of the extension that it belongs to
+ *
+ * @var callable[]
+ */
+ protected $callbacks = [];
+
+ /**
+ * @var array
+ */
+ protected $credits = [];
+
+ /**
+ * Any thing else in the $info that hasn't
+ * already been processed
+ *
+ * @var array
+ */
+ protected $attributes = [];
+
+ /**
+ * Extension attributes, keyed by name =>
+ * settings.
+ *
+ * @var array
+ */
+ protected $extAttributes = [];
+
+ /**
+ * @param string $path
+ * @param array $info
+ * @param int $version manifest_version for info
+ * @return array
+ */
+ public function extractInfo( $path, array $info, $version ) {
+ $dir = dirname( $path );
+ $this->extractHooks( $info );
+ $this->extractExtensionMessagesFiles( $dir, $info );
+ $this->extractMessagesDirs( $dir, $info );
+ $this->extractNamespaces( $info );
+ $this->extractResourceLoaderModules( $dir, $info );
+ if ( isset( $info['ServiceWiringFiles'] ) ) {
+ $this->extractPathBasedGlobal(
+ 'wgServiceWiringFiles',
+ $dir,
+ $info['ServiceWiringFiles']
+ );
+ }
+ if ( isset( $info['ParserTestFiles'] ) ) {
+ $this->extractPathBasedGlobal(
+ 'wgParserTestFiles',
+ $dir,
+ $info['ParserTestFiles']
+ );
+ }
+ $name = $this->extractCredits( $path, $info );
+ if ( isset( $info['callback'] ) ) {
+ $this->callbacks[$name] = $info['callback'];
+ }
+
+ // config should be after all core globals are extracted,
+ // so duplicate setting detection will work fully
+ if ( $version === 2 ) {
+ $this->extractConfig2( $info, $dir );
+ } else {
+ // $version === 1
+ $this->extractConfig1( $info );
+ }
+
+ if ( $version === 2 ) {
+ $this->extractAttributes( $path, $info );
+ }
+
+ foreach ( $info as $key => $val ) {
+ // If it's a global setting,
+ if ( in_array( $key, self::$globalSettings ) ) {
+ $this->storeToArray( $path, "wg$key", $val, $this->globals );
+ continue;
+ }
+ // Ignore anything that starts with a @
+ if ( $key[0] === '@' ) {
+ continue;
+ }
+
+ if ( $version === 2 ) {
+ // Only whitelisted attributes are set
+ if ( in_array( $key, self::$coreAttributes ) ) {
+ $this->storeToArray( $path, $key, $val, $this->attributes );
+ }
+ } else {
+ // version === 1
+ if ( !in_array( $key, self::$notAttributes )
+ && !in_array( $key, self::$creditsAttributes )
+ ) {
+ // If it's not blacklisted, it's an attribute
+ $this->storeToArray( $path, $key, $val, $this->attributes );
+ }
+ }
+
+ }
+ }
+
+ /**
+ * @param string $path
+ * @param array $info
+ */
+ protected function extractAttributes( $path, array $info ) {
+ if ( isset( $info['attributes'] ) ) {
+ foreach ( $info['attributes'] as $extName => $value ) {
+ $this->storeToArray( $path, $extName, $value, $this->extAttributes );
+ }
+ }
+ }
+
+ public function getExtractedInfo() {
+ // Make sure the merge strategies are set
+ foreach ( $this->globals as $key => $val ) {
+ if ( isset( self::$mergeStrategies[$key] ) ) {
+ $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::$mergeStrategies[$key];
+ }
+ }
+
+ // Merge $this->extAttributes into $this->attributes depending on what is loaded
+ foreach ( $this->extAttributes as $extName => $value ) {
+ // Only set the attribute if $extName is loaded (and hence present in credits)
+ if ( isset( $this->credits[$extName] ) ) {
+ foreach ( $value as $attrName => $attrValue ) {
+ $this->storeToArray(
+ '', // Don't provide a path since it's impossible to generate an error here
+ $extName . $attrName,
+ $attrValue,
+ $this->attributes
+ );
+ }
+ unset( $this->extAttributes[$extName] );
+ }
+ }
+
+ return [
+ 'globals' => $this->globals,
+ 'defines' => $this->defines,
+ 'callbacks' => $this->callbacks,
+ 'credits' => $this->credits,
+ 'attributes' => $this->attributes,
+ ];
+ }
+
+ public function getRequirements( array $info ) {
+ return isset( $info['requires'] ) ? $info['requires'] : [];
+ }
+
+ protected function extractHooks( array $info ) {
+ if ( isset( $info['Hooks'] ) ) {
+ foreach ( $info['Hooks'] as $name => $value ) {
+ if ( is_array( $value ) ) {
+ foreach ( $value as $callback ) {
+ $this->globals['wgHooks'][$name][] = $callback;
+ }
+ } else {
+ $this->globals['wgHooks'][$name][] = $value;
+ }
+ }
+ }
+ }
+
+ /**
+ * Register namespaces with the appropriate global settings
+ *
+ * @param array $info
+ */
+ protected function extractNamespaces( array $info ) {
+ if ( isset( $info['namespaces'] ) ) {
+ foreach ( $info['namespaces'] as $ns ) {
+ if ( defined( $ns['constant'] ) ) {
+ // If the namespace constant is already defined, use it.
+ // This allows namespace IDs to be overwritten locally.
+ $id = constant( $ns['constant'] );
+ } else {
+ $id = $ns['id'];
+ $this->defines[ $ns['constant'] ] = $id;
+ }
+
+ if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
+ // If it is not conditional, register it
+ $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
+ }
+ if ( isset( $ns['gender'] ) ) {
+ $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
+ }
+ if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
+ $this->globals['wgNamespacesWithSubpages'][$id] = true;
+ }
+ if ( isset( $ns['content'] ) && $ns['content'] ) {
+ $this->globals['wgContentNamespaces'][] = $id;
+ }
+ if ( isset( $ns['defaultcontentmodel'] ) ) {
+ $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
+ }
+ if ( isset( $ns['protection'] ) ) {
+ $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
+ }
+ if ( isset( $ns['capitallinkoverride'] ) ) {
+ $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
+ }
+ }
+ }
+ }
+
+ protected function extractResourceLoaderModules( $dir, array $info ) {
+ $defaultPaths = isset( $info['ResourceFileModulePaths'] )
+ ? $info['ResourceFileModulePaths']
+ : false;
+ if ( isset( $defaultPaths['localBasePath'] ) ) {
+ if ( $defaultPaths['localBasePath'] === '' ) {
+ // Avoid double slashes (e.g. /extensions/Example//path)
+ $defaultPaths['localBasePath'] = $dir;
+ } else {
+ $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
+ }
+ }
+
+ foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles' ] as $setting ) {
+ if ( isset( $info[$setting] ) ) {
+ foreach ( $info[$setting] as $name => $data ) {
+ if ( isset( $data['localBasePath'] ) ) {
+ if ( $data['localBasePath'] === '' ) {
+ // Avoid double slashes (e.g. /extensions/Example//path)
+ $data['localBasePath'] = $dir;
+ } else {
+ $data['localBasePath'] = "$dir/{$data['localBasePath']}";
+ }
+ }
+ if ( $defaultPaths ) {
+ $data += $defaultPaths;
+ }
+ $this->globals["wg$setting"][$name] = $data;
+ }
+ }
+ }
+ }
+
+ protected function extractExtensionMessagesFiles( $dir, array $info ) {
+ if ( isset( $info['ExtensionMessagesFiles'] ) ) {
+ foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
+ $file = "$dir/$file";
+ }
+ $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
+ }
+ }
+
+ /**
+ * Set message-related settings, which need to be expanded to use
+ * absolute paths
+ *
+ * @param string $dir
+ * @param array $info
+ */
+ protected function extractMessagesDirs( $dir, array $info ) {
+ if ( isset( $info['MessagesDirs'] ) ) {
+ foreach ( $info['MessagesDirs'] as $name => $files ) {
+ foreach ( (array)$files as $file ) {
+ $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
+ }
+ }
+ }
+ }
+
+ /**
+ * @param string $path
+ * @param array $info
+ * @return string Name of thing
+ * @throws Exception
+ */
+ protected function extractCredits( $path, array $info ) {
+ $credits = [
+ 'path' => $path,
+ 'type' => isset( $info['type'] ) ? $info['type'] : 'other',
+ ];
+ foreach ( self::$creditsAttributes as $attr ) {
+ if ( isset( $info[$attr] ) ) {
+ $credits[$attr] = $info[$attr];
+ }
+ }
+
+ $name = $credits['name'];
+
+ // If someone is loading the same thing twice, throw
+ // a nice error (T121493)
+ if ( isset( $this->credits[$name] ) ) {
+ $firstPath = $this->credits[$name]['path'];
+ $secondPath = $credits['path'];
+ throw new Exception( "It was attempted to load $name twice, from $firstPath and $secondPath." );
+ }
+
+ $this->credits[$name] = $credits;
+ $this->globals['wgExtensionCredits'][$credits['type']][] = $credits;
+
+ return $name;
+ }
+
+ /**
+ * Set configuration settings for manifest_version == 1
+ * @todo In the future, this should be done via Config interfaces
+ *
+ * @param array $info
+ */
+ protected function extractConfig1( array $info ) {
+ if ( isset( $info['config'] ) ) {
+ if ( isset( $info['config']['_prefix'] ) ) {
+ $prefix = $info['config']['_prefix'];
+ unset( $info['config']['_prefix'] );
+ } else {
+ $prefix = 'wg';
+ }
+ foreach ( $info['config'] as $key => $val ) {
+ if ( $key[0] !== '@' ) {
+ $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
+ }
+ }
+ }
+ }
+
+ /**
+ * Set configuration settings for manifest_version == 2
+ * @todo In the future, this should be done via Config interfaces
+ *
+ * @param array $info
+ * @param string $dir
+ */
+ protected function extractConfig2( array $info, $dir ) {
+ 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( $data['merge_strategy'] ) ) {
+ $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
+ }
+ if ( isset( $data['path'] ) && $data['path'] ) {
+ $value = "$dir/$value";
+ }
+ $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
+ }
+ }
+ }
+
+ /**
+ * Helper function to set a value to a specific global, if it isn't set already.
+ *
+ * @param string $key The config key with the prefix and anything
+ * @param mixed $value The value of the config
+ * @param string $extName Name of the extension
+ */
+ private function addConfigGlobal( $key, $value, $extName ) {
+ if ( array_key_exists( $key, $this->globals ) ) {
+ throw new RuntimeException(
+ "The configuration setting '$key' was already set by MediaWiki core or"
+ . " another extension, and cannot be set again by $extName." );
+ }
+ $this->globals[$key] = $value;
+ }
+
+ protected function extractPathBasedGlobal( $global, $dir, $paths ) {
+ foreach ( $paths as $path ) {
+ $this->globals[$global][] = "$dir/$path";
+ }
+ }
+
+ /**
+ * @param string $path
+ * @param string $name
+ * @param array $value
+ * @param array &$array
+ * @throws InvalidArgumentException
+ */
+ protected function storeToArray( $path, $name, $value, &$array ) {
+ if ( !is_array( $value ) ) {
+ throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
+ }
+ if ( isset( $array[$name] ) ) {
+ $array[$name] = array_merge_recursive( $array[$name], $value );
+ } else {
+ $array[$name] = $value;
+ }
+ }
+
+ public function getExtraAutoloaderPaths( $dir, array $info ) {
+ $paths = [];
+ if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
+ $paths[] = "$dir/vendor/autoload.php";
+ }
+ return $paths;
+ }
+}
diff --git a/www/wiki/includes/registration/ExtensionRegistry.php b/www/wiki/includes/registration/ExtensionRegistry.php
new file mode 100644
index 00000000..aae5fc28
--- /dev/null
+++ b/www/wiki/includes/registration/ExtensionRegistry.php
@@ -0,0 +1,424 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * ExtensionRegistry class
+ *
+ * The Registry loads JSON files, and uses a Processor
+ * to extract information from them. It also registers
+ * classes with the autoloader.
+ *
+ * @since 1.25
+ */
+class ExtensionRegistry {
+
+ /**
+ * "requires" key that applies to MediaWiki core/$wgVersion
+ */
+ const MEDIAWIKI_CORE = 'MediaWiki';
+
+ /**
+ * Version of the highest supported manifest version
+ * Note: Update MANIFEST_VERSION_MW_VERSION when changing this
+ */
+ const MANIFEST_VERSION = 2;
+
+ /**
+ * MediaWiki version constraint representing what the current
+ * highest MANIFEST_VERSION is supported in
+ */
+ const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
+
+ /**
+ * Version of the oldest supported manifest version
+ */
+ const OLDEST_MANIFEST_VERSION = 1;
+
+ /**
+ * Bump whenever the registration cache needs resetting
+ */
+ const CACHE_VERSION = 6;
+
+ /**
+ * Special key that defines the merge strategy
+ *
+ * @since 1.26
+ */
+ const MERGE_STRATEGY = '_merge_strategy';
+
+ /**
+ * Array of loaded things, keyed by name, values are credits information
+ *
+ * @var array
+ */
+ private $loaded = [];
+
+ /**
+ * List of paths that should be loaded
+ *
+ * @var array
+ */
+ protected $queued = [];
+
+ /**
+ * Whether we are done loading things
+ *
+ * @var bool
+ */
+ private $finished = false;
+
+ /**
+ * Items in the JSON file that aren't being
+ * set as globals
+ *
+ * @var array
+ */
+ protected $attributes = [];
+
+ /**
+ * @var ExtensionRegistry
+ */
+ private static $instance;
+
+ /**
+ * @codeCoverageIgnore
+ * @return ExtensionRegistry
+ */
+ public static function getInstance() {
+ if ( self::$instance === null ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @param string $path Absolute path to the JSON file
+ */
+ public function queue( $path ) {
+ global $wgExtensionInfoMTime;
+
+ $mtime = $wgExtensionInfoMTime;
+ if ( $mtime === false ) {
+ if ( file_exists( $path ) ) {
+ $mtime = filemtime( $path );
+ } else {
+ throw new Exception( "$path does not exist!" );
+ }
+ // @codeCoverageIgnoreStart
+ if ( $mtime === false ) {
+ $err = error_get_last();
+ throw new Exception( "Couldn't stat $path: {$err['message']}" );
+ // @codeCoverageIgnoreEnd
+ }
+ }
+ $this->queued[$path] = $mtime;
+ }
+
+ /**
+ * @throws MWException If the queue is already marked as finished (no further things should
+ * be loaded then).
+ */
+ public function loadFromQueue() {
+ global $wgVersion, $wgDevelopmentWarnings;
+ if ( !$this->queued ) {
+ return;
+ }
+
+ if ( $this->finished ) {
+ throw new MWException(
+ "The following paths tried to load late: "
+ . implode( ', ', array_keys( $this->queued ) )
+ );
+ }
+
+ // A few more things to vary the cache on
+ $versions = [
+ 'registration' => self::CACHE_VERSION,
+ 'mediawiki' => $wgVersion
+ ];
+
+ // We use a try/catch because we don't want to fail here
+ // if $wgObjectCaches is not configured properly for APC setup
+ try {
+ $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
+ } catch ( MWException $e ) {
+ $cache = new EmptyBagOStuff();
+ }
+ // See if this queue is in APC
+ $key = $cache->makeKey(
+ 'registration',
+ md5( json_encode( $this->queued + $versions ) )
+ );
+ $data = $cache->get( $key );
+ if ( $data ) {
+ $this->exportExtractedData( $data );
+ } else {
+ $data = $this->readFromQueue( $this->queued );
+ $this->exportExtractedData( $data );
+ // Do this late since we don't want to extract it since we already
+ // did that, but it should be cached
+ $data['globals']['wgAutoloadClasses'] += $data['autoload'];
+ unset( $data['autoload'] );
+ if ( !( $data['warnings'] && $wgDevelopmentWarnings ) ) {
+ // If there were no warnings that were shown, cache it
+ $cache->set( $key, $data, 60 * 60 * 24 );
+ }
+ }
+ $this->queued = [];
+ }
+
+ /**
+ * Get the current load queue. Not intended to be used
+ * outside of the installer.
+ *
+ * @return array
+ */
+ public function getQueue() {
+ return $this->queued;
+ }
+
+ /**
+ * Clear the current load queue. Not intended to be used
+ * outside of the installer.
+ */
+ public function clearQueue() {
+ $this->queued = [];
+ }
+
+ /**
+ * After this is called, no more extensions can be loaded
+ *
+ * @since 1.29
+ */
+ public function finish() {
+ $this->finished = true;
+ }
+
+ /**
+ * Process a queue of extensions and return their extracted data
+ *
+ * @param array $queue keys are filenames, values are ignored
+ * @return array extracted info
+ * @throws Exception
+ * @throws ExtensionDependencyError
+ */
+ public function readFromQueue( array $queue ) {
+ global $wgVersion;
+ $autoloadClasses = [];
+ $autoloadNamespaces = [];
+ $autoloaderPaths = [];
+ $processor = new ExtensionProcessor();
+ $versionChecker = new VersionChecker( $wgVersion );
+ $extDependencies = [];
+ $incompatible = [];
+ $warnings = false;
+ foreach ( $queue as $path => $mtime ) {
+ $json = file_get_contents( $path );
+ if ( $json === false ) {
+ throw new Exception( "Unable to read $path, does it exist?" );
+ }
+ $info = json_decode( $json, /* $assoc = */ true );
+ if ( !is_array( $info ) ) {
+ throw new Exception( "$path is not a valid JSON file." );
+ }
+
+ if ( !isset( $info['manifest_version'] ) ) {
+ wfDeprecated(
+ "{$info['name']}'s extension.json or skin.json does not have manifest_version",
+ '1.29'
+ );
+ $warnings = true;
+ // For backwards-compatability, assume a version of 1
+ $info['manifest_version'] = 1;
+ }
+ $version = $info['manifest_version'];
+ if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
+ $incompatible[] = "$path: unsupported manifest_version: {$version}";
+ }
+
+ $dir = dirname( $path );
+ if ( isset( $info['AutoloadClasses'] ) ) {
+ $autoload = $this->processAutoLoader( $dir, $info['AutoloadClasses'] );
+ $GLOBALS['wgAutoloadClasses'] += $autoload;
+ $autoloadClasses += $autoload;
+ }
+ if ( isset( $info['AutoloadNamespaces'] ) ) {
+ $autoloadNamespaces += $this->processAutoLoader( $dir, $info['AutoloadNamespaces'] );
+ AutoLoader::$psr4Namespaces += $autoloadNamespaces;
+ }
+
+ // get all requirements/dependencies for this extension
+ $requires = $processor->getRequirements( $info );
+
+ // validate the information needed and add the requirements
+ if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
+ $extDependencies[$info['name']] = $requires;
+ }
+
+ // Get extra paths for later inclusion
+ $autoloaderPaths = array_merge( $autoloaderPaths,
+ $processor->getExtraAutoloaderPaths( $dir, $info ) );
+ // Compatible, read and extract info
+ $processor->extractInfo( $path, $info, $version );
+ }
+ $data = $processor->getExtractedInfo();
+ $data['warnings'] = $warnings;
+
+ // check for incompatible extensions
+ $incompatible = array_merge(
+ $incompatible,
+ $versionChecker
+ ->setLoadedExtensionsAndSkins( $data['credits'] )
+ ->checkArray( $extDependencies )
+ );
+
+ if ( $incompatible ) {
+ throw new ExtensionDependencyError( $incompatible );
+ }
+
+ // Need to set this so we can += to it later
+ $data['globals']['wgAutoloadClasses'] = [];
+ $data['autoload'] = $autoloadClasses;
+ $data['autoloaderPaths'] = $autoloaderPaths;
+ $data['autoloaderNS'] = $autoloadNamespaces;
+ return $data;
+ }
+
+ protected function exportExtractedData( array $info ) {
+ foreach ( $info['globals'] as $key => $val ) {
+ // If a merge strategy is set, read it and remove it from the value
+ // so it doesn't accidentally end up getting set.
+ if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
+ $mergeStrategy = $val[self::MERGE_STRATEGY];
+ unset( $val[self::MERGE_STRATEGY] );
+ } else {
+ $mergeStrategy = 'array_merge';
+ }
+
+ // Optimistic: If the global is not set, or is an empty array, replace it entirely.
+ // Will be O(1) performance.
+ if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
+ $GLOBALS[$key] = $val;
+ continue;
+ }
+
+ if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
+ // config setting that has already been overridden, don't set it
+ continue;
+ }
+
+ switch ( $mergeStrategy ) {
+ case 'array_merge_recursive':
+ $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
+ break;
+ case 'array_replace_recursive':
+ $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
+ break;
+ case 'array_plus_2d':
+ $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
+ break;
+ case 'array_plus':
+ $GLOBALS[$key] += $val;
+ break;
+ case 'array_merge':
+ $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
+ break;
+ default:
+ throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
+ }
+ }
+
+ if ( isset( $info['autoloaderNS'] ) ) {
+ AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
+ }
+
+ foreach ( $info['defines'] as $name => $val ) {
+ define( $name, $val );
+ }
+ foreach ( $info['autoloaderPaths'] as $path ) {
+ if ( file_exists( $path ) ) {
+ require_once $path;
+ }
+ }
+
+ $this->loaded += $info['credits'];
+ if ( $info['attributes'] ) {
+ if ( !$this->attributes ) {
+ $this->attributes = $info['attributes'];
+ } else {
+ $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
+ }
+ }
+
+ foreach ( $info['callbacks'] as $name => $cb ) {
+ if ( !is_callable( $cb ) ) {
+ if ( is_array( $cb ) ) {
+ $cb = '[ ' . implode( ', ', $cb ) . ' ]';
+ }
+ throw new UnexpectedValueException( "callback '$cb' is not callable" );
+ }
+ call_user_func( $cb, $info['credits'][$name] );
+ }
+ }
+
+ /**
+ * Loads and processes the given JSON file without delay
+ *
+ * If some extensions are already queued, this will load
+ * those as well.
+ *
+ * @param string $path Absolute path to the JSON file
+ */
+ public function load( $path ) {
+ $this->loadFromQueue(); // First clear the queue
+ $this->queue( $path );
+ $this->loadFromQueue();
+ }
+
+ /**
+ * Whether a thing has been loaded
+ * @param string $name
+ * @return bool
+ */
+ public function isLoaded( $name ) {
+ return isset( $this->loaded[$name] );
+ }
+
+ /**
+ * @param string $name
+ * @return array
+ */
+ public function getAttribute( $name ) {
+ if ( isset( $this->attributes[$name] ) ) {
+ return $this->attributes[$name];
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Get information about all things
+ *
+ * @return array
+ */
+ public function getAllThings() {
+ return $this->loaded;
+ }
+
+ /**
+ * Fully expand autoloader paths
+ *
+ * @param string $dir
+ * @param array $files
+ * @return array
+ */
+ protected function processAutoLoader( $dir, array $files ) {
+ // Make paths absolute, relative to the JSON file
+ foreach ( $files as &$file ) {
+ $file = "$dir/$file";
+ }
+ return $files;
+ }
+}
diff --git a/www/wiki/includes/registration/Processor.php b/www/wiki/includes/registration/Processor.php
new file mode 100644
index 00000000..210deb1b
--- /dev/null
+++ b/www/wiki/includes/registration/Processor.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * Processors read associated arrays and register
+ * whatever is required
+ *
+ * @since 1.25
+ */
+interface Processor {
+
+ /**
+ * Main entry point, processes the information
+ * provided.
+ * Callers should call "callback" after calling
+ * this function.
+ *
+ * @param string $path Absolute path of JSON file
+ * @param array $info
+ * @param int $version manifest_version for info
+ * @return array "credits" information to store
+ */
+ public function extractInfo( $path, array $info, $version );
+
+ /**
+ * @return array With following keys:
+ * 'globals' - variables to be set to $GLOBALS
+ * 'defines' - constants to define
+ * 'callbacks' - functions to be executed by the registry
+ * 'credits' - metadata to be stored by registry
+ * 'attributes' - registration info which isn't a global variable
+ */
+ public function getExtractedInfo();
+
+ /**
+ * Get the requirements for the provided info
+ *
+ * @since 1.26
+ * @param array $info
+ * @return array Where keys are the name to have a constraint on,
+ * like 'MediaWiki'. Values are a constraint string like "1.26.1".
+ */
+ public function getRequirements( array $info );
+
+ /**
+ * Get the path for additional autoloaders, e.g. the one of Composer.
+ *
+ * @param string $dir
+ * @param array $info
+ * @return array Containing the paths for autoloader file(s).
+ * @since 1.27
+ */
+ public function getExtraAutoloaderPaths( $dir, array $info );
+}
diff --git a/www/wiki/includes/registration/VersionChecker.php b/www/wiki/includes/registration/VersionChecker.php
new file mode 100644
index 00000000..59853b42
--- /dev/null
+++ b/www/wiki/includes/registration/VersionChecker.php
@@ -0,0 +1,236 @@
+<?php
+
+/**
+ * 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
+ *
+ * @author Legoktm
+ * @author Florian Schmidt
+ */
+
+use Composer\Semver\VersionParser;
+use Composer\Semver\Constraint\Constraint;
+
+/**
+ * Provides functions to check a set of extensions with dependencies against
+ * a set of loaded extensions and given version information.
+ *
+ * @since 1.29
+ */
+class VersionChecker {
+ /**
+ * @var Constraint|bool representing $wgVersion
+ */
+ private $coreVersion = false;
+
+ /**
+ * @var array Loaded extensions
+ */
+ private $loaded = [];
+
+ /**
+ * @var VersionParser
+ */
+ private $versionParser;
+
+ /**
+ * @param string $coreVersion Current version of core
+ */
+ public function __construct( $coreVersion ) {
+ $this->versionParser = new VersionParser();
+ $this->setCoreVersion( $coreVersion );
+ }
+
+ /**
+ * Set an array with credits of all loaded extensions and skins.
+ *
+ * @param array $credits An array of installed extensions with credits of them
+ * @return VersionChecker $this
+ */
+ public function setLoadedExtensionsAndSkins( array $credits ) {
+ $this->loaded = $credits;
+
+ return $this;
+ }
+
+ /**
+ * Set MediaWiki core version.
+ *
+ * @param string $coreVersion Current version of core
+ */
+ private function setCoreVersion( $coreVersion ) {
+ try {
+ $this->coreVersion = new Constraint(
+ '==',
+ $this->versionParser->normalize( $coreVersion )
+ );
+ $this->coreVersion->setPrettyString( $coreVersion );
+ } catch ( UnexpectedValueException $e ) {
+ // Non-parsable version, don't fatal.
+ }
+ }
+
+ /**
+ * Check all given dependencies if they are compatible with the named
+ * installed extensions in the $credits array.
+ *
+ * Example $extDependencies:
+ * {
+ * 'FooBar' => {
+ * 'MediaWiki' => '>= 1.25.0',
+ * 'extensions' => {
+ * 'FooBaz' => '>= 1.25.0'
+ * },
+ * 'skins' => {
+ * 'BazBar' => '>= 1.0.0'
+ * }
+ * }
+ * }
+ *
+ * @param array $extDependencies All extensions that depend on other ones
+ * @return array
+ */
+ public function checkArray( array $extDependencies ) {
+ $errors = [];
+ foreach ( $extDependencies as $extension => $dependencies ) {
+ foreach ( $dependencies as $dependencyType => $values ) {
+ switch ( $dependencyType ) {
+ case ExtensionRegistry::MEDIAWIKI_CORE:
+ $mwError = $this->handleMediaWikiDependency( $values, $extension );
+ if ( $mwError !== false ) {
+ $errors[] = [
+ 'msg' => $mwError,
+ 'type' => 'incompatible-core',
+ ];
+ }
+ break;
+ case 'extensions':
+ case 'skins':
+ foreach ( $values as $dependency => $constraint ) {
+ $extError = $this->handleExtensionDependency(
+ $dependency, $constraint, $extension, $dependencyType
+ );
+ if ( $extError !== false ) {
+ $errors[] = $extError;
+ }
+ }
+ break;
+ default:
+ throw new UnexpectedValueException( 'Dependency type ' . $dependencyType .
+ ' unknown in ' . $extension );
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Handle a dependency to MediaWiki core. It will check, if a MediaWiki version constraint was
+ * set with self::setCoreVersion before this call (if not, it will return an empty array) and
+ * checks the version constraint given against it.
+ *
+ * @param string $constraint The required version constraint for this dependency
+ * @param string $checkedExt The Extension, which depends on this dependency
+ * @return bool|string false if no error, or a string with the message
+ */
+ private function handleMediaWikiDependency( $constraint, $checkedExt ) {
+ if ( $this->coreVersion === false ) {
+ // Couldn't parse the core version, so we can't check anything
+ return false;
+ }
+
+ // if the installed and required version are compatible, return an empty array
+ if ( $this->versionParser->parseConstraints( $constraint )
+ ->matches( $this->coreVersion ) ) {
+ return false;
+ }
+ // otherwise mark this as incompatible.
+ return "{$checkedExt} is not compatible with the current "
+ . "MediaWiki core (version {$this->coreVersion->getPrettyString()}), it requires: "
+ . "$constraint.";
+ }
+
+ /**
+ * Handle a dependency to another extension.
+ *
+ * @param string $dependencyName The name of the dependency
+ * @param string $constraint The required version constraint for this dependency
+ * @param string $checkedExt The Extension, which depends on this dependency
+ * @param string $type Either 'extensions' or 'skins'
+ * @return bool|array false for no errors, or an array of info
+ */
+ private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt,
+ $type
+ ) {
+ // Check if the dependency is even installed
+ if ( !isset( $this->loaded[$dependencyName] ) ) {
+ return [
+ 'msg' => "{$checkedExt} requires {$dependencyName} to be installed.",
+ 'type' => "missing-$type",
+ 'missing' => $dependencyName,
+ ];
+ }
+ // Check if the dependency has specified a version
+ if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
+ // If we depend upon any version, and none is set, that's fine.
+ if ( $constraint === '*' ) {
+ wfDebug( "{$dependencyName} does not expose its version, but {$checkedExt}"
+ . " mentions it with constraint '*'. Assume it's ok so." );
+ return false;
+ } else {
+ // Otherwise, mark it as incompatible.
+ $msg = "{$dependencyName} does not expose its version, but {$checkedExt}"
+ . " requires: {$constraint}.";
+ return [
+ 'msg' => $msg,
+ 'type' => "incompatible-$type",
+ 'incompatible' => $checkedExt,
+ ];
+ }
+ } else {
+ // Try to get a constraint for the dependency version
+ try {
+ $installedVersion = new Constraint(
+ '==',
+ $this->versionParser->normalize( $this->loaded[$dependencyName]['version'] )
+ );
+ } catch ( UnexpectedValueException $e ) {
+ // Non-parsable version, output an error message that the version
+ // string is invalid
+ return [
+ 'msg' => "$dependencyName does not have a valid version string.",
+ 'type' => 'invalid-version',
+ ];
+ }
+ // Check if the constraint actually matches...
+ if (
+ !$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
+ ) {
+ $msg = "{$checkedExt} is not compatible with the current "
+ . "installed version of {$dependencyName} "
+ . "({$this->loaded[$dependencyName]['version']}), "
+ . "it requires: " . $constraint . '.';
+ return [
+ 'msg' => $msg,
+ 'type' => "incompatible-$type",
+ 'incompatible' => $checkedExt,
+ ];
+ }
+ }
+
+ return false;
+ }
+}