Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / registration / ExtensionProcessor.php
blob94c60a57f1f288ffc5de5969bafa67143b641531
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 * @param array $info
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'] );
600 } else {
601 $id = $ns['id'];
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;
643 } else {
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;
655 } else {
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;
673 } else {
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
714 * absolute paths
716 * @param string $dir
717 * @param array $info
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
731 * absolute paths
733 * @param string $dir
734 * @param array $info
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.
747 * @param string $dir
748 * @param array $info
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.
765 * @param array $info
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'] ?? [],
781 $rights
787 * @param string $dir
788 * @param array $info
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
800 * @param array $info
802 * @return string Name of thing
803 * @throws Exception
805 protected function extractCredits( $path, array $info ) {
806 $credits = [
807 'path' => $path,
808 'type' => 'other',
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;
830 return $name;
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
859 * @param array $info
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'] );
866 } else {
867 $prefix = 'wg';
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
881 * @param string $dir
883 * @return string[]
885 private function applyPath( array $value, string $dir ): array {
886 $result = [];
888 foreach ( $value as $k => $v ) {
889 $result[$k] = $dir . '/' . $v;
892 return $result;
896 * Set configuration settings for manifest_version == 2
898 * @todo In the future, this should be done via Config interfaces
900 * @param array $info
901 * @param string $dir
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 );
915 } else {
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' ) {
946 wfDeprecatedMsg(
947 "Using the array_merge_recursive merge strategy in extension.json and skin.json" .
948 " was deprecated in MediaWiki 1.42",
949 "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 );
976 } else {
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 );
997 } else {
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()
1010 * @since 1.39
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(
1030 $autoload['files'],
1031 $this->autoloadDev['files']
1036 return $autoload;
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' );