MediaWiki REL1_35
ExtensionProcessor.php
Go to the documentation of this file.
1<?php
2
3class ExtensionProcessor implements Processor {
4
10 protected static $globalSettings = [
11 'ActionFilteredLogs',
12 'Actions',
13 'AddGroups',
14 'APIFormatModules',
15 'APIListModules',
16 'APIMetaModules',
17 'APIModules',
18 'APIPropModules',
19 'AuthManagerAutoConfig',
20 'AvailableRights',
21 'CentralIdLookupProviders',
22 'ChangeCredentialsBlacklist',
23 'ConfigRegistry',
24 'ContentHandlers',
25 'DefaultUserOptions',
26 'ExtensionEntryPointListFiles',
27 'ExtensionFunctions',
28 'FeedClasses',
29 'FileExtensions',
30 'FilterLogTypes',
31 'GrantPermissionGroups',
32 'GrantPermissions',
33 'GroupPermissions',
34 'GroupsAddToSelf',
35 'GroupsRemoveFromSelf',
36 'HiddenPrefs',
37 'ImplicitGroups',
38 'JobClasses',
39 'LogActions',
40 'LogActionsHandlers',
41 'LogHeaders',
42 'LogNames',
43 'LogRestrictions',
44 'LogTypes',
45 'MediaHandlers',
46 'PasswordPolicy',
47 'RateLimits',
48 'RawHtmlMessages',
49 'ReauthenticateTime',
50 'RecentChangesFlags',
51 'RemoveCredentialsBlacklist',
52 'RemoveGroups',
53 'ResourceLoaderSources',
54 'RevokePermissions',
55 'SessionProviders',
56 'SpecialPages',
57 'ValidSkinNames',
58 ];
59
65 protected const CORE_ATTRIBS = [
66 'ParsoidModules',
67 'RestRoutes',
68 'SkinOOUIThemes',
69 'SearchMappings',
70 'TrackingCategories',
71 ];
72
80 protected const MERGE_STRATEGIES = [
81 'wgAuthManagerAutoConfig' => 'array_plus_2d',
82 'wgCapitalLinkOverrides' => 'array_plus',
83 'wgExtraGenderNamespaces' => 'array_plus',
84 'wgGrantPermissions' => 'array_plus_2d',
85 'wgGroupPermissions' => 'array_plus_2d',
86 'wgHooks' => 'array_merge_recursive',
87 'wgNamespaceContentModels' => 'array_plus',
88 'wgNamespaceProtection' => 'array_plus',
89 'wgNamespacesWithSubpages' => 'array_plus',
90 'wgPasswordPolicy' => 'array_merge_recursive',
91 'wgRateLimits' => 'array_plus_2d',
92 'wgRevokePermissions' => 'array_plus_2d',
93 ];
94
100 protected const CREDIT_ATTRIBS = [
101 'type',
102 'author',
103 'description',
104 'descriptionmsg',
105 'license-name',
106 'name',
107 'namemsg',
108 'url',
109 'version',
110 ];
111
118 protected const NOT_ATTRIBS = [
119 'callback',
120 'config',
121 'config_prefix',
122 'load_composer_autoloader',
123 'manifest_version',
124 'namespaces',
125 'requires',
126 'AutoloadClasses',
127 'ExtensionMessagesFiles',
128 'Hooks',
129 'MessagePosterModule',
130 'MessagesDirs',
131 'OOUIThemePaths',
132 'ParserTestFiles',
133 'QUnitTestModule',
134 'ResourceFileModulePaths',
135 'ResourceModuleSkinStyles',
136 'ResourceModules',
137 'ServiceWiringFiles',
138 ];
139
147 protected $globals = [
148 'wgExtensionMessagesFiles' => [],
149 'wgMessagesDirs' => [],
150 ];
151
157 protected $defines = [];
158
165 protected $callbacks = [];
166
170 protected $credits = [];
171
175 protected $config = [];
176
183 protected $attributes = [];
184
191 protected $extAttributes = [];
192
198 public function extractInfo( $path, array $info, $version ) {
199 $dir = dirname( $path );
200 $this->extractHooks( $info, $path );
201 $this->extractExtensionMessagesFiles( $dir, $info );
202 $this->extractMessagesDirs( $dir, $info );
203 $this->extractNamespaces( $info );
204 $this->extractResourceLoaderModules( $dir, $info );
205 if ( isset( $info['ServiceWiringFiles'] ) ) {
207 'wgServiceWiringFiles',
208 $dir,
209 $info['ServiceWiringFiles']
210 );
211 }
212 if ( isset( $info['ParserTestFiles'] ) ) {
214 'wgParserTestFiles',
215 $dir,
216 $info['ParserTestFiles']
217 );
218 }
219 $name = $this->extractCredits( $path, $info );
220 if ( isset( $info['callback'] ) ) {
221 $this->callbacks[$name] = $info['callback'];
222 }
223
224 // config should be after all core globals are extracted,
225 // so duplicate setting detection will work fully
226 if ( $version === 2 ) {
227 $this->extractConfig2( $info, $dir );
228 } else {
229 // $version === 1
230 $this->extractConfig1( $info );
231 }
232
233 // Record the extension name in the ParsoidModules property
234 if ( isset( $info['ParsoidModules'] ) ) {
235 foreach ( $info['ParsoidModules'] as &$module ) {
236 if ( is_string( $module ) ) {
237 $className = $module;
238 $module = [
239 'class' => $className,
240 ];
241 }
242 $module['name'] = $name;
243 }
244 }
245
246 if ( $version === 2 ) {
247 $this->extractAttributes( $path, $info );
248 }
249
250 foreach ( $info as $key => $val ) {
251 // If it's a global setting,
252 if ( in_array( $key, self::$globalSettings ) ) {
253 $this->storeToArrayRecursive( $path, "wg$key", $val, $this->globals );
254 continue;
255 }
256 // Ignore anything that starts with a @
257 if ( $key[0] === '@' ) {
258 continue;
259 }
260
261 if ( $version === 2 ) {
262 // Only whitelisted attributes are set
263 if ( in_array( $key, self::CORE_ATTRIBS ) ) {
264 $this->storeToArray( $path, $key, $val, $this->attributes );
265 }
266 } else {
267 // version === 1
268 if ( !in_array( $key, self::NOT_ATTRIBS )
269 && !in_array( $key, self::CREDIT_ATTRIBS )
270 ) {
271 // If it's not blacklisted, it's an attribute
272 $this->storeToArrayRecursive( $path, $key, $val, $this->attributes );
273 }
274 }
275 }
276 }
277
282 protected function extractAttributes( $path, array $info ) {
283 if ( isset( $info['attributes'] ) ) {
284 foreach ( $info['attributes'] as $extName => $value ) {
285 $this->storeToArrayRecursive( $path, $extName, $value, $this->extAttributes );
286 }
287 }
288 }
289
290 public function getExtractedInfo() {
291 // Make sure the merge strategies are set
292 foreach ( $this->globals as $key => $val ) {
293 if ( isset( self::MERGE_STRATEGIES[$key] ) ) {
294 $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::MERGE_STRATEGIES[$key];
295 }
296 }
297
298 // Merge $this->extAttributes into $this->attributes depending on what is loaded
299 foreach ( $this->extAttributes as $extName => $value ) {
300 // Only set the attribute if $extName is loaded (and hence present in credits)
301 if ( isset( $this->credits[$extName] ) ) {
302 foreach ( $value as $attrName => $attrValue ) {
304 '', // Don't provide a path since it's impossible to generate an error here
305 $extName . $attrName,
306 $attrValue,
307 $this->attributes
308 );
309 }
310 unset( $this->extAttributes[$extName] );
311 }
312 }
313
314 return [
315 'globals' => $this->globals,
316 'config' => $this->config,
317 'defines' => $this->defines,
318 'callbacks' => $this->callbacks,
319 'credits' => $this->credits,
320 'attributes' => $this->attributes,
321 ];
322 }
323
324 public function getRequirements( array $info, $includeDev ) {
325 // Quick shortcuts
326 if ( !$includeDev || !isset( $info['dev-requires'] ) ) {
327 return $info['requires'] ?? [];
328 }
329
330 if ( !isset( $info['requires'] ) ) {
331 return $info['dev-requires'] ?? [];
332 }
333
334 // OK, we actually have to merge everything
335 $merged = [];
336
337 // Helper that combines version requirements by
338 // picking the non-null if one is, or combines
339 // the two. Note that it is not possible for
340 // both inputs to be null.
341 $pick = function ( $a, $b ) {
342 if ( $a === null ) {
343 return $b;
344 } elseif ( $b === null ) {
345 return $a;
346 } else {
347 return "$a $b";
348 }
349 };
350
351 $req = $info['requires'];
352 $dev = $info['dev-requires'];
353 if ( isset( $req['MediaWiki'] ) || isset( $dev['MediaWiki'] ) ) {
354 $merged['MediaWiki'] = $pick(
355 $req['MediaWiki'] ?? null,
356 $dev['MediaWiki'] ?? null
357 );
358 }
359
360 $platform = array_merge(
361 array_keys( $req['platform'] ?? [] ),
362 array_keys( $dev['platform'] ?? [] )
363 );
364 if ( $platform ) {
365 foreach ( $platform as $pkey ) {
366 if ( $pkey === 'php' ) {
367 $value = $pick(
368 $req['platform']['php'] ?? null,
369 $dev['platform']['php'] ?? null
370 );
371 } else {
372 // Prefer dev value, but these should be constant
373 // anyways (ext-* and ability-*)
374 $value = $dev['platform'][$pkey] ?? $req['platform'][$pkey];
375 }
376 $merged['platform'][$pkey] = $value;
377 }
378 }
379
380 foreach ( [ 'extensions', 'skins' ] as $thing ) {
381 $things = array_merge(
382 array_keys( $req[$thing] ?? [] ),
383 array_keys( $dev[$thing] ?? [] )
384 );
385 foreach ( $things as $name ) {
386 $merged[$thing][$name] = $pick(
387 $req[$thing][$name] ?? null,
388 $dev[$thing][$name] ?? null
389 );
390 }
391 }
392 return $merged;
393 }
394
406 private function setArrayHookHandler(
407 array $callback,
408 array $hookHandlersAttr,
409 string $name,
410 string $path
411 ) {
412 if ( isset( $callback['handler'] ) ) {
413 $handlerName = $callback['handler'];
414 $handlerDefinition = $hookHandlersAttr[$handlerName] ?? false;
415 if ( !$handlerDefinition ) {
416 throw new UnexpectedValueException(
417 "Missing handler definition for $name in HookHandlers attribute in $path"
418 );
419 }
420 $callback['handler'] = $handlerDefinition;
421 $callback['extensionPath'] = $path;
422 $this->attributes['Hooks'][$name][] = $callback;
423 } else {
424 foreach ( $callback as $callable ) {
425 if ( is_array( $callable ) ) {
426 if ( isset( $callable['handler'] ) ) { // Non-legacy style handler
427 $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name, $path );
428 } else { // Legacy style handler array
429 $this->globals['wgHooks'][$name][] = $callable;
430 }
431 } elseif ( is_string( $callable ) ) {
432 $this->setStringHookHandler( $callable, $hookHandlersAttr, $name, $path );
433 }
434 }
435 }
436 }
437
448 private function setStringHookHandler(
449 string $callback,
450 array $hookHandlersAttr,
451 string $name,
452 string $path
453 ) {
454 if ( isset( $hookHandlersAttr[$callback] ) ) {
455 $handler = [
456 'handler' => $hookHandlersAttr[$callback],
457 'extensionPath' => $path
458 ];
459 $this->attributes['Hooks'][$name][] = $handler;
460 } else { // legacy style handler
461 $this->globals['wgHooks'][$name][] = $callback;
462 }
463 }
464
473 protected function extractHooks( array $info, string $path ) {
474 $extName = $info['name'];
475 if ( isset( $info['Hooks'] ) ) {
476 $hookHandlersAttr = [];
477 foreach ( $info['HookHandlers'] ?? [] as $name => $def ) {
478 $hookHandlersAttr[$name] = [ 'name' => "$extName-$name" ] + $def;
479 }
480 foreach ( $info['Hooks'] as $name => $callback ) {
481 if ( is_string( $callback ) ) {
482 $this->setStringHookHandler( $callback, $hookHandlersAttr, $name, $path );
483 } elseif ( is_array( $callback ) ) {
484 $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name, $path );
485 }
486 }
487 }
488 if ( isset( $info['DeprecatedHooks'] ) ) {
489 $deprecatedHooks = [];
490 foreach ( $info['DeprecatedHooks'] as $name => $deprecatedHookInfo ) {
491 $deprecatedHookInfo += [ 'component' => $extName ];
492 $deprecatedHooks[$name] = $deprecatedHookInfo;
493 }
494 if ( isset( $this->attributes['DeprecatedHooks'] ) ) {
495 $this->attributes['DeprecatedHooks'] += $deprecatedHooks;
496 } else {
497 $this->attributes['DeprecatedHooks'] = $deprecatedHooks;
498 }
499 }
500 }
501
507 protected function extractNamespaces( array $info ) {
508 if ( isset( $info['namespaces'] ) ) {
509 foreach ( $info['namespaces'] as $ns ) {
510 if ( defined( $ns['constant'] ) ) {
511 // If the namespace constant is already defined, use it.
512 // This allows namespace IDs to be overwritten locally.
513 $id = constant( $ns['constant'] );
514 } else {
515 $id = $ns['id'];
516 }
517 $this->defines[ $ns['constant'] ] = $id;
518
519 if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
520 // If it is not conditional, register it
521 $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
522 }
523 if ( isset( $ns['movable'] ) && !$ns['movable'] ) {
524 $this->attributes['ImmovableNamespaces'][] = $id;
525 }
526 if ( isset( $ns['gender'] ) ) {
527 $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
528 }
529 if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
530 $this->globals['wgNamespacesWithSubpages'][$id] = true;
531 }
532 if ( isset( $ns['content'] ) && $ns['content'] ) {
533 $this->globals['wgContentNamespaces'][] = $id;
534 }
535 if ( isset( $ns['defaultcontentmodel'] ) ) {
536 $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
537 }
538 if ( isset( $ns['protection'] ) ) {
539 $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
540 }
541 if ( isset( $ns['capitallinkoverride'] ) ) {
542 $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
543 }
544
545 }
546 }
547 }
548
549 protected function extractResourceLoaderModules( $dir, array $info ) {
550 $defaultPaths = $info['ResourceFileModulePaths'] ?? false;
551 if ( isset( $defaultPaths['localBasePath'] ) ) {
552 if ( $defaultPaths['localBasePath'] === '' ) {
553 // Avoid double slashes (e.g. /extensions/Example//path)
554 $defaultPaths['localBasePath'] = $dir;
555 } else {
556 $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
557 }
558 }
559
560 foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
561 if ( isset( $info[$setting] ) ) {
562 foreach ( $info[$setting] as $name => $data ) {
563 if ( isset( $data['localBasePath'] ) ) {
564 if ( $data['localBasePath'] === '' ) {
565 // Avoid double slashes (e.g. /extensions/Example//path)
566 $data['localBasePath'] = $dir;
567 } else {
568 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
569 }
570 }
571 if ( $defaultPaths ) {
572 $data += $defaultPaths;
573 }
574 $this->attributes[$setting][$name] = $data;
575 }
576 }
577 }
578
579 if ( isset( $info['QUnitTestModule'] ) ) {
580 $data = $info['QUnitTestModule'];
581 if ( isset( $data['localBasePath'] ) ) {
582 if ( $data['localBasePath'] === '' ) {
583 // Avoid double slashes (e.g. /extensions/Example//path)
584 $data['localBasePath'] = $dir;
585 } else {
586 $data['localBasePath'] = "$dir/{$data['localBasePath']}";
587 }
588 }
589 $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data;
590 }
591
592 if ( isset( $info['MessagePosterModule'] ) ) {
593 $data = $info['MessagePosterModule'];
594 $basePath = $data['localBasePath'] ?? '';
595 $baseDir = $basePath === '' ? $dir : "$dir/$basePath";
596 foreach ( $data['scripts'] ?? [] as $scripts ) {
597 $this->attributes['MessagePosterModule']['scripts'][] =
598 new ResourceLoaderFilePath( $scripts, $baseDir );
599 }
600 foreach ( $data['dependencies'] ?? [] as $dependency ) {
601 $this->attributes['MessagePosterModule']['dependencies'][] = $dependency;
602 }
603 }
604 }
605
606 protected function extractExtensionMessagesFiles( $dir, array $info ) {
607 if ( isset( $info['ExtensionMessagesFiles'] ) ) {
608 foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
609 $file = "$dir/$file";
610 }
611 $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
612 }
613 }
614
622 protected function extractMessagesDirs( $dir, array $info ) {
623 if ( isset( $info['MessagesDirs'] ) ) {
624 foreach ( $info['MessagesDirs'] as $name => $files ) {
625 foreach ( (array)$files as $file ) {
626 $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
627 }
628 }
629 }
630 }
631
638 protected function extractCredits( $path, array $info ) {
639 $credits = [
640 'path' => $path,
641 'type' => 'other',
642 ];
643 foreach ( self::CREDIT_ATTRIBS as $attr ) {
644 if ( isset( $info[$attr] ) ) {
645 $credits[$attr] = $info[$attr];
646 }
647 }
648
649 $name = $credits['name'];
650
651 // If someone is loading the same thing twice, throw
652 // a nice error (T121493)
653 if ( isset( $this->credits[$name] ) ) {
654 $firstPath = $this->credits[$name]['path'];
655 $secondPath = $credits['path'];
656 throw new Exception( "It was attempted to load $name twice, from $firstPath and $secondPath." );
657 }
658
659 $this->credits[$name] = $credits;
660
661 return $name;
662 }
663
670 protected function extractConfig1( array $info ) {
671 if ( isset( $info['config'] ) ) {
672 if ( isset( $info['config']['_prefix'] ) ) {
673 $prefix = $info['config']['_prefix'];
674 unset( $info['config']['_prefix'] );
675 } else {
676 $prefix = 'wg';
677 }
678 foreach ( $info['config'] as $key => $val ) {
679 if ( $key[0] !== '@' ) {
680 $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
681 }
682 }
683 }
684 }
685
693 protected function extractConfig2( array $info, $dir ) {
694 $prefix = $info['config_prefix'] ?? 'wg';
695 if ( isset( $info['config'] ) ) {
696 foreach ( $info['config'] as $key => $data ) {
697 $value = $data['value'];
698 if ( isset( $data['path'] ) && $data['path'] ) {
699 $callback = function ( $value ) use ( $dir ) {
700 return "$dir/$value";
701 };
702 if ( is_array( $value ) ) {
703 $value = array_map( $callback, $value );
704 } else {
705 $value = $callback( $value );
706 }
707 }
708 if ( isset( $data['merge_strategy'] ) ) {
709 $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
710 }
711 $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
712 $data['providedby'] = $info['name'];
713 if ( isset( $info['ConfigRegistry'][0] ) ) {
714 $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0];
715 }
716 $this->config[$key] = $data;
717 }
718 }
719 }
720
728 private function addConfigGlobal( $key, $value, $extName ) {
729 if ( array_key_exists( $key, $this->globals ) ) {
730 throw new RuntimeException(
731 "The configuration setting '$key' was already set by MediaWiki core or"
732 . " another extension, and cannot be set again by $extName." );
733 }
734 $this->globals[$key] = $value;
735 }
736
737 protected function extractPathBasedGlobal( $global, $dir, $paths ) {
738 foreach ( $paths as $path ) {
739 $this->globals[$global][] = "$dir/$path";
740 }
741 }
742
752 protected function storeToArrayRecursive( $path, $name, $value, &$array ) {
753 if ( !is_array( $value ) ) {
754 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
755 }
756 if ( isset( $array[$name] ) ) {
757 $array[$name] = array_merge_recursive( $array[$name], $value );
758 } else {
759 $array[$name] = $value;
760 }
761 }
762
772 protected function storeToArray( $path, $name, $value, &$array ) {
773 if ( !is_array( $value ) ) {
774 throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
775 }
776 if ( isset( $array[$name] ) ) {
777 $array[$name] = array_merge( $array[$name], $value );
778 } else {
779 $array[$name] = $value;
780 }
781 }
782
783 public function getExtraAutoloaderPaths( $dir, array $info ) {
784 $paths = [];
785 if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
786 $paths[] = "$dir/vendor/autoload.php";
787 }
788 return $paths;
789 }
790}
$basePath
Definition addSite.php:5
extractHooks(array $info, string $path)
Extract hook information from Hooks and HookHandler attributes.
extractNamespaces(array $info)
Register namespaces with the appropriate global settings.
extractCredits( $path, array $info)
array $attributes
Any thing else in the $info that hasn't already been processed.
addConfigGlobal( $key, $value, $extName)
Helper function to set a value to a specific global, if it isn't set already.
callable[] $callbacks
Things to be called once registration of these extensions are done keyed by the name of the extension...
array $defines
Things that should be define()'d.
extractExtensionMessagesFiles( $dir, array $info)
extractConfig1(array $info)
Set configuration settings for manifest_version == 1.
array $globals
Stuff that is going to be set to $GLOBALS.
extractMessagesDirs( $dir, array $info)
Set message-related settings, which need to be expanded to use absolute paths.
extractConfig2(array $info, $dir)
Set configuration settings for manifest_version == 2.
extractResourceLoaderModules( $dir, array $info)
array $extAttributes
Extension attributes, keyed by name => settings.
getExtraAutoloaderPaths( $dir, array $info)
Get the path for additional autoloaders, e.g.
extractAttributes( $path, array $info)
getRequirements(array $info, $includeDev)
Get the requirements for the provided info.
extractInfo( $path, array $info, $version)
storeToArray( $path, $name, $value, &$array)
Stores $value to $array; using array_merge() if $array already contains $name.
setStringHookHandler(string $callback, array $hookHandlersAttr, string $name, string $path)
When handler value is a string, set $wgHooks or Hooks attribute.
extractPathBasedGlobal( $global, $dir, $paths)
setArrayHookHandler(array $callback, array $hookHandlersAttr, string $name, string $path)
When handler value is an array, set $wgHooks or Hooks attribute Could be legacy hook e....
storeToArrayRecursive( $path, $name, $value, &$array)
Stores $value to $array; using array_merge_recursive() if $array already contains $name.
static array $globalSettings
Keys that should be set to $GLOBALS.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Processors read associated arrays and register whatever is required.
Definition Processor.php:9
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42