Merge "Mocha tests: Support language links to en-x-piglatin"
[mediawiki.git] / includes / registration / ExtensionProcessor.php
blob085acdd852e52a9833f24245abf9bf3575fb36e9
1 <?php
3 namespace MediaWiki\Registration;
5 use Exception;
6 use InvalidArgumentException;
7 use MediaWiki\MainConfigNames;
8 use MediaWiki\ResourceLoader\FilePath;
9 use RuntimeException;
10 use UnexpectedValueException;
12 /**
13 * Load extension manifests and then aggregate their contents.
15 * @ingroup ExtensionRegistry
16 * @newable since 1.39
18 class ExtensionProcessor implements Processor {
20 /**
21 * Keys that should be set to $GLOBALS
23 * @var array
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,
79 /**
80 * Top-level attributes that come from MW core
82 protected const CORE_ATTRIBS = [
83 'ParsoidModules',
84 'RestRoutes',
85 'SkinOOUIThemes',
86 'SkinCodexThemes',
87 'SearchMappings',
88 'TrackingCategories',
89 'LateJSConfigVarNames',
90 'TempUserSerialProviders',
91 'TempUserSerialMappings',
92 'DatabaseVirtualDomains',
93 'UserOptionsStoreProviders',
96 /**
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 = [
121 'type',
122 'author',
123 'description',
124 'descriptionmsg',
125 'license-name',
126 'name',
127 'namemsg',
128 'url',
129 'version',
133 * Things that are not 'attributes', and are not in
134 * $globalSettings or CREDIT_ATTRIBS.
136 protected const NOT_ATTRIBS = [
137 'callback',
138 'config',
139 'config_prefix',
140 'load_composer_autoloader',
141 'manifest_version',
142 'namespaces',
143 'requires',
144 'AutoloadClasses',
145 'AutoloadNamespaces',
146 'ExtensionMessagesFiles',
147 'TranslationAliasesDirs',
148 'ForeignResourcesDir',
149 'Hooks',
150 'DomainEventSubscribers',
151 'MessagePosterModule',
152 'MessagesDirs',
153 'OOUIThemePaths',
154 'QUnitTestModule',
155 'ResourceFileModulePaths',
156 'ResourceModuleSkinStyles',
157 'ResourceModules',
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
166 * @var array
168 protected $globals = [
169 'wgExtensionMessagesFiles' => [],
170 'wgRestAPIAdditionalRouteFiles' => [],
171 'wgMessagesDirs' => [],
172 'TranslationAliasesDirs' => [],
176 * Things that should be define()'d
178 * @var array
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
187 * @var callable[]
189 protected $callbacks = [];
192 * @var array
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.
201 * @var string[][]
203 protected $autoload = [
204 'files' => [],
205 'classes' => [],
206 'namespaces' => [],
210 * Autoloader information for development.
211 * Same structure as $autoload.
213 * @var string[][]
215 protected $autoloadDev = [
216 'files' => [],
217 'classes' => [],
218 'namespaces' => [],
222 * Anything else in the $info that hasn't
223 * already been processed
225 * @var array
227 protected $attributes = [];
230 * Extension attributes, keyed by name =>
231 * settings.
233 * @var array
235 protected $extAttributes = [];
238 * Extracts extension info from the given JSON file.
240 * @param string $path
242 * @return void
244 public function extractInfoFromFile( string $path ) {
245 $json = file_get_contents( $path );
246 $info = json_decode( $json, true );
248 if ( !$info ) {
249 throw new RuntimeException( "Failed to load JSON data from $path" );
252 $this->extractInfo( $path, $info, $info['manifest_version'] );
256 * @param string $path
257 * @param array $info
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',
277 $dir,
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 );
292 } else {
293 // $version === 1
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;
302 $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 );
320 continue;
322 // Ignore anything that starts with a @
323 if ( $key[0] === '@' ) {
324 continue;
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 );
332 } else {
333 // version === 1
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
346 * @param array $info
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,
372 $attrValue,
373 $this->attributes
376 unset( $this->extAttributes[$extName] );
380 $autoload = $this->getExtractedAutoloadInfo( $includeDev );
382 return [
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 ) {
395 // Quick shortcuts
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
405 $merged = [];
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 ) {
412 if ( $a === null ) {
413 return $b;
414 } elseif ( $b === null ) {
415 return $a;
416 } else {
417 return "$a $b";
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'] ?? [] )
434 if ( $platform ) {
435 foreach ( $platform as $pkey ) {
436 if ( $pkey === 'php' ) {
437 $value = $pick(
438 $req['platform']['php'] ?? null,
439 $dev['platform']['php'] ?? null
441 } else {
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
463 return $merged;
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(
479 array $callback,
480 array $hookHandlersAttr,
481 string $name,
482 string $path
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;
495 } else {
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(
521 string $callback,
522 array $hookHandlersAttr,
523 string $name,
524 string $path
526 if ( isset( $hookHandlersAttr[$callback] ) ) {
527 $handler = [
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;
568 } else {
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
591 protected function extractNamespaces( array $info ) {
592 if ( isset( $info['namespaces'] ) ) {
593 foreach ( $info['namespaces'] as $ns ) {
594 if ( defined( $ns['constant'] ) ) {
595 // If the namespace constant is already defined, use it.
596 // This allows namespace IDs to be overwritten locally.
597 $id = constant( $ns['constant'] );
598 } else {
599 $id = $ns['id'];
601 $this->defines[ $ns['constant'] ] = $id;
603 if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
604 // If it is not conditional, register it
605 $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
607 if ( isset( $ns['movable'] ) && !$ns['movable'] ) {
608 $this->attributes['ImmovableNamespaces'][] = $id;
610 if ( isset( $ns['gender'] ) ) {
611 $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
613 if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
614 $this->globals['wgNamespacesWithSubpages'][$id] = true;
616 if ( isset( $ns['content'] ) && $ns['content'] ) {
617 $this->globals['wgContentNamespaces'][] = $id;
619 if ( isset( $ns['defaultcontentmodel'] ) ) {
620 $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
622 if ( isset( $ns['protection'] ) ) {
623 $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
625 if ( isset( $ns['capitallinkoverride'] ) ) {
626 $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
628 if ( isset( $ns['includable'] ) && !$ns['includable'] ) {
629 $this->globals['wgNonincludableNamespaces'][] = $id;
635 protected function extractResourceLoaderModules( $dir, array $info ) {
636 $defaultPaths = $info['ResourceFileModulePaths'] ?? false;
637 if ( isset( $defaultPaths['localBasePath'] ) ) {
638 if ( $defaultPaths['localBasePath'] === '' ) {
639 // Avoid double slashes (e.g. /extensions/Example//path)
640 $defaultPaths['localBasePath'] = $dir;
641 } else {
642 $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
646 foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
647 if ( isset( $info[$setting] ) ) {
648 foreach ( $info[$setting] as $name => $data ) {
649 if ( isset( $data['localBasePath'] ) ) {
650 if ( $data['localBasePath'] === '' ) {
651 // Avoid double slashes (e.g. /extensions/Example//path)
652 $data['localBasePath'] = $dir;
653 } else {
654 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
657 if ( $defaultPaths ) {
658 $data += $defaultPaths;
660 $this->attributes[$setting][$name] = $data;
665 if ( isset( $info['QUnitTestModule'] ) ) {
666 $data = $info['QUnitTestModule'];
667 if ( isset( $data['localBasePath'] ) ) {
668 if ( $data['localBasePath'] === '' ) {
669 // Avoid double slashes (e.g. /extensions/Example//path)
670 $data['localBasePath'] = $dir;
671 } else {
672 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
675 $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data;
678 if ( isset( $info['MessagePosterModule'] ) ) {
679 $data = $info['MessagePosterModule'];
680 $basePath = $data['localBasePath'] ?? '';
681 $baseDir = $basePath === '' ? $dir : "$dir/$basePath";
682 foreach ( $data['scripts'] ?? [] as $scripts ) {
683 $this->attributes['MessagePosterModule']['scripts'][] =
684 new FilePath( $scripts, $baseDir );
686 foreach ( $data['dependencies'] ?? [] as $dependency ) {
687 $this->attributes['MessagePosterModule']['dependencies'][] = $dependency;
692 protected function extractExtensionMessagesFiles( $dir, array $info ) {
693 if ( isset( $info['ExtensionMessagesFiles'] ) ) {
694 foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
695 $file = "$dir/$file";
697 $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
701 protected function extractRestModuleFiles( $dir, array $info ) {
702 $var = MainConfigNames::RestAPIAdditionalRouteFiles;
703 if ( isset( $info['RestModuleFiles'] ) ) {
704 foreach ( $info['RestModuleFiles'] as &$file ) {
705 $this->globals["wg$var"][] = "$dir/$file";
711 * Set message-related settings, which need to be expanded to use
712 * absolute paths
714 * @param string $dir
715 * @param array $info
717 protected function extractMessagesDirs( $dir, array $info ) {
718 if ( isset( $info['MessagesDirs'] ) ) {
719 foreach ( $info['MessagesDirs'] as $name => $files ) {
720 foreach ( (array)$files as $file ) {
721 $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
728 * Set localization related settings, which need to be expanded to use
729 * absolute paths
731 * @param string $dir
732 * @param array $info
734 protected function extractTranslationAliasesDirs( $dir, array $info ) {
735 foreach ( $info['TranslationAliasesDirs'] ?? [] as $name => $files ) {
736 foreach ( (array)$files as $file ) {
737 $this->globals['wgTranslationAliasesDirs'][$name][] = "$dir/$file";
743 * Extract skins and handle path correction for templateDirectory.
745 * @param string $dir
746 * @param array $info
748 protected function extractSkins( $dir, array $info ) {
749 if ( isset( $info['ValidSkinNames'] ) ) {
750 foreach ( $info['ValidSkinNames'] as $skinKey => $data ) {
751 if ( isset( $data['args'][0] ) ) {
752 $templateDirectory = $data['args'][0]['templateDirectory'] ?? 'templates';
753 $data['args'][0]['templateDirectory'] = $dir . '/' . $templateDirectory;
755 $this->globals['wgValidSkinNames'][$skinKey] = $data;
761 * Extract any user rights that should be granted implicitly.
763 protected function extractImplicitRights( array $info ) {
764 // Rate limits are only configurable for rights that are either in wgImplicitRights
765 // or in wgAvailableRights. Extensions that define rate limits should not have to
766 // explicitly add them to wgImplicitRights as well, we can do that automatically.
768 if ( isset( $info['RateLimits'] ) ) {
769 $rights = array_keys( $info['RateLimits'] );
771 if ( isset( $info['AvailableRights'] ) ) {
772 $rights = array_diff( $rights, $info['AvailableRights'] );
775 $this->globals['wgImplicitRights'] = array_merge(
776 $this->globals['wgImplicitRights'] ?? [],
777 $rights
783 * @param string $dir
784 * @param array $info
786 protected function extractSkinImportPaths( $dir, array $info ) {
787 if ( isset( $info['SkinLessImportPaths'] ) ) {
788 foreach ( $info['SkinLessImportPaths'] as $skin => $subpath ) {
789 $this->attributes['SkinLessImportPaths'][$skin] = "$dir/$subpath";
795 * @param string $path
796 * @param array $info
798 * @return string Name of thing
799 * @throws Exception
801 protected function extractCredits( $path, array $info ) {
802 $credits = [
803 'path' => $path,
804 'type' => 'other',
806 foreach ( self::CREDIT_ATTRIBS as $attr ) {
807 if ( isset( $info[$attr] ) ) {
808 $credits[$attr] = $info[$attr];
812 $name = $credits['name'];
814 // If someone is loading the same thing twice, throw
815 // a nice error (T121493)
816 if ( isset( $this->credits[$name] ) ) {
817 $firstPath = $this->credits[$name]['path'];
818 $secondPath = $credits['path'];
819 throw new InvalidArgumentException(
820 "It was attempted to load $name twice, from $firstPath and $secondPath."
824 $this->credits[$name] = $credits;
826 return $name;
829 protected function extractForeignResourcesDir( array $info, string $name, string $dir ): void {
830 if ( array_key_exists( 'ForeignResourcesDir', $info ) ) {
831 if ( !is_string( $info['ForeignResourcesDir'] ) ) {
832 throw new InvalidArgumentException( "Incorrect ForeignResourcesDir type, must be a string (in $name)" );
834 $this->attributes['ForeignResourcesDir'][$name] = "{$dir}/{$info['ForeignResourcesDir']}";
838 protected function extractInstallerTasks( string $path, array $info ): void {
839 if ( isset( $info['InstallerTasks'] ) ) {
840 // Use a fixed path for the schema base path for now. This could be
841 // made configurable if there were a use case for that.
842 $schemaBasePath = $path . '/sql';
843 foreach ( $info['InstallerTasks'] as $taskSpec ) {
844 $this->attributes['InstallerTasks'][]
845 = $taskSpec + [ 'schemaBasePath' => $schemaBasePath ];
851 * Set configuration settings for manifest_version == 1
853 * @todo In the future, this should be done via Config interfaces
855 * @param array $info
857 protected function extractConfig1( array $info ) {
858 if ( isset( $info['config'] ) ) {
859 if ( isset( $info['config']['_prefix'] ) ) {
860 $prefix = $info['config']['_prefix'];
861 unset( $info['config']['_prefix'] );
862 } else {
863 $prefix = 'wg';
865 foreach ( $info['config'] as $key => $val ) {
866 if ( $key[0] !== '@' ) {
867 $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
874 * Applies a base path to the given string or string array.
876 * @param string[] $value
877 * @param string $dir
879 * @return string[]
881 private function applyPath( array $value, string $dir ): array {
882 $result = [];
884 foreach ( $value as $k => $v ) {
885 $result[$k] = $dir . '/' . $v;
888 return $result;
892 * Set configuration settings for manifest_version == 2
894 * @todo In the future, this should be done via Config interfaces
896 * @param array $info
897 * @param string $dir
899 protected function extractConfig2( array $info, $dir ) {
900 $prefix = $info['config_prefix'] ?? 'wg';
901 if ( isset( $info['config'] ) ) {
902 foreach ( $info['config'] as $key => $data ) {
903 if ( !array_key_exists( 'value', $data ) ) {
904 throw new UnexpectedValueException( "Missing value for config $key" );
907 $value = $data['value'];
908 if ( isset( $data['path'] ) && $data['path'] ) {
909 if ( is_array( $value ) ) {
910 $value = $this->applyPath( $value, $dir );
911 } else {
912 $value = "$dir/$value";
915 if ( isset( $data['merge_strategy'] ) ) {
916 $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
918 $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
919 $data['providedby'] = $info['name'];
920 if ( isset( $info['ConfigRegistry'][0] ) ) {
921 $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0];
928 * Helper function to set a value to a specific global config variable if it isn't set already.
930 * @param string $key The config key with the prefix and anything
931 * @param mixed $value The value of the config
932 * @param string $extName Name of the extension
934 private function addConfigGlobal( $key, $value, $extName ) {
935 if ( array_key_exists( $key, $this->globals ) ) {
936 throw new RuntimeException(
937 "The configuration setting '$key' was already set by MediaWiki core or"
938 . " another extension, and cannot be set again by $extName." );
940 if ( isset( $value[ExtensionRegistry::MERGE_STRATEGY] ) &&
941 $value[ExtensionRegistry::MERGE_STRATEGY] === 'array_merge_recursive' ) {
942 wfDeprecatedMsg(
943 "Using the array_merge_recursive merge strategy in extension.json and skin.json" .
944 " was deprecated in MediaWiki 1.42",
945 "1.42"
948 $this->globals[$key] = $value;
951 protected function extractPathBasedGlobal( $global, $dir, $paths ) {
952 foreach ( $paths as $path ) {
953 $this->globals[$global][] = "$dir/$path";
958 * Stores $value to $array; using array_merge_recursive() if $array already contains $name
960 * @param string $path
961 * @param string $name
962 * @param array $value
963 * @param array &$array
966 protected function storeToArrayRecursive( $path, $name, $value, &$array ) {
967 if ( !is_array( $value ) ) {
968 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
970 if ( isset( $array[$name] ) ) {
971 $array[$name] = array_merge_recursive( $array[$name], $value );
972 } else {
973 $array[$name] = $value;
978 * Stores $value to $array; using array_merge() if $array already contains $name
980 * @param string $path
981 * @param string $name
982 * @param array $value
983 * @param array &$array
985 * @throws InvalidArgumentException
987 protected function storeToArray( $path, $name, $value, &$array ) {
988 if ( !is_array( $value ) ) {
989 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
991 if ( isset( $array[$name] ) ) {
992 $array[$name] = array_merge( $array[$name], $value );
993 } else {
994 $array[$name] = $value;
999 * Returns the extracted autoload info.
1000 * The autoload info is returned as an associative array with three keys:
1001 * - files: a list of files to load, for use with Autoloader::loadFile()
1002 * - classes: a map of class names to files, for use with Autoloader::registerClass()
1003 * - namespaces: a map of namespace names to directories, for use
1004 * with Autoloader::registerNamespace()
1006 * @since 1.39
1008 * @param bool $includeDev
1010 * @return array[] The autoload info.
1012 public function getExtractedAutoloadInfo( bool $includeDev = false ): array {
1013 $autoload = $this->autoload;
1015 if ( $includeDev ) {
1016 $autoload['classes'] += $this->autoloadDev['classes'];
1017 $autoload['namespaces'] += $this->autoloadDev['namespaces'];
1019 // NOTE: This is here for completeness. Per MW 1.39,
1020 // $this->autoloadDev['files'] is always empty.
1021 // So avoid the performance hit of array_merge().
1022 if ( !empty( $this->autoloadDev['files'] ) ) {
1023 // NOTE: Don't use += with numeric keys!
1024 // Could use PHPUtils::pushArray.
1025 $autoload['files'] = array_merge(
1026 $autoload['files'],
1027 $this->autoloadDev['files']
1032 return $autoload;
1035 private function extractAutoload( array $info, string $dir ) {
1036 if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
1037 $file = "$dir/vendor/autoload.php";
1038 if ( file_exists( $file ) ) {
1039 $this->autoload['files'][] = $file;
1043 if ( isset( $info['AutoloadClasses'] ) ) {
1044 $paths = $this->applyPath( $info['AutoloadClasses'], $dir );
1045 $this->autoload['classes'] += $paths;
1048 if ( isset( $info['AutoloadNamespaces'] ) ) {
1049 $paths = $this->applyPath( $info['AutoloadNamespaces'], $dir );
1050 $this->autoload['namespaces'] += $paths;
1053 if ( isset( $info['TestAutoloadClasses'] ) ) {
1054 $paths = $this->applyPath( $info['TestAutoloadClasses'], $dir );
1055 $this->autoloadDev['classes'] += $paths;
1058 if ( isset( $info['TestAutoloadNamespaces'] ) ) {
1059 $paths = $this->applyPath( $info['TestAutoloadNamespaces'], $dir );
1060 $this->autoloadDev['namespaces'] += $paths;
1065 /** @deprecated class alias since 1.43 */
1066 class_alias( ExtensionProcessor::class, 'ExtensionProcessor' );