3 namespace MediaWiki\Registration
;
6 use InvalidArgumentException
;
7 use MediaWiki\MainConfigNames
;
8 use MediaWiki\ResourceLoader\FilePath
;
10 use UnexpectedValueException
;
13 * Load extension manifests and then aggregate their contents.
15 * @ingroup ExtensionRegistry
18 class ExtensionProcessor
implements Processor
{
21 * Keys that should be set to $GLOBALS
25 protected static $globalSettings = [
26 MainConfigNames
::ActionFilteredLogs
,
27 MainConfigNames
::Actions
,
28 MainConfigNames
::AddGroups
,
29 MainConfigNames
::APIFormatModules
,
30 MainConfigNames
::APIListModules
,
31 MainConfigNames
::APIMetaModules
,
32 MainConfigNames
::APIModules
,
33 MainConfigNames
::APIPropModules
,
34 MainConfigNames
::AuthManagerAutoConfig
,
35 MainConfigNames
::AvailableRights
,
36 MainConfigNames
::CentralIdLookupProviders
,
37 MainConfigNames
::ChangeCredentialsBlacklist
,
38 MainConfigNames
::ConditionalUserOptions
,
39 MainConfigNames
::ConfigRegistry
,
40 MainConfigNames
::ContentHandlers
,
41 MainConfigNames
::DefaultUserOptions
,
42 MainConfigNames
::ExtensionEntryPointListFiles
,
43 MainConfigNames
::ExtensionFunctions
,
44 MainConfigNames
::FeedClasses
,
45 MainConfigNames
::FileExtensions
,
46 MainConfigNames
::FilterLogTypes
,
47 MainConfigNames
::GrantPermissionGroups
,
48 MainConfigNames
::GrantPermissions
,
49 MainConfigNames
::GrantRiskGroups
,
50 MainConfigNames
::GroupPermissions
,
51 MainConfigNames
::GroupsAddToSelf
,
52 MainConfigNames
::GroupsRemoveFromSelf
,
53 MainConfigNames
::HiddenPrefs
,
54 MainConfigNames
::ImplicitGroups
,
55 MainConfigNames
::JobClasses
,
56 MainConfigNames
::LogActions
,
57 MainConfigNames
::LogActionsHandlers
,
58 MainConfigNames
::LogHeaders
,
59 MainConfigNames
::LogNames
,
60 MainConfigNames
::LogRestrictions
,
61 MainConfigNames
::LogTypes
,
62 MainConfigNames
::MediaHandlers
,
63 MainConfigNames
::OutputPipelineStages
,
64 MainConfigNames
::PasswordPolicy
,
65 MainConfigNames
::PrivilegedGroups
,
66 MainConfigNames
::RateLimits
,
67 MainConfigNames
::RawHtmlMessages
,
68 MainConfigNames
::ReauthenticateTime
,
69 MainConfigNames
::RecentChangesFlags
,
70 MainConfigNames
::RemoveCredentialsBlacklist
,
71 MainConfigNames
::RemoveGroups
,
72 MainConfigNames
::ResourceLoaderSources
,
73 MainConfigNames
::RevokePermissions
,
74 MainConfigNames
::SessionProviders
,
75 MainConfigNames
::SpecialPages
,
76 MainConfigNames
::UserRegistrationProviders
,
80 * Top-level attributes that come from MW core
82 protected const CORE_ATTRIBS
= [
89 'LateJSConfigVarNames',
90 'TempUserSerialProviders',
91 'TempUserSerialMappings',
92 'DatabaseVirtualDomains',
93 'UserOptionsStoreProviders',
97 * Mapping of global settings to their specific merge strategies.
99 * @see ExtensionRegistry::exportExtractedData
100 * @see getExtractedInfo
102 protected const MERGE_STRATEGIES
= [
103 'wgAuthManagerAutoConfig' => 'array_plus_2d',
104 'wgCapitalLinkOverrides' => 'array_plus',
105 'wgExtraGenderNamespaces' => 'array_plus',
106 'wgGrantPermissions' => 'array_plus_2d',
107 'wgGroupPermissions' => 'array_plus_2d',
108 'wgHooks' => 'array_merge_recursive',
109 'wgNamespaceContentModels' => 'array_plus',
110 'wgNamespaceProtection' => 'array_plus',
111 'wgNamespacesWithSubpages' => 'array_plus',
112 'wgPasswordPolicy' => 'array_merge_recursive',
113 'wgRateLimits' => 'array_plus_2d',
114 'wgRevokePermissions' => 'array_plus_2d',
118 * Keys that are part of the extension credits
120 protected const CREDIT_ATTRIBS
= [
133 * Things that are not 'attributes', and are not in
134 * $globalSettings or CREDIT_ATTRIBS.
136 protected const NOT_ATTRIBS
= [
140 'load_composer_autoloader',
145 'AutoloadNamespaces',
146 'ExtensionMessagesFiles',
147 'TranslationAliasesDirs',
148 'ForeignResourcesDir',
150 'DomainEventSubscribers',
151 'MessagePosterModule',
155 'ResourceFileModulePaths',
156 'ResourceModuleSkinStyles',
158 'ServiceWiringFiles',
162 * Stuff that is going to be set to $GLOBALS
164 * Some keys are pre-set to arrays, so we can += to them
168 protected $globals = [
169 'wgExtensionMessagesFiles' => [],
170 'wgRestAPIAdditionalRouteFiles' => [],
171 'wgMessagesDirs' => [],
172 'TranslationAliasesDirs' => [],
176 * Things that should be define()'d
180 protected $defines = [];
183 * Things to be called once the registration of these extensions is done
185 * Keyed by the name of the extension that it belongs to
189 protected $callbacks = [];
194 protected $credits = [];
197 * Autoloader information.
198 * Each element is an array of strings.
199 * 'files' is just a list, 'classes' and 'namespaces' are associative.
203 protected $autoload = [
210 * Autoloader information for development.
211 * Same structure as $autoload.
215 protected $autoloadDev = [
222 * Anything else in the $info that hasn't
223 * already been processed
227 protected $attributes = [];
230 * Extension attributes, keyed by name =>
235 protected $extAttributes = [];
238 * Extracts extension info from the given JSON file.
240 * @param string $path
244 public function extractInfoFromFile( string $path ) {
245 $json = file_get_contents( $path );
246 $info = json_decode( $json, true );
249 throw new RuntimeException( "Failed to load JSON data from $path" );
252 $this->extractInfo( $path, $info, $info['manifest_version'] );
256 * @param string $path
258 * @param int $version manifest_version for info
260 public function extractInfo( $path, array $info, $version ) {
261 $dir = dirname( $path );
262 $this->extractHooks( $info, $path );
263 $this->extractDomainEventSubscribers( $info, $path );
264 $this->extractExtensionMessagesFiles( $dir, $info );
265 $this->extractRestModuleFiles( $dir, $info );
266 $this->extractMessagesDirs( $dir, $info );
267 $this->extractTranslationAliasesDirs( $dir, $info );
268 $this->extractSkins( $dir, $info );
269 $this->extractSkinImportPaths( $dir, $info );
270 $this->extractNamespaces( $info );
271 $this->extractImplicitRights( $info );
272 $this->extractResourceLoaderModules( $dir, $info );
273 $this->extractInstallerTasks( $dir, $info );
274 if ( isset( $info['ServiceWiringFiles'] ) ) {
275 $this->extractPathBasedGlobal(
276 'wgServiceWiringFiles',
278 $info['ServiceWiringFiles']
281 $name = $this->extractCredits( $path, $info );
282 if ( isset( $info['callback'] ) ) {
283 $this->callbacks
[$name] = $info['callback'];
286 $this->extractAutoload( $info, $dir );
288 // config should be after all core globals are extracted,
289 // so duplicate setting detection will work fully
290 if ( $version >= 2 ) {
291 $this->extractConfig2( $info, $dir );
294 $this->extractConfig1( $info );
297 // Record the extension name in the ParsoidModules property
298 if ( isset( $info['ParsoidModules'] ) ) {
299 foreach ( $info['ParsoidModules'] as &$module ) {
300 if ( is_string( $module ) ) {
301 $className = $module;
303 'class' => $className,
306 $module['name'] = $name;
310 $this->extractForeignResourcesDir( $info, $name, $dir );
312 if ( $version >= 2 ) {
313 $this->extractAttributes( $path, $info );
316 foreach ( $info as $key => $val ) {
317 // If it's a global setting,
318 if ( in_array( $key, self
::$globalSettings ) ) {
319 $this->storeToArrayRecursive( $path, "wg$key", $val, $this->globals
);
322 // Ignore anything that starts with a @
323 if ( $key[0] === '@' ) {
327 if ( $version >= 2 ) {
328 // Only allowed attributes are set
329 if ( in_array( $key, self
::CORE_ATTRIBS
) ) {
330 $this->storeToArray( $path, $key, $val, $this->attributes
);
334 if ( !in_array( $key, self
::NOT_ATTRIBS
)
335 && !in_array( $key, self
::CREDIT_ATTRIBS
)
337 // If it's not disallowed, it's an attribute
338 $this->storeToArrayRecursive( $path, $key, $val, $this->attributes
);
345 * @param string $path
348 protected function extractAttributes( $path, array $info ) {
349 if ( isset( $info['attributes'] ) ) {
350 foreach ( $info['attributes'] as $extName => $value ) {
351 $this->storeToArrayRecursive( $path, $extName, $value, $this->extAttributes
);
356 public function getExtractedInfo( bool $includeDev = false ) {
357 // Make sure the merge strategies are set
358 foreach ( $this->globals
as $key => $val ) {
359 if ( isset( self
::MERGE_STRATEGIES
[$key] ) ) {
360 $this->globals
[$key][ExtensionRegistry
::MERGE_STRATEGY
] = self
::MERGE_STRATEGIES
[$key];
364 // Merge $this->extAttributes into $this->attributes depending on what is loaded
365 foreach ( $this->extAttributes
as $extName => $value ) {
366 // Only set the attribute if $extName is loaded (and hence present in credits)
367 if ( isset( $this->credits
[$extName] ) ) {
368 foreach ( $value as $attrName => $attrValue ) {
369 $this->storeToArrayRecursive(
370 '', // Don't provide a path since it's impossible to generate an error here
371 $extName . $attrName,
376 unset( $this->extAttributes
[$extName] );
380 $autoload = $this->getExtractedAutoloadInfo( $includeDev );
383 'globals' => $this->globals
,
384 'defines' => $this->defines
,
385 'callbacks' => $this->callbacks
,
386 'credits' => $this->credits
,
387 'attributes' => $this->attributes
,
388 'autoloaderPaths' => $autoload['files'],
389 'autoloaderClasses' => $autoload['classes'],
390 'autoloaderNS' => $autoload['namespaces'],
394 public function getRequirements( array $info, $includeDev ) {
396 if ( !$includeDev ||
!isset( $info['dev-requires'] ) ) {
397 return $info['requires'] ??
[];
400 if ( !isset( $info['requires'] ) ) {
401 return $info['dev-requires'] ??
[];
404 // OK, we actually have to merge everything
407 // Helper that combines version requirements by
408 // picking the non-null if one is, or combines
409 // the two. Note that it is not possible for
410 // both inputs to be null.
411 $pick = static function ( $a, $b ) {
414 } elseif ( $b === null ) {
421 $req = $info['requires'];
422 $dev = $info['dev-requires'];
423 if ( isset( $req['MediaWiki'] ) ||
isset( $dev['MediaWiki'] ) ) {
424 $merged['MediaWiki'] = $pick(
425 $req['MediaWiki'] ??
null,
426 $dev['MediaWiki'] ??
null
430 $platform = array_merge(
431 array_keys( $req['platform'] ??
[] ),
432 array_keys( $dev['platform'] ??
[] )
435 foreach ( $platform as $pkey ) {
436 if ( $pkey === 'php' ) {
438 $req['platform']['php'] ??
null,
439 $dev['platform']['php'] ??
null
442 // Prefer dev value, but these should be constant
443 // anyway (ext-* and ability-*)
444 $value = $dev['platform'][$pkey] ??
$req['platform'][$pkey];
446 $merged['platform'][$pkey] = $value;
450 foreach ( [ 'extensions', 'skins' ] as $thing ) {
451 $things = array_merge(
452 array_keys( $req[$thing] ??
[] ),
453 array_keys( $dev[$thing] ??
[] )
455 foreach ( $things as $name ) {
456 $merged[$thing][$name] = $pick(
457 $req[$thing][$name] ??
null,
458 $dev[$thing][$name] ??
null
467 * When handler value is an array, set $wgHooks or Hooks attribute
468 * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook
469 * referencing a handler definition from 'HookHandler' attribute
471 * @param array $callback Handler
472 * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute
473 * @param string $name
474 * @param string $path extension.json file path
476 * @throws UnexpectedValueException
478 private function setArrayHookHandler(
480 array $hookHandlersAttr,
484 if ( isset( $callback['handler'] ) ) {
485 $handlerName = $callback['handler'];
486 $handlerDefinition = $hookHandlersAttr[$handlerName] ??
false;
487 if ( !$handlerDefinition ) {
488 throw new UnexpectedValueException(
489 "Missing handler definition for $name in HookHandlers attribute in $path"
492 $callback['handler'] = $handlerDefinition;
493 $callback['extensionPath'] = $path;
494 $this->attributes
['Hooks'][$name][] = $callback;
496 foreach ( $callback as $callable ) {
497 if ( is_array( $callable ) ) {
498 if ( isset( $callable['handler'] ) ) { // Non-legacy style handler
499 $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name, $path );
500 } else { // Legacy style handler array
501 $this->globals
['wgHooks'][$name][] = $callable;
503 } elseif ( is_string( $callable ) ) {
504 $this->setStringHookHandler( $callable, $hookHandlersAttr, $name, $path );
511 * When handler value is a string, set $wgHooks or Hooks attribute.
512 * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook
513 * referencing a handler definition from 'HookHandler' attribute
515 * @param string $callback Handler
516 * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute
517 * @param string $name
518 * @param string $path
520 private function setStringHookHandler(
522 array $hookHandlersAttr,
526 if ( isset( $hookHandlersAttr[$callback] ) ) {
528 'handler' => $hookHandlersAttr[$callback],
529 'extensionPath' => $path
531 $this->attributes
['Hooks'][$name][] = $handler;
532 } else { // legacy style handler
533 $this->globals
['wgHooks'][$name][] = $callback;
538 * Extract hook information from Hooks and HookHandler attributes.
539 * Store hook in $wgHooks if a legacy style handler or the 'Hooks' attribute if
540 * a non-legacy handler
542 * @param array $info attributes and associated values from extension.json
543 * @param string $path path to extension.json
545 protected function extractHooks( array $info, string $path ) {
546 $extName = $info['name'];
547 if ( isset( $info['Hooks'] ) ) {
548 $hookHandlersAttr = [];
549 foreach ( $info['HookHandlers'] ??
[] as $name => $def ) {
550 $hookHandlersAttr[$name] = [ 'name' => "$extName-$name" ] +
$def;
552 foreach ( $info['Hooks'] as $name => $callback ) {
553 if ( is_string( $callback ) ) {
554 $this->setStringHookHandler( $callback, $hookHandlersAttr, $name, $path );
555 } elseif ( is_array( $callback ) ) {
556 $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name, $path );
560 if ( isset( $info['DeprecatedHooks'] ) ) {
561 $deprecatedHooks = [];
562 foreach ( $info['DeprecatedHooks'] as $name => $deprecatedHookInfo ) {
563 $deprecatedHookInfo +
= [ 'component' => $extName ];
564 $deprecatedHooks[$name] = $deprecatedHookInfo;
566 if ( isset( $this->attributes
['DeprecatedHooks'] ) ) {
567 $this->attributes
['DeprecatedHooks'] +
= $deprecatedHooks;
569 $this->attributes
['DeprecatedHooks'] = $deprecatedHooks;
575 * Extract domain event subscribers.
577 * @param array $info attributes and associated values from extension.json
578 * @param string $path path to extension.json
580 protected function extractDomainEventSubscribers( array $info, string $path ) {
581 $this->attributes
['DomainEventSubscribers'] ??
= [];
582 foreach ( $info['DomainEventSubscribers'] ??
[] as $subscriber ) {
583 $subscriber['extensionPath'] = $path;
584 $this->attributes
['DomainEventSubscribers'][] = $subscriber;
589 * Register namespaces with the appropriate global settings
593 protected function extractNamespaces( array $info ) {
594 if ( isset( $info['namespaces'] ) ) {
595 foreach ( $info['namespaces'] as $ns ) {
596 if ( defined( $ns['constant'] ) ) {
597 // If the namespace constant is already defined, use it.
598 // This allows namespace IDs to be overwritten locally.
599 $id = constant( $ns['constant'] );
603 $this->defines
[ $ns['constant'] ] = $id;
605 if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
606 // If it is not conditional, register it
607 $this->attributes
['ExtensionNamespaces'][$id] = $ns['name'];
609 if ( isset( $ns['movable'] ) && !$ns['movable'] ) {
610 $this->attributes
['ImmovableNamespaces'][] = $id;
612 if ( isset( $ns['gender'] ) ) {
613 $this->globals
['wgExtraGenderNamespaces'][$id] = $ns['gender'];
615 if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
616 $this->globals
['wgNamespacesWithSubpages'][$id] = true;
618 if ( isset( $ns['content'] ) && $ns['content'] ) {
619 $this->globals
['wgContentNamespaces'][] = $id;
621 if ( isset( $ns['defaultcontentmodel'] ) ) {
622 $this->globals
['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
624 if ( isset( $ns['protection'] ) ) {
625 $this->globals
['wgNamespaceProtection'][$id] = $ns['protection'];
627 if ( isset( $ns['capitallinkoverride'] ) ) {
628 $this->globals
['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
630 if ( isset( $ns['includable'] ) && !$ns['includable'] ) {
631 $this->globals
['wgNonincludableNamespaces'][] = $id;
637 protected function extractResourceLoaderModules( $dir, array $info ) {
638 $defaultPaths = $info['ResourceFileModulePaths'] ??
false;
639 if ( isset( $defaultPaths['localBasePath'] ) ) {
640 if ( $defaultPaths['localBasePath'] === '' ) {
641 // Avoid double slashes (e.g. /extensions/Example//path)
642 $defaultPaths['localBasePath'] = $dir;
644 $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
648 foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
649 if ( isset( $info[$setting] ) ) {
650 foreach ( $info[$setting] as $name => $data ) {
651 if ( isset( $data['localBasePath'] ) ) {
652 if ( $data['localBasePath'] === '' ) {
653 // Avoid double slashes (e.g. /extensions/Example//path)
654 $data['localBasePath'] = $dir;
656 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
659 if ( $defaultPaths ) {
660 $data +
= $defaultPaths;
662 $this->attributes
[$setting][$name] = $data;
667 if ( isset( $info['QUnitTestModule'] ) ) {
668 $data = $info['QUnitTestModule'];
669 if ( isset( $data['localBasePath'] ) ) {
670 if ( $data['localBasePath'] === '' ) {
671 // Avoid double slashes (e.g. /extensions/Example//path)
672 $data['localBasePath'] = $dir;
674 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
677 $this->attributes
['QUnitTestModules']["test.{$info['name']}"] = $data;
680 if ( isset( $info['MessagePosterModule'] ) ) {
681 $data = $info['MessagePosterModule'];
682 $basePath = $data['localBasePath'] ??
'';
683 $baseDir = $basePath === '' ?
$dir : "$dir/$basePath";
684 foreach ( $data['scripts'] ??
[] as $scripts ) {
685 $this->attributes
['MessagePosterModule']['scripts'][] =
686 new FilePath( $scripts, $baseDir );
688 foreach ( $data['dependencies'] ??
[] as $dependency ) {
689 $this->attributes
['MessagePosterModule']['dependencies'][] = $dependency;
694 protected function extractExtensionMessagesFiles( $dir, array $info ) {
695 if ( isset( $info['ExtensionMessagesFiles'] ) ) {
696 foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
697 $file = "$dir/$file";
699 $this->globals
["wgExtensionMessagesFiles"] +
= $info['ExtensionMessagesFiles'];
703 protected function extractRestModuleFiles( $dir, array $info ) {
704 $var = MainConfigNames
::RestAPIAdditionalRouteFiles
;
705 if ( isset( $info['RestModuleFiles'] ) ) {
706 foreach ( $info['RestModuleFiles'] as &$file ) {
707 $this->globals
["wg$var"][] = "$dir/$file";
713 * Set message-related settings, which need to be expanded to use
719 protected function extractMessagesDirs( $dir, array $info ) {
720 if ( isset( $info['MessagesDirs'] ) ) {
721 foreach ( $info['MessagesDirs'] as $name => $files ) {
722 foreach ( (array)$files as $file ) {
723 $this->globals
["wgMessagesDirs"][$name][] = "$dir/$file";
730 * Set localization related settings, which need to be expanded to use
736 protected function extractTranslationAliasesDirs( $dir, array $info ) {
737 foreach ( $info['TranslationAliasesDirs'] ??
[] as $name => $files ) {
738 foreach ( (array)$files as $file ) {
739 $this->globals
['wgTranslationAliasesDirs'][$name][] = "$dir/$file";
745 * Extract skins and handle path correction for templateDirectory.
750 protected function extractSkins( $dir, array $info ) {
751 if ( isset( $info['ValidSkinNames'] ) ) {
752 foreach ( $info['ValidSkinNames'] as $skinKey => $data ) {
753 if ( isset( $data['args'][0] ) ) {
754 $templateDirectory = $data['args'][0]['templateDirectory'] ??
'templates';
755 $data['args'][0]['templateDirectory'] = $dir . '/' . $templateDirectory;
757 $this->globals
['wgValidSkinNames'][$skinKey] = $data;
763 * Extract any user rights that should be granted implicitly.
767 protected function extractImplicitRights( array $info ) {
768 // Rate limits are only configurable for rights that are either in wgImplicitRights
769 // or in wgAvailableRights. Extensions that define rate limits should not have to
770 // explicitly add them to wgImplicitRights as well, we can do that automatically.
772 if ( isset( $info['RateLimits'] ) ) {
773 $rights = array_keys( $info['RateLimits'] );
775 if ( isset( $info['AvailableRights'] ) ) {
776 $rights = array_diff( $rights, $info['AvailableRights'] );
779 $this->globals
['wgImplicitRights'] = array_merge(
780 $this->globals
['wgImplicitRights'] ??
[],
790 protected function extractSkinImportPaths( $dir, array $info ) {
791 if ( isset( $info['SkinLessImportPaths'] ) ) {
792 foreach ( $info['SkinLessImportPaths'] as $skin => $subpath ) {
793 $this->attributes
['SkinLessImportPaths'][$skin] = "$dir/$subpath";
799 * @param string $path
802 * @return string Name of thing
805 protected function extractCredits( $path, array $info ) {
810 foreach ( self
::CREDIT_ATTRIBS
as $attr ) {
811 if ( isset( $info[$attr] ) ) {
812 $credits[$attr] = $info[$attr];
816 $name = $credits['name'];
818 // If someone is loading the same thing twice, throw
819 // a nice error (T121493)
820 if ( isset( $this->credits
[$name] ) ) {
821 $firstPath = $this->credits
[$name]['path'];
822 $secondPath = $credits['path'];
823 throw new InvalidArgumentException(
824 "It was attempted to load $name twice, from $firstPath and $secondPath."
828 $this->credits
[$name] = $credits;
833 protected function extractForeignResourcesDir( array $info, string $name, string $dir ): void
{
834 if ( array_key_exists( 'ForeignResourcesDir', $info ) ) {
835 if ( !is_string( $info['ForeignResourcesDir'] ) ) {
836 throw new InvalidArgumentException( "Incorrect ForeignResourcesDir type, must be a string (in $name)" );
838 $this->attributes
['ForeignResourcesDir'][$name] = "{$dir}/{$info['ForeignResourcesDir']}";
842 protected function extractInstallerTasks( string $path, array $info ): void
{
843 if ( isset( $info['InstallerTasks'] ) ) {
844 // Use a fixed path for the schema base path for now. This could be
845 // made configurable if there were a use case for that.
846 $schemaBasePath = $path . '/sql';
847 foreach ( $info['InstallerTasks'] as $taskSpec ) {
848 $this->attributes
['InstallerTasks'][]
849 = $taskSpec +
[ 'schemaBasePath' => $schemaBasePath ];
855 * Set configuration settings for manifest_version == 1
857 * @todo In the future, this should be done via Config interfaces
861 protected function extractConfig1( array $info ) {
862 if ( isset( $info['config'] ) ) {
863 if ( isset( $info['config']['_prefix'] ) ) {
864 $prefix = $info['config']['_prefix'];
865 unset( $info['config']['_prefix'] );
869 foreach ( $info['config'] as $key => $val ) {
870 if ( $key[0] !== '@' ) {
871 $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
878 * Applies a base path to the given string or string array.
880 * @param string[] $value
885 private function applyPath( array $value, string $dir ): array {
888 foreach ( $value as $k => $v ) {
889 $result[$k] = $dir . '/' . $v;
896 * Set configuration settings for manifest_version == 2
898 * @todo In the future, this should be done via Config interfaces
903 protected function extractConfig2( array $info, $dir ) {
904 $prefix = $info['config_prefix'] ??
'wg';
905 if ( isset( $info['config'] ) ) {
906 foreach ( $info['config'] as $key => $data ) {
907 if ( !array_key_exists( 'value', $data ) ) {
908 throw new UnexpectedValueException( "Missing value for config $key" );
911 $value = $data['value'];
912 if ( isset( $data['path'] ) && $data['path'] ) {
913 if ( is_array( $value ) ) {
914 $value = $this->applyPath( $value, $dir );
916 $value = "$dir/$value";
919 if ( isset( $data['merge_strategy'] ) ) {
920 $value[ExtensionRegistry
::MERGE_STRATEGY
] = $data['merge_strategy'];
922 $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
923 $data['providedby'] = $info['name'];
924 if ( isset( $info['ConfigRegistry'][0] ) ) {
925 $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0];
932 * Helper function to set a value to a specific global config variable if it isn't set already.
934 * @param string $key The config key with the prefix and anything
935 * @param mixed $value The value of the config
936 * @param string $extName Name of the extension
938 private function addConfigGlobal( $key, $value, $extName ) {
939 if ( array_key_exists( $key, $this->globals
) ) {
940 throw new RuntimeException(
941 "The configuration setting '$key' was already set by MediaWiki core or"
942 . " another extension, and cannot be set again by $extName." );
944 if ( isset( $value[ExtensionRegistry
::MERGE_STRATEGY
] ) &&
945 $value[ExtensionRegistry
::MERGE_STRATEGY
] === 'array_merge_recursive' ) {
947 "Using the array_merge_recursive merge strategy in extension.json and skin.json" .
948 " was deprecated in MediaWiki 1.42",
952 $this->globals
[$key] = $value;
955 protected function extractPathBasedGlobal( $global, $dir, $paths ) {
956 foreach ( $paths as $path ) {
957 $this->globals
[$global][] = "$dir/$path";
962 * Stores $value to $array; using array_merge_recursive() if $array already contains $name
964 * @param string $path
965 * @param string $name
966 * @param array $value
967 * @param array &$array
970 protected function storeToArrayRecursive( $path, $name, $value, &$array ) {
971 if ( !is_array( $value ) ) {
972 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
974 if ( isset( $array[$name] ) ) {
975 $array[$name] = array_merge_recursive( $array[$name], $value );
977 $array[$name] = $value;
982 * Stores $value to $array; using array_merge() if $array already contains $name
984 * @param string $path
985 * @param string $name
986 * @param array $value
987 * @param array &$array
989 * @throws InvalidArgumentException
991 protected function storeToArray( $path, $name, $value, &$array ) {
992 if ( !is_array( $value ) ) {
993 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
995 if ( isset( $array[$name] ) ) {
996 $array[$name] = array_merge( $array[$name], $value );
998 $array[$name] = $value;
1003 * Returns the extracted autoload info.
1004 * The autoload info is returned as an associative array with three keys:
1005 * - files: a list of files to load, for use with Autoloader::loadFile()
1006 * - classes: a map of class names to files, for use with Autoloader::registerClass()
1007 * - namespaces: a map of namespace names to directories, for use
1008 * with Autoloader::registerNamespace()
1012 * @param bool $includeDev
1014 * @return array[] The autoload info.
1016 public function getExtractedAutoloadInfo( bool $includeDev = false ): array {
1017 $autoload = $this->autoload
;
1019 if ( $includeDev ) {
1020 $autoload['classes'] +
= $this->autoloadDev
['classes'];
1021 $autoload['namespaces'] +
= $this->autoloadDev
['namespaces'];
1023 // NOTE: This is here for completeness. Per MW 1.39,
1024 // $this->autoloadDev['files'] is always empty.
1025 // So avoid the performance hit of array_merge().
1026 if ( !empty( $this->autoloadDev
['files'] ) ) {
1027 // NOTE: Don't use += with numeric keys!
1028 // Could use PHPUtils::pushArray.
1029 $autoload['files'] = array_merge(
1031 $this->autoloadDev
['files']
1040 * @param array $info
1041 * @param string $dir
1043 private function extractAutoload( array $info, string $dir ) {
1044 if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
1045 $file = "$dir/vendor/autoload.php";
1046 if ( file_exists( $file ) ) {
1047 $this->autoload
['files'][] = $file;
1051 if ( isset( $info['AutoloadClasses'] ) ) {
1052 $paths = $this->applyPath( $info['AutoloadClasses'], $dir );
1053 $this->autoload
['classes'] +
= $paths;
1056 if ( isset( $info['AutoloadNamespaces'] ) ) {
1057 $paths = $this->applyPath( $info['AutoloadNamespaces'], $dir );
1058 $this->autoload
['namespaces'] +
= $paths;
1061 if ( isset( $info['TestAutoloadClasses'] ) ) {
1062 $paths = $this->applyPath( $info['TestAutoloadClasses'], $dir );
1063 $this->autoloadDev
['classes'] +
= $paths;
1066 if ( isset( $info['TestAutoloadNamespaces'] ) ) {
1067 $paths = $this->applyPath( $info['TestAutoloadNamespaces'], $dir );
1068 $this->autoloadDev
['namespaces'] +
= $paths;
1073 /** @deprecated class alias since 1.43 */
1074 class_alias( ExtensionProcessor
::class, 'ExtensionProcessor' );