19 protected static $globalSettings = [
20 MainConfigNames::ActionFilteredLogs,
21 MainConfigNames::Actions,
22 MainConfigNames::AddGroups,
23 MainConfigNames::APIFormatModules,
24 MainConfigNames::APIListModules,
25 MainConfigNames::APIMetaModules,
26 MainConfigNames::APIModules,
27 MainConfigNames::APIPropModules,
28 MainConfigNames::AuthManagerAutoConfig,
29 MainConfigNames::AvailableRights,
30 MainConfigNames::CentralIdLookupProviders,
31 MainConfigNames::ChangeCredentialsBlacklist,
32 MainConfigNames::ConditionalUserOptions,
33 MainConfigNames::ConfigRegistry,
34 MainConfigNames::ContentHandlers,
35 MainConfigNames::DefaultUserOptions,
36 MainConfigNames::ExtensionEntryPointListFiles,
37 MainConfigNames::ExtensionFunctions,
38 MainConfigNames::FeedClasses,
39 MainConfigNames::FileExtensions,
40 MainConfigNames::FilterLogTypes,
41 MainConfigNames::GrantPermissionGroups,
42 MainConfigNames::GrantPermissions,
43 MainConfigNames::GrantRiskGroups,
44 MainConfigNames::GroupPermissions,
45 MainConfigNames::GroupsAddToSelf,
46 MainConfigNames::GroupsRemoveFromSelf,
47 MainConfigNames::HiddenPrefs,
48 MainConfigNames::ImplicitGroups,
49 MainConfigNames::JobClasses,
50 MainConfigNames::LogActions,
51 MainConfigNames::LogActionsHandlers,
52 MainConfigNames::LogHeaders,
53 MainConfigNames::LogNames,
54 MainConfigNames::LogRestrictions,
55 MainConfigNames::LogTypes,
56 MainConfigNames::MediaHandlers,
57 MainConfigNames::PasswordPolicy,
58 MainConfigNames::PrivilegedGroups,
59 MainConfigNames::RateLimits,
60 MainConfigNames::RawHtmlMessages,
61 MainConfigNames::ReauthenticateTime,
62 MainConfigNames::RecentChangesFlags,
63 MainConfigNames::RemoveCredentialsBlacklist,
64 MainConfigNames::RemoveGroups,
65 MainConfigNames::ResourceLoaderSources,
66 MainConfigNames::RevokePermissions,
67 MainConfigNames::SessionProviders,
68 MainConfigNames::SpecialPages,
69 MainConfigNames::UserRegistrationProviders,
77 protected const CORE_ATTRIBS = [
84 'LateJSConfigVarNames',
85 'TempUserSerialProviders',
86 'TempUserSerialMappings',
87 'DatabaseVirtualDomains',
97 protected const MERGE_STRATEGIES = [
98 'wgAuthManagerAutoConfig' =>
'array_plus_2d',
99 'wgCapitalLinkOverrides' =>
'array_plus',
100 'wgExtraGenderNamespaces' =>
'array_plus',
101 'wgGrantPermissions' =>
'array_plus_2d',
102 'wgGroupPermissions' =>
'array_plus_2d',
103 'wgHooks' =>
'array_merge_recursive',
104 'wgNamespaceContentModels' =>
'array_plus',
105 'wgNamespaceProtection' =>
'array_plus',
106 'wgNamespacesWithSubpages' =>
'array_plus',
107 'wgPasswordPolicy' =>
'array_merge_recursive',
108 'wgRateLimits' =>
'array_plus_2d',
109 'wgRevokePermissions' =>
'array_plus_2d',
117 protected const CREDIT_ATTRIBS = [
135 protected const NOT_ATTRIBS = [
139 'load_composer_autoloader',
144 'AutoloadNamespaces',
145 'ExtensionMessagesFiles',
146 'TranslationAliasesDirs',
147 'ForeignResourcesDir',
149 'MessagePosterModule',
153 'ResourceFileModulePaths',
154 'ResourceModuleSkinStyles',
156 'ServiceWiringFiles',
166 protected $globals = [
167 'wgExtensionMessagesFiles' => [],
168 'wgMessagesDirs' => [],
169 'TranslationAliasesDirs' => [],
177 protected $defines = [];
186 protected $callbacks = [];
191 protected $credits = [];
200 protected $autoload = [
212 protected $autoloadDev = [
224 protected $attributes = [];
232 protected $extAttributes = [];
242 $json = file_get_contents(
$path );
243 $info = json_decode( $json,
true );
246 throw new RuntimeException(
"Failed to load JSON data from $path" );
258 $dir = dirname(
$path );
268 if ( isset( $info[
'ServiceWiringFiles'] ) ) {
270 'wgServiceWiringFiles',
272 $info[
'ServiceWiringFiles']
276 if ( isset( $info[
'callback'] ) ) {
277 $this->callbacks[$name] = $info[
'callback'];
280 $this->extractAutoload( $info, $dir );
284 if ( $version >= 2 ) {
292 if ( isset( $info[
'ParsoidModules'] ) ) {
293 foreach ( $info[
'ParsoidModules'] as &$module ) {
294 if ( is_string( $module ) ) {
295 $className = $module;
297 'class' => $className,
300 $module[
'name'] = $name;
306 if ( $version >= 2 ) {
310 foreach ( $info as $key => $val ) {
312 if ( in_array( $key, self::$globalSettings ) ) {
317 if ( $key[0] ===
'@' ) {
321 if ( $version >= 2 ) {
323 if ( in_array( $key, self::CORE_ATTRIBS ) ) {
328 if ( !in_array( $key, self::NOT_ATTRIBS )
329 && !in_array( $key, self::CREDIT_ATTRIBS )
343 if ( isset( $info[
'attributes'] ) ) {
344 foreach ( $info[
'attributes'] as $extName => $value ) {
352 foreach ( $this->globals as $key => $val ) {
353 if ( isset( self::MERGE_STRATEGIES[$key] ) ) {
354 $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::MERGE_STRATEGIES[$key];
359 foreach ( $this->extAttributes as $extName => $value ) {
361 if ( isset( $this->credits[$extName] ) ) {
362 foreach ( $value as $attrName => $attrValue ) {
365 $extName . $attrName,
370 unset( $this->extAttributes[$extName] );
376 'globals' => $this->globals,
377 'defines' => $this->defines,
378 'callbacks' => $this->callbacks,
379 'credits' => $this->credits,
380 'attributes' => $this->attributes,
381 'autoloaderPaths' => $autoload[
'files'],
382 'autoloaderClasses' => $autoload[
'classes'],
383 'autoloaderNS' => $autoload[
'namespaces'],
389 if ( !$includeDev || !isset( $info[
'dev-requires'] ) ) {
390 return $info[
'requires'] ?? [];
393 if ( !isset( $info[
'requires'] ) ) {
394 return $info[
'dev-requires'] ?? [];
404 $pick =
static function ( $a, $b ) {
407 } elseif ( $b ===
null ) {
414 $req = $info[
'requires'];
415 $dev = $info[
'dev-requires'];
416 if ( isset( $req[
'MediaWiki'] ) || isset( $dev[
'MediaWiki'] ) ) {
417 $merged[
'MediaWiki'] = $pick(
418 $req[
'MediaWiki'] ??
null,
419 $dev[
'MediaWiki'] ??
null
423 $platform = array_merge(
424 array_keys( $req[
'platform'] ?? [] ),
425 array_keys( $dev[
'platform'] ?? [] )
428 foreach ( $platform as $pkey ) {
429 if ( $pkey ===
'php' ) {
431 $req[
'platform'][
'php'] ??
null,
432 $dev[
'platform'][
'php'] ??
null
437 $value = $dev[
'platform'][$pkey] ?? $req[
'platform'][$pkey];
439 $merged[
'platform'][$pkey] = $value;
443 foreach ( [
'extensions',
'skins' ] as $thing ) {
444 $things = array_merge(
445 array_keys( $req[$thing] ?? [] ),
446 array_keys( $dev[$thing] ?? [] )
448 foreach ( $things as $name ) {
449 $merged[$thing][$name] = $pick(
450 $req[$thing][$name] ??
null,
451 $dev[$thing][$name] ??
null
469 private function setArrayHookHandler(
471 array $hookHandlersAttr,
475 if ( isset( $callback[
'handler'] ) ) {
476 $handlerName = $callback[
'handler'];
477 $handlerDefinition = $hookHandlersAttr[$handlerName] ??
false;
478 if ( !$handlerDefinition ) {
479 throw new UnexpectedValueException(
480 "Missing handler definition for $name in HookHandlers attribute in $path"
483 $callback[
'handler'] = $handlerDefinition;
484 $callback[
'extensionPath'] =
$path;
485 $this->attributes[
'Hooks'][$name][] = $callback;
487 foreach ( $callback as $callable ) {
488 if ( is_array( $callable ) ) {
489 if ( isset( $callable[
'handler'] ) ) {
490 $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name,
$path );
492 $this->globals[
'wgHooks'][$name][] = $callable;
494 } elseif ( is_string( $callable ) ) {
495 $this->setStringHookHandler( $callable, $hookHandlersAttr, $name,
$path );
511 private function setStringHookHandler(
513 array $hookHandlersAttr,
517 if ( isset( $hookHandlersAttr[$callback] ) ) {
519 'handler' => $hookHandlersAttr[$callback],
520 'extensionPath' =>
$path
522 $this->attributes[
'Hooks'][$name][] = $handler;
524 $this->globals[
'wgHooks'][$name][] = $callback;
537 $extName = $info[
'name'];
538 if ( isset( $info[
'Hooks'] ) ) {
539 $hookHandlersAttr = [];
540 foreach ( $info[
'HookHandlers'] ?? [] as $name => $def ) {
541 $hookHandlersAttr[$name] = [
'name' =>
"$extName-$name" ] + $def;
543 foreach ( $info[
'Hooks'] as $name => $callback ) {
544 if ( is_string( $callback ) ) {
545 $this->setStringHookHandler( $callback, $hookHandlersAttr, $name,
$path );
546 } elseif ( is_array( $callback ) ) {
547 $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name,
$path );
551 if ( isset( $info[
'DeprecatedHooks'] ) ) {
552 $deprecatedHooks = [];
553 foreach ( $info[
'DeprecatedHooks'] as $name => $deprecatedHookInfo ) {
554 $deprecatedHookInfo += [
'component' => $extName ];
555 $deprecatedHooks[$name] = $deprecatedHookInfo;
557 if ( isset( $this->attributes[
'DeprecatedHooks'] ) ) {
558 $this->attributes[
'DeprecatedHooks'] += $deprecatedHooks;
560 $this->attributes[
'DeprecatedHooks'] = $deprecatedHooks;
571 if ( isset( $info[
'namespaces'] ) ) {
572 foreach ( $info[
'namespaces'] as $ns ) {
573 if ( defined( $ns[
'constant'] ) ) {
576 $id = constant( $ns[
'constant'] );
580 $this->defines[ $ns[
'constant'] ] = $id;
582 if ( !( isset( $ns[
'conditional'] ) && $ns[
'conditional'] ) ) {
584 $this->attributes[
'ExtensionNamespaces'][$id] = $ns[
'name'];
586 if ( isset( $ns[
'movable'] ) && !$ns[
'movable'] ) {
587 $this->attributes[
'ImmovableNamespaces'][] = $id;
589 if ( isset( $ns[
'gender'] ) ) {
590 $this->globals[
'wgExtraGenderNamespaces'][$id] = $ns[
'gender'];
592 if ( isset( $ns[
'subpages'] ) && $ns[
'subpages'] ) {
593 $this->globals[
'wgNamespacesWithSubpages'][$id] =
true;
595 if ( isset( $ns[
'content'] ) && $ns[
'content'] ) {
596 $this->globals[
'wgContentNamespaces'][] = $id;
598 if ( isset( $ns[
'defaultcontentmodel'] ) ) {
599 $this->globals[
'wgNamespaceContentModels'][$id] = $ns[
'defaultcontentmodel'];
601 if ( isset( $ns[
'protection'] ) ) {
602 $this->globals[
'wgNamespaceProtection'][$id] = $ns[
'protection'];
604 if ( isset( $ns[
'capitallinkoverride'] ) ) {
605 $this->globals[
'wgCapitalLinkOverrides'][$id] = $ns[
'capitallinkoverride'];
607 if ( isset( $ns[
'includable'] ) && !$ns[
'includable'] ) {
608 $this->globals[
'wgNonincludableNamespaces'][] = $id;
615 $defaultPaths = $info[
'ResourceFileModulePaths'] ??
false;
616 if ( isset( $defaultPaths[
'localBasePath'] ) ) {
617 if ( $defaultPaths[
'localBasePath'] ===
'' ) {
619 $defaultPaths[
'localBasePath'] = $dir;
621 $defaultPaths[
'localBasePath'] =
"$dir/{$defaultPaths['localBasePath']}";
625 foreach ( [
'ResourceModules',
'ResourceModuleSkinStyles',
'OOUIThemePaths' ] as $setting ) {
626 if ( isset( $info[$setting] ) ) {
627 foreach ( $info[$setting] as $name => $data ) {
628 if ( isset( $data[
'localBasePath'] ) ) {
629 if ( $data[
'localBasePath'] ===
'' ) {
631 $data[
'localBasePath'] = $dir;
633 $data[
'localBasePath'] =
"$dir/{$data['localBasePath']}";
636 if ( $defaultPaths ) {
637 $data += $defaultPaths;
639 $this->attributes[$setting][$name] = $data;
644 if ( isset( $info[
'QUnitTestModule'] ) ) {
645 $data = $info[
'QUnitTestModule'];
646 if ( isset( $data[
'localBasePath'] ) ) {
647 if ( $data[
'localBasePath'] ===
'' ) {
649 $data[
'localBasePath'] = $dir;
651 $data[
'localBasePath'] =
"$dir/{$data['localBasePath']}";
654 $this->attributes[
'QUnitTestModules'][
"test.{$info['name']}"] = $data;
657 if ( isset( $info[
'MessagePosterModule'] ) ) {
658 $data = $info[
'MessagePosterModule'];
659 $basePath = $data[
'localBasePath'] ??
'';
660 $baseDir = $basePath ===
'' ? $dir :
"$dir/$basePath";
661 foreach ( $data[
'scripts'] ?? [] as $scripts ) {
662 $this->attributes[
'MessagePosterModule'][
'scripts'][] =
665 foreach ( $data[
'dependencies'] ?? [] as $dependency ) {
666 $this->attributes[
'MessagePosterModule'][
'dependencies'][] = $dependency;
672 if ( isset( $info[
'ExtensionMessagesFiles'] ) ) {
673 foreach ( $info[
'ExtensionMessagesFiles'] as &$file ) {
674 $file =
"$dir/$file";
676 $this->globals[
"wgExtensionMessagesFiles"] += $info[
'ExtensionMessagesFiles'];
688 if ( isset( $info[
'MessagesDirs'] ) ) {
689 foreach ( $info[
'MessagesDirs'] as $name => $files ) {
690 foreach ( (array)$files as $file ) {
691 $this->globals[
"wgMessagesDirs"][$name][] =
"$dir/$file";
705 foreach ( $info[
'TranslationAliasesDirs'] ?? [] as $name => $files ) {
706 foreach ( (array)$files as $file ) {
707 $this->globals[
'wgTranslationAliasesDirs'][$name][] =
"$dir/$file";
719 if ( isset( $info[
'ValidSkinNames'] ) ) {
720 foreach ( $info[
'ValidSkinNames'] as $skinKey => $data ) {
721 if ( isset( $data[
'args'][0] ) ) {
722 $templateDirectory = $data[
'args'][0][
'templateDirectory'] ??
'templates';
723 $data[
'args'][0][
'templateDirectory'] = $dir .
'/' . $templateDirectory;
725 $this->globals[
'wgValidSkinNames'][$skinKey] = $data;
740 if ( isset( $info[
'RateLimits'] ) ) {
741 $rights = array_keys( $info[
'RateLimits'] );
743 if ( isset( $info[
'AvailableRights'] ) ) {
744 $rights = array_diff( $rights, $info[
'AvailableRights'] );
747 $this->globals[
'wgImplicitRights'] = array_merge(
748 $this->globals[
'wgImplicitRights'] ?? [],
759 if ( isset( $info[
'SkinLessImportPaths'] ) ) {
760 foreach ( $info[
'SkinLessImportPaths'] as $skin => $subpath ) {
761 $this->attributes[
'SkinLessImportPaths'][$skin] =
"$dir/$subpath";
777 foreach ( self::CREDIT_ATTRIBS as $attr ) {
778 if ( isset( $info[$attr] ) ) {
779 $credits[$attr] = $info[$attr];
783 $name = $credits[
'name'];
787 if ( isset( $this->credits[$name] ) ) {
788 $firstPath = $this->credits[$name][
'path'];
789 $secondPath = $credits[
'path'];
790 throw new InvalidArgumentException(
791 "It was attempted to load $name twice, from $firstPath and $secondPath."
795 $this->credits[$name] = $credits;
801 if ( array_key_exists(
'ForeignResourcesDir', $info ) ) {
802 if ( !is_string( $info[
'ForeignResourcesDir'] ) ) {
803 throw new InvalidArgumentException(
"Incorrect ForeignResourcesDir type, must be a string (in $name)" );
805 $this->attributes[
'ForeignResourcesDir'][$name] =
"{$dir}/{$info['ForeignResourcesDir']}";
816 if ( isset( $info[
'config'] ) ) {
817 if ( isset( $info[
'config'][
'_prefix'] ) ) {
818 $prefix = $info[
'config'][
'_prefix'];
819 unset( $info[
'config'][
'_prefix'] );
823 foreach ( $info[
'config'] as $key => $val ) {
824 if ( $key[0] !==
'@' ) {
825 $this->addConfigGlobal(
"$prefix$key", $val, $info[
'name'] );
839 private function applyPath( array $value,
string $dir ): array {
842 foreach ( $value as $k => $v ) {
843 $result[$k] = $dir .
'/' . $v;
856 $prefix = $info[
'config_prefix'] ??
'wg';
857 if ( isset( $info[
'config'] ) ) {
858 foreach ( $info[
'config'] as $key => $data ) {
859 if ( !array_key_exists(
'value', $data ) ) {
860 throw new UnexpectedValueException(
"Missing value for config $key" );
863 $value = $data[
'value'];
864 if ( isset( $data[
'path'] ) && $data[
'path'] ) {
865 if ( is_array( $value ) ) {
866 $value = $this->applyPath( $value, $dir );
868 $value =
"$dir/$value";
871 if ( isset( $data[
'merge_strategy'] ) ) {
872 $value[ExtensionRegistry::MERGE_STRATEGY] = $data[
'merge_strategy'];
874 $this->addConfigGlobal(
"$prefix$key", $value, $info[
'name'] );
875 $data[
'providedby'] = $info[
'name'];
876 if ( isset( $info[
'ConfigRegistry'][0] ) ) {
877 $data[
'configregistry'] = array_keys( $info[
'ConfigRegistry'] )[0];
890 private function addConfigGlobal( $key, $value, $extName ) {
891 if ( array_key_exists( $key, $this->globals ) ) {
892 throw new RuntimeException(
893 "The configuration setting '$key' was already set by MediaWiki core or"
894 .
" another extension, and cannot be set again by $extName." );
896 if ( isset( $value[ExtensionRegistry::MERGE_STRATEGY] ) &&
897 $value[ExtensionRegistry::MERGE_STRATEGY] ===
'array_merge_recursive' ) {
899 "Using the array_merge_recursive merge strategy in extension.json and skin.json" .
900 " was deprecated in MediaWiki 1.42",
904 $this->globals[$key] = $value;
908 foreach ( $paths as
$path ) {
909 $this->globals[$global][] =
"$dir/$path";
923 if ( !is_array( $value ) ) {
924 throw new InvalidArgumentException(
"The value for '$name' should be an array (from $path)" );
926 if ( isset( $array[$name] ) ) {
927 $array[$name] = array_merge_recursive( $array[$name], $value );
929 $array[$name] = $value;
943 if ( !is_array( $value ) ) {
944 throw new InvalidArgumentException(
"The value for '$name' should be an array (from $path)" );
946 if ( isset( $array[$name] ) ) {
947 $array[$name] = array_merge( $array[$name], $value );
949 $array[$name] = $value;
968 $autoload = $this->autoload;
971 $autoload[
'classes'] += $this->autoloadDev[
'classes'];
972 $autoload[
'namespaces'] += $this->autoloadDev[
'namespaces'];
977 if ( !empty( $this->autoloadDev[
'files'] ) ) {
980 $autoload[
'files'] = array_merge(
982 $this->autoloadDev[
'files']
994 private function extractAutoload( array $info,
string $dir ) {
995 if ( isset( $info[
'load_composer_autoloader'] ) && $info[
'load_composer_autoloader'] ===
true ) {
996 $file =
"$dir/vendor/autoload.php";
997 if ( file_exists( $file ) ) {
998 $this->autoload[
'files'][] = $file;
1002 if ( isset( $info[
'AutoloadClasses'] ) ) {
1003 $paths = $this->applyPath( $info[
'AutoloadClasses'], $dir );
1004 $this->autoload[
'classes'] += $paths;
1007 if ( isset( $info[
'AutoloadNamespaces'] ) ) {
1008 $paths = $this->applyPath( $info[
'AutoloadNamespaces'], $dir );
1009 $this->autoload[
'namespaces'] += $paths;
1012 if ( isset( $info[
'TestAutoloadClasses'] ) ) {
1013 $paths = $this->applyPath( $info[
'TestAutoloadClasses'], $dir );
1014 $this->autoloadDev[
'classes'] += $paths;
1017 if ( isset( $info[
'TestAutoloadNamespaces'] ) ) {
1018 $paths = $this->applyPath( $info[
'TestAutoloadNamespaces'], $dir );
1019 $this->autoloadDev[
'namespaces'] += $paths;