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::ConfigRegistry,
33 MainConfigNames::ContentHandlers,
34 MainConfigNames::DefaultUserOptions,
35 MainConfigNames::ExtensionEntryPointListFiles,
36 MainConfigNames::ExtensionFunctions,
37 MainConfigNames::FeedClasses,
38 MainConfigNames::FileExtensions,
39 MainConfigNames::FilterLogTypes,
40 MainConfigNames::GrantPermissionGroups,
41 MainConfigNames::GrantPermissions,
42 MainConfigNames::GroupPermissions,
43 MainConfigNames::GroupsAddToSelf,
44 MainConfigNames::GroupsRemoveFromSelf,
45 MainConfigNames::HiddenPrefs,
46 MainConfigNames::ImplicitGroups,
47 MainConfigNames::JobClasses,
48 MainConfigNames::LogActions,
49 MainConfigNames::LogActionsHandlers,
50 MainConfigNames::LogHeaders,
51 MainConfigNames::LogNames,
52 MainConfigNames::LogRestrictions,
53 MainConfigNames::LogTypes,
54 MainConfigNames::MediaHandlers,
55 MainConfigNames::PasswordPolicy,
56 MainConfigNames::PrivilegedGroups,
57 MainConfigNames::RateLimits,
58 MainConfigNames::RawHtmlMessages,
59 MainConfigNames::ReauthenticateTime,
60 MainConfigNames::RecentChangesFlags,
61 MainConfigNames::RemoveCredentialsBlacklist,
62 MainConfigNames::RemoveGroups,
63 MainConfigNames::ResourceLoaderSources,
64 MainConfigNames::RevokePermissions,
65 MainConfigNames::SessionProviders,
66 MainConfigNames::SpecialPages
74 protected const CORE_ATTRIBS = [
81 'LateJSConfigVarNames',
82 'TempUserSerialProviders',
83 'TempUserSerialMappings',
93 protected const MERGE_STRATEGIES = [
94 'wgAuthManagerAutoConfig' =>
'array_plus_2d',
95 'wgCapitalLinkOverrides' =>
'array_plus',
96 'wgExtraGenderNamespaces' =>
'array_plus',
97 'wgGrantPermissions' =>
'array_plus_2d',
98 'wgGroupPermissions' =>
'array_plus_2d',
99 'wgHooks' =>
'array_merge_recursive',
100 'wgNamespaceContentModels' =>
'array_plus',
101 'wgNamespaceProtection' =>
'array_plus',
102 'wgNamespacesWithSubpages' =>
'array_plus',
103 'wgPasswordPolicy' =>
'array_merge_recursive',
104 'wgRateLimits' =>
'array_plus_2d',
105 'wgRevokePermissions' =>
'array_plus_2d',
113 protected const CREDIT_ATTRIBS = [
131 protected const NOT_ATTRIBS = [
135 'load_composer_autoloader',
140 'AutoloadNamespaces',
141 'ExtensionMessagesFiles',
143 'MessagePosterModule',
147 'ResourceFileModulePaths',
148 'ResourceModuleSkinStyles',
150 'ServiceWiringFiles',
161 'wgExtensionMessagesFiles' => [],
162 'wgMessagesDirs' => [],
234 $json = file_get_contents(
$path );
235 $info = json_decode( $json,
true );
238 throw new RuntimeException(
"Failed to load JSON data from $path" );
241 if ( !isset( $info[
'manifest_version'] ) ) {
243 "{$info['name']}'s extension.json or skin.json does not have manifest_version, " .
244 'this is deprecated since MediaWiki 1.29',
247 $info[
'manifest_version'] = 1;
250 $this->
extractInfo( $path, $info, $info[
'manifest_version'] );
259 $dir = dirname(
$path );
267 if ( isset( $info[
'ServiceWiringFiles'] ) ) {
269 'wgServiceWiringFiles',
271 $info[
'ServiceWiringFiles']
275 if ( isset( $info[
'callback'] ) ) {
276 $this->callbacks[$name] = $info[
'callback'];
279 $this->extractAutoload( $info, $dir );
283 if ( $version >= 2 ) {
291 if ( isset( $info[
'ParsoidModules'] ) ) {
292 foreach ( $info[
'ParsoidModules'] as &$module ) {
293 if ( is_string( $module ) ) {
294 $className = $module;
296 'class' => $className,
299 $module[
'name'] = $name;
303 if ( $version >= 2 ) {
307 foreach ( $info as $key => $val ) {
309 if ( in_array( $key, self::$globalSettings ) ) {
314 if ( $key[0] ===
'@' ) {
318 if ( $version >= 2 ) {
320 if ( in_array( $key, self::CORE_ATTRIBS ) ) {
321 $this->
storeToArray( $path, $key, $val, $this->attributes );
325 if ( !in_array( $key, self::NOT_ATTRIBS )
326 && !in_array( $key, self::CREDIT_ATTRIBS )
340 if ( isset( $info[
'attributes'] ) ) {
341 foreach ( $info[
'attributes'] as $extName => $value ) {
349 foreach ( $this->globals as $key => $val ) {
350 if ( isset( self::MERGE_STRATEGIES[$key] ) ) {
351 $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::MERGE_STRATEGIES[$key];
356 foreach ( $this->extAttributes as $extName => $value ) {
358 if ( isset( $this->credits[$extName] ) ) {
359 foreach ( $value as $attrName => $attrValue ) {
362 $extName . $attrName,
367 unset( $this->extAttributes[$extName] );
379 'autoloaderClasses' =>
$autoload[
'classes'],
380 'autoloaderNS' =>
$autoload[
'namespaces'],
387 if ( !$includeDev || !isset( $info[
'dev-requires'] ) ) {
388 return $info[
'requires'] ?? [];
391 if ( !isset( $info[
'requires'] ) ) {
392 return $info[
'dev-requires'] ?? [];
402 $pick =
static function ( $a, $b ) {
405 } elseif ( $b ===
null ) {
412 $req = $info[
'requires'];
413 $dev = $info[
'dev-requires'];
414 if ( isset( $req[
'MediaWiki'] ) || isset( $dev[
'MediaWiki'] ) ) {
415 $merged[
'MediaWiki'] = $pick(
416 $req[
'MediaWiki'] ??
null,
417 $dev[
'MediaWiki'] ??
null
421 $platform = array_merge(
422 array_keys( $req[
'platform'] ?? [] ),
423 array_keys( $dev[
'platform'] ?? [] )
426 foreach ( $platform as $pkey ) {
427 if ( $pkey ===
'php' ) {
429 $req[
'platform'][
'php'] ??
null,
430 $dev[
'platform'][
'php'] ??
null
435 $value = $dev[
'platform'][$pkey] ?? $req[
'platform'][$pkey];
437 $merged[
'platform'][$pkey] = $value;
441 foreach ( [
'extensions',
'skins' ] as $thing ) {
442 $things = array_merge(
443 array_keys( $req[$thing] ?? [] ),
444 array_keys( $dev[$thing] ?? [] )
446 foreach ( $things as $name ) {
447 $merged[$thing][$name] = $pick(
448 $req[$thing][$name] ??
null,
449 $dev[$thing][$name] ??
null
467 private function setArrayHookHandler(
469 array $hookHandlersAttr,
473 if ( isset( $callback[
'handler'] ) ) {
474 $handlerName = $callback[
'handler'];
475 $handlerDefinition = $hookHandlersAttr[$handlerName] ??
false;
476 if ( !$handlerDefinition ) {
477 throw new UnexpectedValueException(
478 "Missing handler definition for $name in HookHandlers attribute in $path"
481 $callback[
'handler'] = $handlerDefinition;
482 $callback[
'extensionPath'] =
$path;
483 $this->attributes[
'Hooks'][$name][] = $callback;
485 foreach ( $callback as $callable ) {
486 if ( is_array( $callable ) ) {
487 if ( isset( $callable[
'handler'] ) ) {
488 $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name,
$path );
490 $this->globals[
'wgHooks'][$name][] = $callable;
492 } elseif ( is_string( $callable ) ) {
493 $this->setStringHookHandler( $callable, $hookHandlersAttr, $name,
$path );
509 private function setStringHookHandler(
511 array $hookHandlersAttr,
515 if ( isset( $hookHandlersAttr[$callback] ) ) {
517 'handler' => $hookHandlersAttr[$callback],
518 'extensionPath' =>
$path
520 $this->attributes[
'Hooks'][$name][] = $handler;
522 $this->globals[
'wgHooks'][$name][] = $callback;
535 $extName = $info[
'name'];
536 if ( isset( $info[
'Hooks'] ) ) {
537 $hookHandlersAttr = [];
538 foreach ( $info[
'HookHandlers'] ?? [] as $name => $def ) {
539 $hookHandlersAttr[$name] = [
'name' =>
"$extName-$name" ] + $def;
541 foreach ( $info[
'Hooks'] as $name => $callback ) {
542 if ( is_string( $callback ) ) {
543 $this->setStringHookHandler( $callback, $hookHandlersAttr, $name,
$path );
544 } elseif ( is_array( $callback ) ) {
545 $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name,
$path );
549 if ( isset( $info[
'DeprecatedHooks'] ) ) {
550 $deprecatedHooks = [];
551 foreach ( $info[
'DeprecatedHooks'] as $name => $deprecatedHookInfo ) {
552 $deprecatedHookInfo += [
'component' => $extName ];
553 $deprecatedHooks[$name] = $deprecatedHookInfo;
555 if ( isset( $this->attributes[
'DeprecatedHooks'] ) ) {
556 $this->attributes[
'DeprecatedHooks'] += $deprecatedHooks;
558 $this->attributes[
'DeprecatedHooks'] = $deprecatedHooks;
569 if ( isset( $info[
'namespaces'] ) ) {
570 foreach ( $info[
'namespaces'] as $ns ) {
571 if ( defined( $ns[
'constant'] ) ) {
574 $id = constant( $ns[
'constant'] );
578 $this->defines[ $ns[
'constant'] ] = $id;
580 if ( !( isset( $ns[
'conditional'] ) && $ns[
'conditional'] ) ) {
582 $this->attributes[
'ExtensionNamespaces'][$id] = $ns[
'name'];
584 if ( isset( $ns[
'movable'] ) && !$ns[
'movable'] ) {
585 $this->attributes[
'ImmovableNamespaces'][] = $id;
587 if ( isset( $ns[
'gender'] ) ) {
588 $this->globals[
'wgExtraGenderNamespaces'][$id] = $ns[
'gender'];
590 if ( isset( $ns[
'subpages'] ) && $ns[
'subpages'] ) {
591 $this->globals[
'wgNamespacesWithSubpages'][$id] =
true;
593 if ( isset( $ns[
'content'] ) && $ns[
'content'] ) {
594 $this->globals[
'wgContentNamespaces'][] = $id;
596 if ( isset( $ns[
'defaultcontentmodel'] ) ) {
597 $this->globals[
'wgNamespaceContentModels'][$id] = $ns[
'defaultcontentmodel'];
599 if ( isset( $ns[
'protection'] ) ) {
600 $this->globals[
'wgNamespaceProtection'][$id] = $ns[
'protection'];
602 if ( isset( $ns[
'capitallinkoverride'] ) ) {
603 $this->globals[
'wgCapitalLinkOverrides'][$id] = $ns[
'capitallinkoverride'];
605 if ( isset( $ns[
'includable'] ) && !$ns[
'includable'] ) {
606 $this->globals[
'wgNonincludableNamespaces'][] = $id;
613 $defaultPaths = $info[
'ResourceFileModulePaths'] ??
false;
614 if ( isset( $defaultPaths[
'localBasePath'] ) ) {
615 if ( $defaultPaths[
'localBasePath'] ===
'' ) {
617 $defaultPaths[
'localBasePath'] = $dir;
619 $defaultPaths[
'localBasePath'] =
"$dir/{$defaultPaths['localBasePath']}";
623 foreach ( [
'ResourceModules',
'ResourceModuleSkinStyles',
'OOUIThemePaths' ] as $setting ) {
624 if ( isset( $info[$setting] ) ) {
625 foreach ( $info[$setting] as $name => $data ) {
626 if ( isset( $data[
'localBasePath'] ) ) {
627 if ( $data[
'localBasePath'] ===
'' ) {
629 $data[
'localBasePath'] = $dir;
631 $data[
'localBasePath'] =
"$dir/{$data['localBasePath']}";
634 if ( $defaultPaths ) {
635 $data += $defaultPaths;
637 $this->attributes[$setting][$name] = $data;
642 if ( isset( $info[
'QUnitTestModule'] ) ) {
643 $data = $info[
'QUnitTestModule'];
644 if ( isset( $data[
'localBasePath'] ) ) {
645 if ( $data[
'localBasePath'] ===
'' ) {
647 $data[
'localBasePath'] = $dir;
649 $data[
'localBasePath'] =
"$dir/{$data['localBasePath']}";
653 $data[
'targets'] = [
'test' ];
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";
704 if ( isset( $info[
'ValidSkinNames'] ) ) {
705 foreach ( $info[
'ValidSkinNames'] as $skinKey => $data ) {
706 if ( isset( $data[
'args'][0][
'templateDirectory'] ) ) {
707 $templateDirectory = $data[
'args'][0][
'templateDirectory'];
708 $correctedPath = $dir .
'/' . $templateDirectory;
714 if ( is_dir( $correctedPath ) ) {
715 $data[
'args'][0][
'templateDirectory'] = $correctedPath;
717 $data[
'args'][0][
'templateDirectory'] = $templateDirectory;
719 'Template directory should be relative to skin or omitted for skin ' . $skinKey,
723 } elseif ( isset( $data[
'args'][0] ) ) {
725 $data[
'args'][0][
'templateDirectory'] = $dir .
'/templates';
727 $this->globals[
'wgValidSkinNames'][$skinKey] = $data;
737 if ( isset( $info[
'SkinLessImportPaths'] ) ) {
738 foreach ( $info[
'SkinLessImportPaths'] as $skin => $subpath ) {
739 $this->attributes[
'SkinLessImportPaths'][$skin] =
"$dir/$subpath";
755 foreach ( self::CREDIT_ATTRIBS as $attr ) {
756 if ( isset( $info[$attr] ) ) {
765 if ( isset( $this->credits[$name] ) ) {
766 $firstPath = $this->credits[$name][
'path'];
768 throw new Exception(
"It was attempted to load $name twice, from $firstPath and $secondPath." );
783 if ( isset( $info[
'config'] ) ) {
784 if ( isset( $info[
'config'][
'_prefix'] ) ) {
785 $prefix = $info[
'config'][
'_prefix'];
786 unset( $info[
'config'][
'_prefix'] );
790 foreach ( $info[
'config'] as $key => $val ) {
791 if ( $key[0] !==
'@' ) {
792 $this->addConfigGlobal(
"$prefix$key", $val, $info[
'name'] );
806 private function applyPath( array $value,
string $dir ): array {
809 foreach ( $value as $k => $v ) {
810 $result[$k] = $dir .
'/' . $v;
823 $prefix = $info[
'config_prefix'] ??
'wg';
824 if ( isset( $info[
'config'] ) ) {
825 foreach ( $info[
'config'] as $key => $data ) {
826 if ( !array_key_exists(
'value', $data ) ) {
827 throw new UnexpectedValueException(
"Missing value for config $key" );
830 $value = $data[
'value'];
831 if ( isset( $data[
'path'] ) && $data[
'path'] ) {
832 if ( is_array( $value ) ) {
833 $value = $this->applyPath( $value, $dir );
835 $value =
"$dir/$value";
838 if ( isset( $data[
'merge_strategy'] ) ) {
839 $value[ExtensionRegistry::MERGE_STRATEGY] = $data[
'merge_strategy'];
841 $this->addConfigGlobal(
"$prefix$key", $value, $info[
'name'] );
842 $data[
'providedby'] = $info[
'name'];
843 if ( isset( $info[
'ConfigRegistry'][0] ) ) {
844 $data[
'configregistry'] = array_keys( $info[
'ConfigRegistry'] )[0];
857 private function addConfigGlobal( $key, $value, $extName ) {
858 if ( array_key_exists( $key, $this->globals ) ) {
859 throw new RuntimeException(
860 "The configuration setting '$key' was already set by MediaWiki core or"
861 .
" another extension, and cannot be set again by $extName." );
863 $this->globals[$key] = $value;
867 foreach ( $paths as
$path ) {
868 $this->globals[$global][] =
"$dir/$path";
882 if ( !is_array( $value ) ) {
883 throw new InvalidArgumentException(
"The value for '$name' should be an array (from $path)" );
885 if ( isset( $array[$name] ) ) {
886 $array[$name] = array_merge_recursive( $array[$name], $value );
888 $array[$name] = $value;
902 if ( !is_array( $value ) ) {
903 throw new InvalidArgumentException(
"The value for '$name' should be an array (from $path)" );
905 if ( isset( $array[$name] ) ) {
906 $array[$name] = array_merge( $array[$name], $value );
908 $array[$name] = $value;
923 if ( isset( $info[
'load_composer_autoloader'] ) && $info[
'load_composer_autoloader'] ===
true ) {
924 $paths[] =
"$dir/vendor/autoload.php";
944 $autoload = $this->autoload;
947 $autoload[
'classes'] += $this->autoloadDev[
'classes'];
948 $autoload[
'namespaces'] += $this->autoloadDev[
'namespaces'];
953 if ( !empty( $this->autoloadDev[
'files'] ) ) {
956 $autoload[
'files'] = array_merge(
958 $this->autoloadDev[
'files']
970 private function extractAutoload( array $info,
string $dir ) {
971 if ( isset( $info[
'load_composer_autoloader'] ) && $info[
'load_composer_autoloader'] ===
true ) {
972 $file =
"$dir/vendor/autoload.php";
973 if ( file_exists(
$file ) ) {
974 $this->autoload[
'files'][] =
$file;
978 if ( isset( $info[
'AutoloadClasses'] ) ) {
979 $paths = $this->applyPath( $info[
'AutoloadClasses'], $dir );
980 $this->autoload[
'classes'] += $paths;
983 if ( isset( $info[
'AutoloadNamespaces'] ) ) {
984 $paths = $this->applyPath( $info[
'AutoloadNamespaces'], $dir );
985 $this->autoload[
'namespaces'] += $paths;
988 if ( isset( $info[
'TestAutoloadClasses'] ) ) {
989 $paths = $this->applyPath( $info[
'TestAutoloadClasses'], $dir );
990 $this->autoloadDev[
'classes'] += $paths;
993 if ( isset( $info[
'TestAutoloadNamespaces'] ) ) {
994 $paths = $this->applyPath( $info[
'TestAutoloadNamespaces'], $dir );
995 $this->autoloadDev[
'namespaces'] += $paths;