MediaWiki master
ExtensionProcessor.php
Go to the documentation of this file.
1<?php
2
4
5use Exception;
6use InvalidArgumentException;
9use RuntimeException;
10use UnexpectedValueException;
11
18class ExtensionProcessor implements Processor {
19
25 protected static $globalSettings = [
77 ];
78
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',
94 'NotificationHandlers',
95 'NotificationMiddleware',
96 'RecentChangeSources',
97 'ShadowPageProviders',
98 'CodeHighlightProviders',
99 ];
100
107 protected const MERGE_STRATEGIES = [
108 'wgAddGroups' => 'array_merge_recursive',
109 'wgAuthManagerAutoConfig' => 'array_plus_2d',
110 'wgCapitalLinkOverrides' => 'array_plus',
111 'wgExtraGenderNamespaces' => 'array_plus',
112 'wgGrantPermissions' => 'array_plus_2d',
113 'wgGroupPermissions' => 'array_plus_2d',
114 'wgHooks' => 'array_merge_recursive',
115 'wgNamespaceContentModels' => 'array_plus',
116 'wgNamespaceProtection' => 'array_plus',
117 'wgNamespacesWithSubpages' => 'array_plus',
118 'wgPasswordPolicy' => 'array_merge_recursive',
119 'wgRateLimits' => 'array_plus_2d',
120 'wgRemoveGroups' => 'array_merge_recursive',
121 'wgRevokePermissions' => 'array_plus_2d',
122 ];
123
127 protected const CREDIT_ATTRIBS = [
128 'type',
129 'author',
130 'description',
131 'descriptionmsg',
132 'license-name',
133 'name',
134 'namemsg',
135 'url',
136 'version',
137 ];
138
143 protected const NOT_ATTRIBS = [
144 'callback',
145 'config',
146 'config_prefix',
147 'load_composer_autoloader',
148 'manifest_version',
149 'namespaces',
150 'requires',
151 'AutoloadClasses',
152 'AutoloadNamespaces',
153 'ExtensionMessagesFiles',
154 'TranslationAliasesDirs',
155 'ForeignResourcesDir',
156 'Hooks',
157 'DomainEventIngresses',
158 'MessagePosterModule',
159 'MessagesDirs',
160 'OOUIThemePaths',
161 'QUnitTestModule',
162 'ResourceFileModulePaths',
163 'ResourceModuleSkinStyles',
164 'ResourceModules',
165 'ServiceWiringFiles',
166 ];
167
175 protected $globals = [
176 'wgExtensionMessagesFiles' => [],
177 'wgRestAPIAdditionalRouteFiles' => [],
178 'wgMessagesDirs' => [],
179 'TranslationAliasesDirs' => [],
180 ];
181
187 protected $defines = [];
188
196 protected $callbacks = [];
197
201 protected $credits = [];
202
210 protected $autoload = [
211 'files' => [],
212 'classes' => [],
213 'namespaces' => [],
214 ];
215
222 protected $autoloadDev = [
223 'files' => [],
224 'classes' => [],
225 'namespaces' => [],
226 ];
227
234 protected $attributes = [];
235
242 protected $extAttributes = [];
243
251 public function extractInfoFromFile( string $path ) {
252 $json = file_get_contents( $path );
253 $info = json_decode( $json, true );
254
255 if ( !$info ) {
256 throw new RuntimeException( "Failed to load JSON data from $path" );
257 }
258
259 $this->extractInfo( $path, $info, $info['manifest_version'] );
260 }
261
267 public function extractInfo( $path, array $info, $version ) {
268 $dir = dirname( $path );
269 $this->extractHooks( $info, $path );
270 $this->extractDomainEventIngresses( $info, $path );
271 $this->extractExtensionMessagesFiles( $dir, $info );
272 $this->extractRestModuleFiles( $dir, $info );
273 $this->extractMessagesDirs( $dir, $info );
274 $this->extractTranslationAliasesDirs( $dir, $info );
275 $this->extractSkins( $dir, $info );
276 $this->extractSkinImportPaths( $dir, $info );
277 $this->extractNamespaces( $info );
278 $this->extractImplicitRights( $info );
279 $this->extractResourceLoaderModules( $dir, $info );
280 $this->extractInstallerTasks( $dir, $info );
281 if ( isset( $info['ServiceWiringFiles'] ) ) {
283 'wgServiceWiringFiles',
284 $dir,
285 $info['ServiceWiringFiles']
286 );
287 }
288 $name = $this->extractCredits( $path, $info );
289 if ( isset( $info['callback'] ) ) {
290 $this->callbacks[$name] = $info['callback'];
291 }
292
293 $this->extractAutoload( $info, $dir );
294
295 // config should be after all core globals are extracted,
296 // so duplicate setting detection will work fully
297 if ( $version >= 2 ) {
298 $this->extractConfig2( $info, $dir );
299 } else {
300 // $version === 1
301 $this->extractConfig1( $info );
302 }
303
304 // Record the extension name in the ParsoidModules property
305 if ( isset( $info['ParsoidModules'] ) ) {
306 foreach ( $info['ParsoidModules'] as &$module ) {
307 if ( is_string( $module ) ) {
308 $className = $module;
309 $module = [
310 'class' => $className,
311 ];
312 }
313 $module['name'] ??= $name;
314 $module['extension-name'] = $name;
315 }
316 }
317
318 $this->extractForeignResourcesDir( $info, $name, $dir );
319
320 if ( $version >= 2 ) {
321 $this->extractAttributes( $path, $info );
322 }
323
324 foreach ( $info as $key => $val ) {
325 // If it's a global setting,
326 if ( in_array( $key, self::$globalSettings ) ) {
327 $this->storeToArrayRecursive( $path, "wg$key", $val, $this->globals );
328 continue;
329 }
330 // Ignore anything that starts with a @
331 if ( $key[0] === '@' ) {
332 continue;
333 }
334
335 if ( $version >= 2 ) {
336 // Only allowed attributes are set
337 if ( in_array( $key, self::CORE_ATTRIBS ) ) {
338 $this->storeToArray( $path, $key, $val, $this->attributes );
339 }
340 } else {
341 // version === 1
342 if ( !in_array( $key, self::NOT_ATTRIBS )
343 && !in_array( $key, self::CREDIT_ATTRIBS )
344 ) {
345 // If it's not disallowed, it's an attribute
346 $this->storeToArrayRecursive( $path, $key, $val, $this->attributes );
347 }
348 }
349 }
350 }
351
356 protected function extractAttributes( $path, array $info ) {
357 if ( isset( $info['attributes'] ) ) {
358 foreach ( $info['attributes'] as $extName => $value ) {
359 $this->storeToArrayRecursive( $path, $extName, $value, $this->extAttributes );
360 }
361 }
362 }
363
365 public function getExtractedInfo( bool $includeDev = false ) {
366 // Make sure the merge strategies are set
367 foreach ( $this->globals as $key => $val ) {
368 if ( isset( self::MERGE_STRATEGIES[$key] ) ) {
369 $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::MERGE_STRATEGIES[$key];
370 }
371 }
372
373 // Merge $this->extAttributes into $this->attributes depending on what is loaded
374 foreach ( $this->extAttributes as $extName => $value ) {
375 // Only set the attribute if $extName is loaded (and hence present in credits)
376 if ( isset( $this->credits[$extName] ) ) {
377 foreach ( $value as $attrName => $attrValue ) {
379 '', // Don't provide a path since it's impossible to generate an error here
380 $extName . $attrName,
381 $attrValue,
382 $this->attributes
383 );
384 }
385 unset( $this->extAttributes[$extName] );
386 }
387 }
388
389 $autoload = $this->getExtractedAutoloadInfo( $includeDev );
390
391 return [
392 'globals' => $this->globals,
393 'defines' => $this->defines,
394 'callbacks' => $this->callbacks,
395 'credits' => $this->credits,
396 'attributes' => $this->attributes,
397 'autoloaderPaths' => $autoload['files'],
398 'autoloaderClasses' => $autoload['classes'],
399 'autoloaderNS' => $autoload['namespaces'],
400 ];
401 }
402
404 public function getRequirements( array $info, $includeDev ) {
405 // Quick shortcuts
406 if ( !$includeDev || !isset( $info['dev-requires'] ) ) {
407 return $info['requires'] ?? [];
408 }
409
410 if ( !isset( $info['requires'] ) ) {
411 return $info['dev-requires'] ?? [];
412 }
413
414 // OK, we actually have to merge everything
415 $merged = [];
416
417 // Helper that combines version requirements by
418 // picking the non-null if one is, or combines
419 // the two. Note that it is not possible for
420 // both inputs to be null.
421 $pick = static function ( $a, $b ) {
422 if ( $a === null ) {
423 return $b;
424 } elseif ( $b === null ) {
425 return $a;
426 } else {
427 return "$a $b";
428 }
429 };
430
431 $req = $info['requires'];
432 $dev = $info['dev-requires'];
433 if ( isset( $req['MediaWiki'] ) || isset( $dev['MediaWiki'] ) ) {
434 $merged['MediaWiki'] = $pick(
435 $req['MediaWiki'] ?? null,
436 $dev['MediaWiki'] ?? null
437 );
438 }
439
440 $platform = array_merge(
441 array_keys( $req['platform'] ?? [] ),
442 array_keys( $dev['platform'] ?? [] )
443 );
444 if ( $platform ) {
445 foreach ( $platform as $pkey ) {
446 if ( $pkey === 'php' ) {
447 $value = $pick(
448 $req['platform']['php'] ?? null,
449 $dev['platform']['php'] ?? null
450 );
451 } else {
452 // Prefer dev value, but these should be constant
453 // anyway (ext-* and ability-*)
454 $value = $dev['platform'][$pkey] ?? $req['platform'][$pkey];
455 }
456 $merged['platform'][$pkey] = $value;
457 }
458 }
459
460 foreach ( [ 'extensions', 'skins' ] as $thing ) {
461 $things = array_merge(
462 array_keys( $req[$thing] ?? [] ),
463 array_keys( $dev[$thing] ?? [] )
464 );
465 foreach ( $things as $name ) {
466 $merged[$thing][$name] = $pick(
467 $req[$thing][$name] ?? null,
468 $dev[$thing][$name] ?? null
469 );
470 }
471 }
472
473 return $merged;
474 }
475
488 private function setArrayHookHandler(
489 array $callback,
490 array $hookHandlersAttr,
491 string $name,
492 string $path
493 ) {
494 if ( isset( $callback['handler'] ) ) {
495 $handlerName = $callback['handler'];
496 $handlerDefinition = $hookHandlersAttr[$handlerName] ?? false;
497 if ( !$handlerDefinition ) {
498 throw new UnexpectedValueException(
499 "Missing handler definition for $name in HookHandlers attribute in $path"
500 );
501 }
502 $callback['handler'] = $handlerDefinition;
503 $callback['extensionPath'] = $path;
504 $this->attributes['Hooks'][$name][] = $callback;
505 } else {
506 foreach ( $callback as $callable ) {
507 if ( is_array( $callable ) ) {
508 if ( isset( $callable['handler'] ) ) { // Non-legacy style handler
509 $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name, $path );
510 } else { // Legacy style handler array
511 $this->globals['wgHooks'][$name][] = $callable;
512 }
513 } elseif ( is_string( $callable ) ) {
514 $this->setStringHookHandler( $callable, $hookHandlersAttr, $name, $path );
515 }
516 }
517 }
518 }
519
530 private function setStringHookHandler(
531 string $callback,
532 array $hookHandlersAttr,
533 string $name,
534 string $path
535 ) {
536 if ( isset( $hookHandlersAttr[$callback] ) ) {
537 $handler = [
538 'handler' => $hookHandlersAttr[$callback],
539 'extensionPath' => $path
540 ];
541 $this->attributes['Hooks'][$name][] = $handler;
542 } else { // legacy style handler
543 $this->globals['wgHooks'][$name][] = $callback;
544 }
545 }
546
555 protected function extractHooks( array $info, string $path ) {
556 $extName = $info['name'];
557 if ( isset( $info['Hooks'] ) ) {
558 $hookHandlersAttr = [];
559 foreach ( $info['HookHandlers'] ?? [] as $name => $def ) {
560 $hookHandlersAttr[$name] = [ 'name' => "$extName-$name" ] + $def;
561 }
562 foreach ( $info['Hooks'] as $name => $callback ) {
563 if ( is_string( $callback ) ) {
564 $this->setStringHookHandler( $callback, $hookHandlersAttr, $name, $path );
565 } elseif ( is_array( $callback ) ) {
566 $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name, $path );
567 }
568 }
569 }
570 if ( isset( $info['DeprecatedHooks'] ) ) {
571 $deprecatedHooks = [];
572 foreach ( $info['DeprecatedHooks'] as $name => $deprecatedHookInfo ) {
573 $deprecatedHookInfo += [ 'component' => $extName ];
574 $deprecatedHooks[$name] = $deprecatedHookInfo;
575 }
576 if ( isset( $this->attributes['DeprecatedHooks'] ) ) {
577 $this->attributes['DeprecatedHooks'] += $deprecatedHooks;
578 } else {
579 $this->attributes['DeprecatedHooks'] = $deprecatedHooks;
580 }
581 }
582 }
583
590 protected function extractDomainEventIngresses( array $info, string $path ) {
591 $this->attributes['DomainEventIngresses'] ??= [];
592 foreach ( $info['DomainEventIngresses'] ?? [] as $subscriber ) {
593 $subscriber['extensionPath'] = $path;
594 $this->attributes['DomainEventIngresses'][] = $subscriber;
595 }
596 }
597
601 protected function extractNamespaces( array $info ) {
602 if ( isset( $info['namespaces'] ) ) {
603 foreach ( $info['namespaces'] as $ns ) {
604 if ( defined( $ns['constant'] ) ) {
605 // If the namespace constant is already defined, use it.
606 // This allows namespace IDs to be overwritten locally.
607 $id = constant( $ns['constant'] );
608 } else {
609 $id = $ns['id'];
610 }
611 $this->defines[ $ns['constant'] ] = $id;
612
613 if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
614 // If it is not conditional, register it
615 $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
616 }
617 if ( isset( $ns['movable'] ) && !$ns['movable'] ) {
618 $this->attributes['ImmovableNamespaces'][] = $id;
619 }
620 if ( isset( $ns['gender'] ) ) {
621 $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
622 }
623 if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
624 $this->globals['wgNamespacesWithSubpages'][$id] = true;
625 }
626 if ( isset( $ns['content'] ) && $ns['content'] ) {
627 $this->globals['wgContentNamespaces'][] = $id;
628 }
629 if ( isset( $ns['defaultcontentmodel'] ) ) {
630 $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
631 }
632 if ( isset( $ns['protection'] ) ) {
633 $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
634 }
635 if ( isset( $ns['capitallinkoverride'] ) ) {
636 $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
637 }
638 if ( isset( $ns['includable'] ) && !$ns['includable'] ) {
639 $this->globals['wgNonincludableNamespaces'][] = $id;
640 }
641 }
642 }
643 }
644
645 protected function extractResourceLoaderModules( string $dir, array $info ) {
646 $defaultPaths = $info['ResourceFileModulePaths'] ?? false;
647 if ( isset( $defaultPaths['localBasePath'] ) ) {
648 if ( $defaultPaths['localBasePath'] === '' ) {
649 // Avoid double slashes (e.g. /extensions/Example//path)
650 $defaultPaths['localBasePath'] = $dir;
651 } else {
652 $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
653 }
654 }
655
656 foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
657 if ( isset( $info[$setting] ) ) {
658 foreach ( $info[$setting] as $name => $data ) {
659 if ( isset( $data['localBasePath'] ) ) {
660 if ( $data['localBasePath'] === '' ) {
661 // Avoid double slashes (e.g. /extensions/Example//path)
662 $data['localBasePath'] = $dir;
663 } else {
664 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
665 }
666 }
667 if ( $defaultPaths ) {
668 $data += $defaultPaths;
669 }
670 $this->attributes[$setting][$name] = $data;
671 }
672 }
673 }
674
675 if ( isset( $info['QUnitTestModule'] ) ) {
676 $data = $info['QUnitTestModule'];
677 if ( isset( $data['localBasePath'] ) ) {
678 if ( $data['localBasePath'] === '' ) {
679 // Avoid double slashes (e.g. /extensions/Example//path)
680 $data['localBasePath'] = $dir;
681 } else {
682 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
683 }
684 }
685 $this->attributes['QUnitTestModule']["test.{$info['name']}"] = $data;
686 }
687
688 if ( isset( $info['MessagePosterModule'] ) ) {
689 $data = $info['MessagePosterModule'];
690 $basePath = $data['localBasePath'] ?? '';
691 $baseDir = $basePath === '' ? $dir : "$dir/$basePath";
692 foreach ( $data['scripts'] ?? [] as $scripts ) {
693 $this->attributes['MessagePosterModule']['scripts'][] =
694 new FilePath( $scripts, $baseDir );
695 }
696 foreach ( $data['dependencies'] ?? [] as $dependency ) {
697 $this->attributes['MessagePosterModule']['dependencies'][] = $dependency;
698 }
699 }
700 }
701
702 protected function extractExtensionMessagesFiles( string $dir, array $info ) {
703 if ( isset( $info['ExtensionMessagesFiles'] ) ) {
704 foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
705 $file = "$dir/$file";
706 }
707 $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
708 }
709 }
710
711 protected function extractRestModuleFiles( string $dir, array $info ) {
713 if ( isset( $info['RestModuleFiles'] ) ) {
714 foreach ( $info['RestModuleFiles'] as &$file ) {
715 $this->globals["wg$var"][] = "$dir/$file";
716 }
717 }
718 }
719
727 protected function extractMessagesDirs( $dir, array $info ) {
728 if ( isset( $info['MessagesDirs'] ) ) {
729 foreach ( $info['MessagesDirs'] as $name => $files ) {
730 foreach ( (array)$files as $file ) {
731 $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
732 }
733 }
734 }
735 }
736
744 protected function extractTranslationAliasesDirs( $dir, array $info ) {
745 foreach ( $info['TranslationAliasesDirs'] ?? [] as $name => $files ) {
746 foreach ( (array)$files as $file ) {
747 $this->globals['wgTranslationAliasesDirs'][$name][] = "$dir/$file";
748 }
749 }
750 }
751
758 protected function extractSkins( $dir, array $info ) {
759 if ( isset( $info['ValidSkinNames'] ) ) {
760 foreach ( $info['ValidSkinNames'] as $skinKey => $data ) {
761 if ( isset( $data['args'][0] ) ) {
762 $templateDirectory = $data['args'][0]['templateDirectory'] ?? 'templates';
763 $data['args'][0]['templateDirectory'] = $dir . '/' . $templateDirectory;
764 }
765 $this->globals['wgValidSkinNames'][$skinKey] = $data;
766 }
767 }
768 }
769
773 protected function extractImplicitRights( array $info ) {
774 // Rate limits are only configurable for rights that are either in wgImplicitRights
775 // or in wgAvailableRights. Extensions that define rate limits should not have to
776 // explicitly add them to wgImplicitRights as well, we can do that automatically.
777
778 if ( isset( $info['RateLimits'] ) ) {
779 $rights = array_keys( $info['RateLimits'] );
780
781 if ( isset( $info['AvailableRights'] ) ) {
782 $rights = array_diff( $rights, $info['AvailableRights'] );
783 }
784
785 $this->globals['wgImplicitRights'] = array_merge(
786 $this->globals['wgImplicitRights'] ?? [],
787 $rights
788 );
789 }
790 }
791
796 protected function extractSkinImportPaths( $dir, array $info ) {
797 if ( isset( $info['SkinLessImportPaths'] ) ) {
798 foreach ( $info['SkinLessImportPaths'] as $skin => $subpath ) {
799 $this->attributes['SkinLessImportPaths'][$skin] = "$dir/$subpath";
800 }
801 }
802 }
803
811 protected function extractCredits( $path, array $info ) {
812 $credits = [
813 'path' => $path,
814 'type' => 'other',
815 ];
816 foreach ( self::CREDIT_ATTRIBS as $attr ) {
817 if ( isset( $info[$attr] ) ) {
818 $credits[$attr] = $info[$attr];
819 }
820 }
821
822 $name = $credits['name'];
823
824 // If someone is loading the same thing twice, throw
825 // a nice error (T121493)
826 if ( isset( $this->credits[$name] ) ) {
827 $firstPath = $this->credits[$name]['path'];
828 $secondPath = $credits['path'];
829 throw new InvalidArgumentException(
830 "It was attempted to load $name twice, from $firstPath and $secondPath."
831 );
832 }
833
834 $this->credits[$name] = $credits;
835
836 return $name;
837 }
838
839 protected function extractForeignResourcesDir( array $info, string $name, string $dir ): void {
840 if ( array_key_exists( 'ForeignResourcesDir', $info ) ) {
841 if ( !is_string( $info['ForeignResourcesDir'] ) ) {
842 throw new InvalidArgumentException( "Incorrect ForeignResourcesDir type, must be a string (in $name)" );
843 }
844 $this->attributes['ForeignResourcesDir'][$name] = "{$dir}/{$info['ForeignResourcesDir']}";
845 }
846 }
847
848 protected function extractInstallerTasks( string $path, array $info ): void {
849 if ( isset( $info['InstallerTasks'] ) ) {
850 // Use a fixed path for the schema base path for now. This could be
851 // made configurable if there were a use case for that.
852 $schemaBasePath = $path . '/sql';
853 foreach ( $info['InstallerTasks'] as $taskSpec ) {
854 $this->attributes['InstallerTasks'][]
855 = $taskSpec + [ 'schemaBasePath' => $schemaBasePath ];
856 }
857 }
858 }
859
867 protected function extractConfig1( array $info ) {
868 if ( isset( $info['config'] ) ) {
869 if ( isset( $info['config']['_prefix'] ) ) {
870 $prefix = $info['config']['_prefix'];
871 unset( $info['config']['_prefix'] );
872 } else {
873 $prefix = 'wg';
874 }
875 foreach ( $info['config'] as $key => $val ) {
876 if ( $key[0] !== '@' ) {
877 $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
878 }
879 }
880 }
881 }
882
892 private function applyPath( array $value, string $dir, bool $ensurePathSuffix = false ): array {
893 $result = [];
894
895 foreach ( $value as $k => $v ) {
896 if ( $ensurePathSuffix && !str_ends_with( $v, '/' ) ) {
897 $v .= '/';
898 }
899 $result[$k] = $dir . '/' . $v;
900 }
901
902 return $result;
903 }
904
913 protected function extractConfig2( array $info, $dir ) {
914 $prefix = $info['config_prefix'] ?? 'wg';
915 if ( isset( $info['config'] ) ) {
916 foreach ( $info['config'] as $key => $data ) {
917 if ( !array_key_exists( 'value', $data ) ) {
918 throw new UnexpectedValueException( "Missing value for config $key" );
919 }
920
921 $value = $data['value'];
922 if ( isset( $data['path'] ) && $data['path'] ) {
923 if ( is_array( $value ) ) {
924 $value = $this->applyPath( $value, $dir );
925 } else {
926 $value = "$dir/$value";
927 }
928 }
929 if ( isset( $data['merge_strategy'] ) ) {
930 $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
931 }
932 $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
933 }
934 }
935 }
936
944 private function addConfigGlobal( $key, $value, $extName ) {
945 if ( array_key_exists( $key, $this->globals ) ) {
946 throw new RuntimeException(
947 "The configuration setting '$key' was already set by MediaWiki core or"
948 . " another extension, and cannot be set again by $extName." );
949 }
950 if ( isset( $value[ExtensionRegistry::MERGE_STRATEGY] ) &&
951 $value[ExtensionRegistry::MERGE_STRATEGY] === 'array_merge_recursive' ) {
953 "Using the array_merge_recursive merge strategy in extension.json and skin.json" .
954 " was deprecated in MediaWiki 1.42",
955 "1.42"
956 );
957 }
958 $this->globals[$key] = $value;
959 }
960
961 protected function extractPathBasedGlobal( string $global, string $dir, array $paths ) {
962 foreach ( $paths as $path ) {
963 $this->globals[$global][] = "$dir/$path";
964 }
965 }
966
975 protected function storeToArrayRecursive( $path, $name, $value, &$array ) {
976 if ( !is_array( $value ) ) {
977 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
978 }
979 if ( isset( $array[$name] ) ) {
980 $array[$name] = array_merge_recursive( $array[$name], $value );
981 } else {
982 $array[$name] = $value;
983 }
984 }
985
996 protected function storeToArray( $path, $name, $value, &$array ) {
997 if ( !is_array( $value ) ) {
998 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
999 }
1000 if ( isset( $array[$name] ) ) {
1001 $array[$name] = array_merge( $array[$name], $value );
1002 } else {
1003 $array[$name] = $value;
1004 }
1005 }
1006
1021 public function getExtractedAutoloadInfo( bool $includeDev = false ): array {
1022 $autoload = $this->autoload;
1023
1024 if ( $includeDev ) {
1025 $autoload['classes'] += $this->autoloadDev['classes'];
1026 $autoload['namespaces'] += $this->autoloadDev['namespaces'];
1027
1028 // NOTE: This is here for completeness. Per MW 1.39,
1029 // $this->autoloadDev['files'] is always empty.
1030 // So avoid the performance hit of array_merge().
1031 if ( !empty( $this->autoloadDev['files'] ) ) {
1032 // NOTE: Don't use += with numeric keys!
1033 // Could use PHPUtils::pushArray.
1034 $autoload['files'] = array_merge(
1035 $autoload['files'],
1036 $this->autoloadDev['files']
1037 );
1038 }
1039 }
1040
1041 return $autoload;
1042 }
1043
1044 private function extractAutoload( array $info, string $dir ) {
1045 if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
1046 $file = "$dir/vendor/autoload.php";
1047 if ( file_exists( $file ) ) {
1048 $this->autoload['files'][] = $file;
1049 }
1050 }
1051
1052 if ( isset( $info['AutoloadClasses'] ) ) {
1053 $paths = $this->applyPath( $info['AutoloadClasses'], $dir );
1054 $this->autoload['classes'] += $paths;
1055 }
1056
1057 if ( isset( $info['AutoloadNamespaces'] ) ) {
1058 $paths = $this->applyPath( $info['AutoloadNamespaces'], $dir, true );
1059 $this->autoload['namespaces'] += $paths;
1060 }
1061
1062 if ( isset( $info['TestAutoloadClasses'] ) ) {
1063 $paths = $this->applyPath( $info['TestAutoloadClasses'], $dir );
1064 $this->autoloadDev['classes'] += $paths;
1065 }
1066
1067 if ( isset( $info['TestAutoloadNamespaces'] ) ) {
1068 $paths = $this->applyPath( $info['TestAutoloadNamespaces'], $dir, true );
1069 $this->autoloadDev['namespaces'] += $paths;
1070 }
1071 }
1072}
1073
1075class_alias( ExtensionProcessor::class, 'ExtensionProcessor' );
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
A class containing constants representing the names of configuration variables.
const HiddenPrefs
Name constant for the HiddenPrefs setting, for use with Config::get()
const DefaultUserOptions
Name constant for the DefaultUserOptions setting, for use with Config::get()
const FeedClasses
Name constant for the FeedClasses setting, for use with Config::get()
const AvailableRights
Name constant for the AvailableRights setting, for use with Config::get()
const APIListModules
Name constant for the APIListModules setting, for use with Config::get()
const APIFormatModules
Name constant for the APIFormatModules setting, for use with Config::get()
const ImplicitGroups
Name constant for the ImplicitGroups setting, for use with Config::get()
const RemoveCredentialsBlacklist
Name constant for the RemoveCredentialsBlacklist setting, for use with Config::get()
const LogRestrictions
Name constant for the LogRestrictions setting, for use with Config::get()
const ExtensionFunctions
Name constant for the ExtensionFunctions setting, for use with Config::get()
const ActionFilteredLogs
Name constant for the ActionFilteredLogs setting, for use with Config::get()
const RevokePermissions
Name constant for the RevokePermissions setting, for use with Config::get()
const RecentChangesFlags
Name constant for the RecentChangesFlags setting, for use with Config::get()
const GroupsRemoveFromSelf
Name constant for the GroupsRemoveFromSelf setting, for use with Config::get()
const Actions
Name constant for the Actions setting, for use with Config::get()
const CentralIdLookupProviders
Name constant for the CentralIdLookupProviders setting, for use with Config::get()
const ExtensionEntryPointListFiles
Name constant for the ExtensionEntryPointListFiles setting, for use with Config::get()
const APIModules
Name constant for the APIModules setting, for use with Config::get()
const LogNames
Name constant for the LogNames setting, for use with Config::get()
const SpecialPages
Name constant for the SpecialPages setting, for use with Config::get()
const UserRegistrationProviders
Name constant for the UserRegistrationProviders setting, for use with Config::get()
const AuthManagerAutoConfig
Name constant for the AuthManagerAutoConfig setting, for use with Config::get()
const FilterLogTypes
Name constant for the FilterLogTypes setting, for use with Config::get()
const LogTypes
Name constant for the LogTypes setting, for use with Config::get()
const ResourceLoaderSources
Name constant for the ResourceLoaderSources setting, for use with Config::get()
const LogHeaders
Name constant for the LogHeaders setting, for use with Config::get()
const JobClasses
Name constant for the JobClasses setting, for use with Config::get()
const RateLimits
Name constant for the RateLimits setting, for use with Config::get()
const MediaHandlers
Name constant for the MediaHandlers setting, for use with Config::get()
const OutputPipelineStages
Name constant for the OutputPipelineStages setting, for use with Config::get()
const GrantRiskGroups
Name constant for the GrantRiskGroups setting, for use with Config::get()
const GrantPermissionGroups
Name constant for the GrantPermissionGroups setting, for use with Config::get()
const GroupPermissions
Name constant for the GroupPermissions setting, for use with Config::get()
const AddGroups
Name constant for the AddGroups setting, for use with Config::get()
const ConfigRegistry
Name constant for the ConfigRegistry setting, for use with Config::get()
const FileExtensions
Name constant for the FileExtensions setting, for use with Config::get()
const SessionProviders
Name constant for the SessionProviders setting, for use with Config::get()
const RawHtmlMessages
Name constant for the RawHtmlMessages setting, for use with Config::get()
const LogActionsHandlers
Name constant for the LogActionsHandlers setting, for use with Config::get()
const LogActions
Name constant for the LogActions setting, for use with Config::get()
const APIPropModules
Name constant for the APIPropModules setting, for use with Config::get()
const PrivilegedGroups
Name constant for the PrivilegedGroups setting, for use with Config::get()
const APIMetaModules
Name constant for the APIMetaModules setting, for use with Config::get()
const GroupsAddToSelf
Name constant for the GroupsAddToSelf setting, for use with Config::get()
const ContentHandlers
Name constant for the ContentHandlers setting, for use with Config::get()
const GrantPermissions
Name constant for the GrantPermissions setting, for use with Config::get()
const ChangeCredentialsBlacklist
Name constant for the ChangeCredentialsBlacklist setting, for use with Config::get()
const ReauthenticateTime
Name constant for the ReauthenticateTime setting, for use with Config::get()
const RemoveGroups
Name constant for the RemoveGroups setting, for use with Config::get()
const PasswordPolicy
Name constant for the PasswordPolicy setting, for use with Config::get()
const ConditionalUserOptions
Name constant for the ConditionalUserOptions setting, for use with Config::get()
const RestAPIAdditionalRouteFiles
Name constant for the RestAPIAdditionalRouteFiles setting, for use with Config::get()
Load extension manifests and then aggregate their contents.
string[][] $autoload
Autoloader information.
array $extAttributes
Extension attributes, keyed by name => settings.
extractDomainEventIngresses(array $info, string $path)
Extract domain event subscribers.
extractHooks(array $info, string $path)
Extract hook information from Hooks and HookHandler attributes.
getExtractedAutoloadInfo(bool $includeDev=false)
Returns the extracted autoload info.
array $attributes
Anything else in the $info that hasn't already been processed.
extractTranslationAliasesDirs( $dir, array $info)
Set localization related settings, which need to be expanded to use absolute paths.
extractMessagesDirs( $dir, array $info)
Set message-related settings, which need to be expanded to use absolute paths.
extractPathBasedGlobal(string $global, string $dir, array $paths)
extractExtensionMessagesFiles(string $dir, array $info)
const MERGE_STRATEGIES
Mapping of global settings to their specific merge strategies.
storeToArrayRecursive( $path, $name, $value, &$array)
Stores $value to $array; using array_merge_recursive() if $array already contains $name.
const CREDIT_ATTRIBS
Keys that are part of the extension credits.
extractForeignResourcesDir(array $info, string $name, string $dir)
extractResourceLoaderModules(string $dir, array $info)
array $defines
Things that should be define()'d.
storeToArray( $path, $name, $value, &$array)
Stores $value to $array; using array_merge() if $array already contains $name.
const CORE_ATTRIBS
Top-level attributes that come from MW core.
extractConfig2(array $info, $dir)
Set configuration settings for manifest_version == 2.
const NOT_ATTRIBS
Things that are not 'attributes', and are not in $globalSettings or CREDIT_ATTRIBS.
extractSkins( $dir, array $info)
Extract skins and handle path correction for templateDirectory.
extractInfoFromFile(string $path)
Extracts extension info from the given JSON file.
string[][] $autoloadDev
Autoloader information for development.
array $globals
Stuff that is going to be set to $GLOBALS.
extractInfo( $path, array $info, $version)
extractImplicitRights(array $info)
Extract any user rights that should be granted implicitly.
extractConfig1(array $info)
Set configuration settings for manifest_version == 1.
getRequirements(array $info, $includeDev)
Get the requirements for the provided info.1.26array Where keys are the name to have a constraint on,...
extractNamespaces(array $info)
Register namespaces with the appropriate global settings.
static array $globalSettings
Keys that should be set to $GLOBALS.
callable[] $callbacks
Things to be called once the registration of these extensions is done.
const MERGE_STRATEGY
Special key that defines the merge strategy.
A path to a bundled file (such as JavaScript or CSS), along with a remote and local base path.
Definition FilePath.php:20
Generic processor that reads associated arrays and registers whatever is required.
Definition Processor.php:11