Merge "api: Check for post_max_size on api requests"
[mediawiki.git] / includes / registration / ExtensionProcessor.php
blob5902176b9fe203d8a4718f457301d740619efde2
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 'MessagePosterModule',
151 'MessagesDirs',
152 'OOUIThemePaths',
153 'QUnitTestModule',
154 'ResourceFileModulePaths',
155 'ResourceModuleSkinStyles',
156 'ResourceModules',
157 'ServiceWiringFiles',
161 * Stuff that is going to be set to $GLOBALS
163 * Some keys are pre-set to arrays, so we can += to them
165 * @var array
167 protected $globals = [
168 'wgExtensionMessagesFiles' => [],
169 'wgRestAPIAdditionalRouteFiles' => [],
170 'wgMessagesDirs' => [],
171 'TranslationAliasesDirs' => [],
175 * Things that should be define()'d
177 * @var array
179 protected $defines = [];
182 * Things to be called once the registration of these extensions is done
184 * Keyed by the name of the extension that it belongs to
186 * @var callable[]
188 protected $callbacks = [];
191 * @var array
193 protected $credits = [];
196 * Autoloader information.
197 * Each element is an array of strings.
198 * 'files' is just a list, 'classes' and 'namespaces' are associative.
200 * @var string[][]
202 protected $autoload = [
203 'files' => [],
204 'classes' => [],
205 'namespaces' => [],
209 * Autoloader information for development.
210 * Same structure as $autoload.
212 * @var string[][]
214 protected $autoloadDev = [
215 'files' => [],
216 'classes' => [],
217 'namespaces' => [],
221 * Anything else in the $info that hasn't
222 * already been processed
224 * @var array
226 protected $attributes = [];
229 * Extension attributes, keyed by name =>
230 * settings.
232 * @var array
234 protected $extAttributes = [];
237 * Extracts extension info from the given JSON file.
239 * @param string $path
241 * @return void
243 public function extractInfoFromFile( string $path ) {
244 $json = file_get_contents( $path );
245 $info = json_decode( $json, true );
247 if ( !$info ) {
248 throw new RuntimeException( "Failed to load JSON data from $path" );
251 $this->extractInfo( $path, $info, $info['manifest_version'] );
255 * @param string $path
256 * @param array $info
257 * @param int $version manifest_version for info
259 public function extractInfo( $path, array $info, $version ) {
260 $dir = dirname( $path );
261 $this->extractHooks( $info, $path );
262 $this->extractExtensionMessagesFiles( $dir, $info );
263 $this->extractRestModuleFiles( $dir, $info );
264 $this->extractMessagesDirs( $dir, $info );
265 $this->extractTranslationAliasesDirs( $dir, $info );
266 $this->extractSkins( $dir, $info );
267 $this->extractSkinImportPaths( $dir, $info );
268 $this->extractNamespaces( $info );
269 $this->extractImplicitRights( $info );
270 $this->extractResourceLoaderModules( $dir, $info );
271 if ( isset( $info['ServiceWiringFiles'] ) ) {
272 $this->extractPathBasedGlobal(
273 'wgServiceWiringFiles',
274 $dir,
275 $info['ServiceWiringFiles']
278 $name = $this->extractCredits( $path, $info );
279 if ( isset( $info['callback'] ) ) {
280 $this->callbacks[$name] = $info['callback'];
283 $this->extractAutoload( $info, $dir );
285 // config should be after all core globals are extracted,
286 // so duplicate setting detection will work fully
287 if ( $version >= 2 ) {
288 $this->extractConfig2( $info, $dir );
289 } else {
290 // $version === 1
291 $this->extractConfig1( $info );
294 // Record the extension name in the ParsoidModules property
295 if ( isset( $info['ParsoidModules'] ) ) {
296 foreach ( $info['ParsoidModules'] as &$module ) {
297 if ( is_string( $module ) ) {
298 $className = $module;
299 $module = [
300 'class' => $className,
303 $module['name'] = $name;
307 $this->extractForeignResourcesDir( $info, $name, $dir );
309 if ( $version >= 2 ) {
310 $this->extractAttributes( $path, $info );
313 foreach ( $info as $key => $val ) {
314 // If it's a global setting,
315 if ( in_array( $key, self::$globalSettings ) ) {
316 $this->storeToArrayRecursive( $path, "wg$key", $val, $this->globals );
317 continue;
319 // Ignore anything that starts with a @
320 if ( $key[0] === '@' ) {
321 continue;
324 if ( $version >= 2 ) {
325 // Only allowed attributes are set
326 if ( in_array( $key, self::CORE_ATTRIBS ) ) {
327 $this->storeToArray( $path, $key, $val, $this->attributes );
329 } else {
330 // version === 1
331 if ( !in_array( $key, self::NOT_ATTRIBS )
332 && !in_array( $key, self::CREDIT_ATTRIBS )
334 // If it's not disallowed, it's an attribute
335 $this->storeToArrayRecursive( $path, $key, $val, $this->attributes );
342 * @param string $path
343 * @param array $info
345 protected function extractAttributes( $path, array $info ) {
346 if ( isset( $info['attributes'] ) ) {
347 foreach ( $info['attributes'] as $extName => $value ) {
348 $this->storeToArrayRecursive( $path, $extName, $value, $this->extAttributes );
353 public function getExtractedInfo( bool $includeDev = false ) {
354 // Make sure the merge strategies are set
355 foreach ( $this->globals as $key => $val ) {
356 if ( isset( self::MERGE_STRATEGIES[$key] ) ) {
357 $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::MERGE_STRATEGIES[$key];
361 // Merge $this->extAttributes into $this->attributes depending on what is loaded
362 foreach ( $this->extAttributes as $extName => $value ) {
363 // Only set the attribute if $extName is loaded (and hence present in credits)
364 if ( isset( $this->credits[$extName] ) ) {
365 foreach ( $value as $attrName => $attrValue ) {
366 $this->storeToArrayRecursive(
367 '', // Don't provide a path since it's impossible to generate an error here
368 $extName . $attrName,
369 $attrValue,
370 $this->attributes
373 unset( $this->extAttributes[$extName] );
377 $autoload = $this->getExtractedAutoloadInfo( $includeDev );
379 return [
380 'globals' => $this->globals,
381 'defines' => $this->defines,
382 'callbacks' => $this->callbacks,
383 'credits' => $this->credits,
384 'attributes' => $this->attributes,
385 'autoloaderPaths' => $autoload['files'],
386 'autoloaderClasses' => $autoload['classes'],
387 'autoloaderNS' => $autoload['namespaces'],
391 public function getRequirements( array $info, $includeDev ) {
392 // Quick shortcuts
393 if ( !$includeDev || !isset( $info['dev-requires'] ) ) {
394 return $info['requires'] ?? [];
397 if ( !isset( $info['requires'] ) ) {
398 return $info['dev-requires'] ?? [];
401 // OK, we actually have to merge everything
402 $merged = [];
404 // Helper that combines version requirements by
405 // picking the non-null if one is, or combines
406 // the two. Note that it is not possible for
407 // both inputs to be null.
408 $pick = static function ( $a, $b ) {
409 if ( $a === null ) {
410 return $b;
411 } elseif ( $b === null ) {
412 return $a;
413 } else {
414 return "$a $b";
418 $req = $info['requires'];
419 $dev = $info['dev-requires'];
420 if ( isset( $req['MediaWiki'] ) || isset( $dev['MediaWiki'] ) ) {
421 $merged['MediaWiki'] = $pick(
422 $req['MediaWiki'] ?? null,
423 $dev['MediaWiki'] ?? null
427 $platform = array_merge(
428 array_keys( $req['platform'] ?? [] ),
429 array_keys( $dev['platform'] ?? [] )
431 if ( $platform ) {
432 foreach ( $platform as $pkey ) {
433 if ( $pkey === 'php' ) {
434 $value = $pick(
435 $req['platform']['php'] ?? null,
436 $dev['platform']['php'] ?? null
438 } else {
439 // Prefer dev value, but these should be constant
440 // anyway (ext-* and ability-*)
441 $value = $dev['platform'][$pkey] ?? $req['platform'][$pkey];
443 $merged['platform'][$pkey] = $value;
447 foreach ( [ 'extensions', 'skins' ] as $thing ) {
448 $things = array_merge(
449 array_keys( $req[$thing] ?? [] ),
450 array_keys( $dev[$thing] ?? [] )
452 foreach ( $things as $name ) {
453 $merged[$thing][$name] = $pick(
454 $req[$thing][$name] ?? null,
455 $dev[$thing][$name] ?? null
460 return $merged;
464 * When handler value is an array, set $wgHooks or Hooks attribute
465 * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook
466 * referencing a handler definition from 'HookHandler' attribute
468 * @param array $callback Handler
469 * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute
470 * @param string $name
471 * @param string $path extension.json file path
473 * @throws UnexpectedValueException
475 private function setArrayHookHandler(
476 array $callback,
477 array $hookHandlersAttr,
478 string $name,
479 string $path
481 if ( isset( $callback['handler'] ) ) {
482 $handlerName = $callback['handler'];
483 $handlerDefinition = $hookHandlersAttr[$handlerName] ?? false;
484 if ( !$handlerDefinition ) {
485 throw new UnexpectedValueException(
486 "Missing handler definition for $name in HookHandlers attribute in $path"
489 $callback['handler'] = $handlerDefinition;
490 $callback['extensionPath'] = $path;
491 $this->attributes['Hooks'][$name][] = $callback;
492 } else {
493 foreach ( $callback as $callable ) {
494 if ( is_array( $callable ) ) {
495 if ( isset( $callable['handler'] ) ) { // Non-legacy style handler
496 $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name, $path );
497 } else { // Legacy style handler array
498 $this->globals['wgHooks'][$name][] = $callable;
500 } elseif ( is_string( $callable ) ) {
501 $this->setStringHookHandler( $callable, $hookHandlersAttr, $name, $path );
508 * When handler value is a string, set $wgHooks or Hooks attribute.
509 * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook
510 * referencing a handler definition from 'HookHandler' attribute
512 * @param string $callback Handler
513 * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute
514 * @param string $name
515 * @param string $path
517 private function setStringHookHandler(
518 string $callback,
519 array $hookHandlersAttr,
520 string $name,
521 string $path
523 if ( isset( $hookHandlersAttr[$callback] ) ) {
524 $handler = [
525 'handler' => $hookHandlersAttr[$callback],
526 'extensionPath' => $path
528 $this->attributes['Hooks'][$name][] = $handler;
529 } else { // legacy style handler
530 $this->globals['wgHooks'][$name][] = $callback;
535 * Extract hook information from Hooks and HookHandler attributes.
536 * Store hook in $wgHooks if a legacy style handler or the 'Hooks' attribute if
537 * a non-legacy handler
539 * @param array $info attributes and associated values from extension.json
540 * @param string $path path to extension.json
542 protected function extractHooks( array $info, string $path ) {
543 $extName = $info['name'];
544 if ( isset( $info['Hooks'] ) ) {
545 $hookHandlersAttr = [];
546 foreach ( $info['HookHandlers'] ?? [] as $name => $def ) {
547 $hookHandlersAttr[$name] = [ 'name' => "$extName-$name" ] + $def;
549 foreach ( $info['Hooks'] as $name => $callback ) {
550 if ( is_string( $callback ) ) {
551 $this->setStringHookHandler( $callback, $hookHandlersAttr, $name, $path );
552 } elseif ( is_array( $callback ) ) {
553 $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name, $path );
557 if ( isset( $info['DeprecatedHooks'] ) ) {
558 $deprecatedHooks = [];
559 foreach ( $info['DeprecatedHooks'] as $name => $deprecatedHookInfo ) {
560 $deprecatedHookInfo += [ 'component' => $extName ];
561 $deprecatedHooks[$name] = $deprecatedHookInfo;
563 if ( isset( $this->attributes['DeprecatedHooks'] ) ) {
564 $this->attributes['DeprecatedHooks'] += $deprecatedHooks;
565 } else {
566 $this->attributes['DeprecatedHooks'] = $deprecatedHooks;
572 * Register namespaces with the appropriate global settings
574 * @param array $info
576 protected function extractNamespaces( array $info ) {
577 if ( isset( $info['namespaces'] ) ) {
578 foreach ( $info['namespaces'] as $ns ) {
579 if ( defined( $ns['constant'] ) ) {
580 // If the namespace constant is already defined, use it.
581 // This allows namespace IDs to be overwritten locally.
582 $id = constant( $ns['constant'] );
583 } else {
584 $id = $ns['id'];
586 $this->defines[ $ns['constant'] ] = $id;
588 if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
589 // If it is not conditional, register it
590 $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
592 if ( isset( $ns['movable'] ) && !$ns['movable'] ) {
593 $this->attributes['ImmovableNamespaces'][] = $id;
595 if ( isset( $ns['gender'] ) ) {
596 $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
598 if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
599 $this->globals['wgNamespacesWithSubpages'][$id] = true;
601 if ( isset( $ns['content'] ) && $ns['content'] ) {
602 $this->globals['wgContentNamespaces'][] = $id;
604 if ( isset( $ns['defaultcontentmodel'] ) ) {
605 $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
607 if ( isset( $ns['protection'] ) ) {
608 $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
610 if ( isset( $ns['capitallinkoverride'] ) ) {
611 $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
613 if ( isset( $ns['includable'] ) && !$ns['includable'] ) {
614 $this->globals['wgNonincludableNamespaces'][] = $id;
620 protected function extractResourceLoaderModules( $dir, array $info ) {
621 $defaultPaths = $info['ResourceFileModulePaths'] ?? false;
622 if ( isset( $defaultPaths['localBasePath'] ) ) {
623 if ( $defaultPaths['localBasePath'] === '' ) {
624 // Avoid double slashes (e.g. /extensions/Example//path)
625 $defaultPaths['localBasePath'] = $dir;
626 } else {
627 $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
631 foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
632 if ( isset( $info[$setting] ) ) {
633 foreach ( $info[$setting] as $name => $data ) {
634 if ( isset( $data['localBasePath'] ) ) {
635 if ( $data['localBasePath'] === '' ) {
636 // Avoid double slashes (e.g. /extensions/Example//path)
637 $data['localBasePath'] = $dir;
638 } else {
639 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
642 if ( $defaultPaths ) {
643 $data += $defaultPaths;
645 $this->attributes[$setting][$name] = $data;
650 if ( isset( $info['QUnitTestModule'] ) ) {
651 $data = $info['QUnitTestModule'];
652 if ( isset( $data['localBasePath'] ) ) {
653 if ( $data['localBasePath'] === '' ) {
654 // Avoid double slashes (e.g. /extensions/Example//path)
655 $data['localBasePath'] = $dir;
656 } else {
657 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
660 $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data;
663 if ( isset( $info['MessagePosterModule'] ) ) {
664 $data = $info['MessagePosterModule'];
665 $basePath = $data['localBasePath'] ?? '';
666 $baseDir = $basePath === '' ? $dir : "$dir/$basePath";
667 foreach ( $data['scripts'] ?? [] as $scripts ) {
668 $this->attributes['MessagePosterModule']['scripts'][] =
669 new FilePath( $scripts, $baseDir );
671 foreach ( $data['dependencies'] ?? [] as $dependency ) {
672 $this->attributes['MessagePosterModule']['dependencies'][] = $dependency;
677 protected function extractExtensionMessagesFiles( $dir, array $info ) {
678 if ( isset( $info['ExtensionMessagesFiles'] ) ) {
679 foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
680 $file = "$dir/$file";
682 $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
686 protected function extractRestModuleFiles( $dir, array $info ) {
687 $var = MainConfigNames::RestAPIAdditionalRouteFiles;
688 if ( isset( $info['RestModuleFiles'] ) ) {
689 foreach ( $info['RestModuleFiles'] as &$file ) {
690 $this->globals["wg$var"][] = "$dir/$file";
696 * Set message-related settings, which need to be expanded to use
697 * absolute paths
699 * @param string $dir
700 * @param array $info
702 protected function extractMessagesDirs( $dir, array $info ) {
703 if ( isset( $info['MessagesDirs'] ) ) {
704 foreach ( $info['MessagesDirs'] as $name => $files ) {
705 foreach ( (array)$files as $file ) {
706 $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
713 * Set localization related settings, which need to be expanded to use
714 * absolute paths
716 * @param string $dir
717 * @param array $info
719 protected function extractTranslationAliasesDirs( $dir, array $info ) {
720 foreach ( $info['TranslationAliasesDirs'] ?? [] as $name => $files ) {
721 foreach ( (array)$files as $file ) {
722 $this->globals['wgTranslationAliasesDirs'][$name][] = "$dir/$file";
728 * Extract skins and handle path correction for templateDirectory.
730 * @param string $dir
731 * @param array $info
733 protected function extractSkins( $dir, array $info ) {
734 if ( isset( $info['ValidSkinNames'] ) ) {
735 foreach ( $info['ValidSkinNames'] as $skinKey => $data ) {
736 if ( isset( $data['args'][0] ) ) {
737 $templateDirectory = $data['args'][0]['templateDirectory'] ?? 'templates';
738 $data['args'][0]['templateDirectory'] = $dir . '/' . $templateDirectory;
740 $this->globals['wgValidSkinNames'][$skinKey] = $data;
746 * Extract any user rights that should be granted implicitly.
748 * @param array $info
750 protected function extractImplicitRights( array $info ) {
751 // Rate limits are only configurable for rights that are either in wgImplicitRights
752 // or in wgAvailableRights. Extensions that define rate limits should not have to
753 // explicitly add them to wgImplicitRights as well, we can do that automatically.
755 if ( isset( $info['RateLimits'] ) ) {
756 $rights = array_keys( $info['RateLimits'] );
758 if ( isset( $info['AvailableRights'] ) ) {
759 $rights = array_diff( $rights, $info['AvailableRights'] );
762 $this->globals['wgImplicitRights'] = array_merge(
763 $this->globals['wgImplicitRights'] ?? [],
764 $rights
770 * @param string $dir
771 * @param array $info
773 protected function extractSkinImportPaths( $dir, array $info ) {
774 if ( isset( $info['SkinLessImportPaths'] ) ) {
775 foreach ( $info['SkinLessImportPaths'] as $skin => $subpath ) {
776 $this->attributes['SkinLessImportPaths'][$skin] = "$dir/$subpath";
782 * @param string $path
783 * @param array $info
785 * @return string Name of thing
786 * @throws Exception
788 protected function extractCredits( $path, array $info ) {
789 $credits = [
790 'path' => $path,
791 'type' => 'other',
793 foreach ( self::CREDIT_ATTRIBS as $attr ) {
794 if ( isset( $info[$attr] ) ) {
795 $credits[$attr] = $info[$attr];
799 $name = $credits['name'];
801 // If someone is loading the same thing twice, throw
802 // a nice error (T121493)
803 if ( isset( $this->credits[$name] ) ) {
804 $firstPath = $this->credits[$name]['path'];
805 $secondPath = $credits['path'];
806 throw new InvalidArgumentException(
807 "It was attempted to load $name twice, from $firstPath and $secondPath."
811 $this->credits[$name] = $credits;
813 return $name;
816 protected function extractForeignResourcesDir( array $info, string $name, string $dir ): void {
817 if ( array_key_exists( 'ForeignResourcesDir', $info ) ) {
818 if ( !is_string( $info['ForeignResourcesDir'] ) ) {
819 throw new InvalidArgumentException( "Incorrect ForeignResourcesDir type, must be a string (in $name)" );
821 $this->attributes['ForeignResourcesDir'][$name] = "{$dir}/{$info['ForeignResourcesDir']}";
826 * Set configuration settings for manifest_version == 1
828 * @todo In the future, this should be done via Config interfaces
830 * @param array $info
832 protected function extractConfig1( array $info ) {
833 if ( isset( $info['config'] ) ) {
834 if ( isset( $info['config']['_prefix'] ) ) {
835 $prefix = $info['config']['_prefix'];
836 unset( $info['config']['_prefix'] );
837 } else {
838 $prefix = 'wg';
840 foreach ( $info['config'] as $key => $val ) {
841 if ( $key[0] !== '@' ) {
842 $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
849 * Applies a base path to the given string or string array.
851 * @param string[] $value
852 * @param string $dir
854 * @return string[]
856 private function applyPath( array $value, string $dir ): array {
857 $result = [];
859 foreach ( $value as $k => $v ) {
860 $result[$k] = $dir . '/' . $v;
863 return $result;
867 * Set configuration settings for manifest_version == 2
869 * @todo In the future, this should be done via Config interfaces
871 * @param array $info
872 * @param string $dir
874 protected function extractConfig2( array $info, $dir ) {
875 $prefix = $info['config_prefix'] ?? 'wg';
876 if ( isset( $info['config'] ) ) {
877 foreach ( $info['config'] as $key => $data ) {
878 if ( !array_key_exists( 'value', $data ) ) {
879 throw new UnexpectedValueException( "Missing value for config $key" );
882 $value = $data['value'];
883 if ( isset( $data['path'] ) && $data['path'] ) {
884 if ( is_array( $value ) ) {
885 $value = $this->applyPath( $value, $dir );
886 } else {
887 $value = "$dir/$value";
890 if ( isset( $data['merge_strategy'] ) ) {
891 $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
893 $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
894 $data['providedby'] = $info['name'];
895 if ( isset( $info['ConfigRegistry'][0] ) ) {
896 $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0];
903 * Helper function to set a value to a specific global config variable if it isn't set already.
905 * @param string $key The config key with the prefix and anything
906 * @param mixed $value The value of the config
907 * @param string $extName Name of the extension
909 private function addConfigGlobal( $key, $value, $extName ) {
910 if ( array_key_exists( $key, $this->globals ) ) {
911 throw new RuntimeException(
912 "The configuration setting '$key' was already set by MediaWiki core or"
913 . " another extension, and cannot be set again by $extName." );
915 if ( isset( $value[ExtensionRegistry::MERGE_STRATEGY] ) &&
916 $value[ExtensionRegistry::MERGE_STRATEGY] === 'array_merge_recursive' ) {
917 wfDeprecatedMsg(
918 "Using the array_merge_recursive merge strategy in extension.json and skin.json" .
919 " was deprecated in MediaWiki 1.42",
920 "1.42"
923 $this->globals[$key] = $value;
926 protected function extractPathBasedGlobal( $global, $dir, $paths ) {
927 foreach ( $paths as $path ) {
928 $this->globals[$global][] = "$dir/$path";
933 * Stores $value to $array; using array_merge_recursive() if $array already contains $name
935 * @param string $path
936 * @param string $name
937 * @param array $value
938 * @param array &$array
941 protected function storeToArrayRecursive( $path, $name, $value, &$array ) {
942 if ( !is_array( $value ) ) {
943 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
945 if ( isset( $array[$name] ) ) {
946 $array[$name] = array_merge_recursive( $array[$name], $value );
947 } else {
948 $array[$name] = $value;
953 * Stores $value to $array; using array_merge() if $array already contains $name
955 * @param string $path
956 * @param string $name
957 * @param array $value
958 * @param array &$array
960 * @throws InvalidArgumentException
962 protected function storeToArray( $path, $name, $value, &$array ) {
963 if ( !is_array( $value ) ) {
964 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
966 if ( isset( $array[$name] ) ) {
967 $array[$name] = array_merge( $array[$name], $value );
968 } else {
969 $array[$name] = $value;
974 * Returns the extracted autoload info.
975 * The autoload info is returned as an associative array with three keys:
976 * - files: a list of files to load, for use with Autoloader::loadFile()
977 * - classes: a map of class names to files, for use with Autoloader::registerClass()
978 * - namespaces: a map of namespace names to directories, for use
979 * with Autoloader::registerNamespace()
981 * @since 1.39
983 * @param bool $includeDev
985 * @return array[] The autoload info.
987 public function getExtractedAutoloadInfo( bool $includeDev = false ): array {
988 $autoload = $this->autoload;
990 if ( $includeDev ) {
991 $autoload['classes'] += $this->autoloadDev['classes'];
992 $autoload['namespaces'] += $this->autoloadDev['namespaces'];
994 // NOTE: This is here for completeness. Per MW 1.39,
995 // $this->autoloadDev['files'] is always empty.
996 // So avoid the performance hit of array_merge().
997 if ( !empty( $this->autoloadDev['files'] ) ) {
998 // NOTE: Don't use += with numeric keys!
999 // Could use PHPUtils::pushArray.
1000 $autoload['files'] = array_merge(
1001 $autoload['files'],
1002 $this->autoloadDev['files']
1007 return $autoload;
1011 * @param array $info
1012 * @param string $dir
1014 private function extractAutoload( array $info, string $dir ) {
1015 if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
1016 $file = "$dir/vendor/autoload.php";
1017 if ( file_exists( $file ) ) {
1018 $this->autoload['files'][] = $file;
1022 if ( isset( $info['AutoloadClasses'] ) ) {
1023 $paths = $this->applyPath( $info['AutoloadClasses'], $dir );
1024 $this->autoload['classes'] += $paths;
1027 if ( isset( $info['AutoloadNamespaces'] ) ) {
1028 $paths = $this->applyPath( $info['AutoloadNamespaces'], $dir );
1029 $this->autoload['namespaces'] += $paths;
1032 if ( isset( $info['TestAutoloadClasses'] ) ) {
1033 $paths = $this->applyPath( $info['TestAutoloadClasses'], $dir );
1034 $this->autoloadDev['classes'] += $paths;
1037 if ( isset( $info['TestAutoloadNamespaces'] ) ) {
1038 $paths = $this->applyPath( $info['TestAutoloadNamespaces'], $dir );
1039 $this->autoloadDev['namespaces'] += $paths;
1044 /** @deprecated class alias since 1.43 */
1045 class_alias( ExtensionProcessor::class, 'ExtensionProcessor' );