MediaWiki  master
ExtensionProcessor.php
Go to the documentation of this file.
1 <?php
2 
3 class 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 static $coreAttributes = [
66  'SkinOOUIThemes',
67  'TrackingCategories',
68  'RestRoutes',
69  ];
70 
78  protected static $mergeStrategies = [
79  'wgAuthManagerAutoConfig' => 'array_plus_2d',
80  'wgCapitalLinkOverrides' => 'array_plus',
81  'wgExtensionCredits' => 'array_merge_recursive',
82  'wgExtraGenderNamespaces' => 'array_plus',
83  'wgGrantPermissions' => 'array_plus_2d',
84  'wgGroupPermissions' => 'array_plus_2d',
85  'wgHooks' => 'array_merge_recursive',
86  'wgNamespaceContentModels' => 'array_plus',
87  'wgNamespaceProtection' => 'array_plus',
88  'wgNamespacesWithSubpages' => 'array_plus',
89  'wgPasswordPolicy' => 'array_merge_recursive',
90  'wgRateLimits' => 'array_plus_2d',
91  'wgRevokePermissions' => 'array_plus_2d',
92  ];
93 
99  protected static $creditsAttributes = [
100  'name',
101  'namemsg',
102  'author',
103  'version',
104  'url',
105  'description',
106  'descriptionmsg',
107  'license-name',
108  ];
109 
116  protected static $notAttributes = [
117  'callback',
118  'requires',
119  'Hooks',
120  'namespaces',
121  'ResourceFileModulePaths',
122  'ResourceModules',
123  'ResourceModuleSkinStyles',
124  'OOUIThemePaths',
125  'QUnitTestModule',
126  'MessagePosterModule',
127  'ExtensionMessagesFiles',
128  'MessagesDirs',
129  'type',
130  'config',
131  'config_prefix',
132  'ServiceWiringFiles',
133  'ParserTestFiles',
134  'AutoloadClasses',
135  'manifest_version',
136  'load_composer_autoloader',
137  ];
138 
146  protected $globals = [
147  'wgExtensionMessagesFiles' => [],
148  'wgMessagesDirs' => [],
149  ];
150 
156  protected $defines = [];
157 
164  protected $callbacks = [];
165 
169  protected $credits = [];
170 
174  protected $config = [];
175 
182  protected $attributes = [];
183 
190  protected $extAttributes = [];
191 
197  public function extractInfo( $path, array $info, $version ) {
198  $dir = dirname( $path );
199  $this->extractHooks( $info );
200  $this->extractExtensionMessagesFiles( $dir, $info );
201  $this->extractMessagesDirs( $dir, $info );
202  $this->extractNamespaces( $info );
203  $this->extractResourceLoaderModules( $dir, $info );
204  if ( isset( $info['ServiceWiringFiles'] ) ) {
205  $this->extractPathBasedGlobal(
206  'wgServiceWiringFiles',
207  $dir,
208  $info['ServiceWiringFiles']
209  );
210  }
211  if ( isset( $info['ParserTestFiles'] ) ) {
212  $this->extractPathBasedGlobal(
213  'wgParserTestFiles',
214  $dir,
215  $info['ParserTestFiles']
216  );
217  }
218  $name = $this->extractCredits( $path, $info );
219  if ( isset( $info['callback'] ) ) {
220  $this->callbacks[$name] = $info['callback'];
221  }
222 
223  // config should be after all core globals are extracted,
224  // so duplicate setting detection will work fully
225  if ( $version === 2 ) {
226  $this->extractConfig2( $info, $dir );
227  } else {
228  // $version === 1
229  $this->extractConfig1( $info );
230  }
231 
232  if ( $version === 2 ) {
233  $this->extractAttributes( $path, $info );
234  }
235 
236  foreach ( $info as $key => $val ) {
237  // If it's a global setting,
238  if ( in_array( $key, self::$globalSettings ) ) {
239  $this->storeToArray( $path, "wg$key", $val, $this->globals );
240  continue;
241  }
242  // Ignore anything that starts with a @
243  if ( $key[0] === '@' ) {
244  continue;
245  }
246 
247  if ( $version === 2 ) {
248  // Only whitelisted attributes are set
249  if ( in_array( $key, self::$coreAttributes ) ) {
250  $this->storeToArray( $path, $key, $val, $this->attributes );
251  }
252  } else {
253  // version === 1
254  if ( !in_array( $key, self::$notAttributes )
255  && !in_array( $key, self::$creditsAttributes )
256  ) {
257  // If it's not blacklisted, it's an attribute
258  $this->storeToArray( $path, $key, $val, $this->attributes );
259  }
260  }
261 
262  }
263  }
264 
269  protected function extractAttributes( $path, array $info ) {
270  if ( isset( $info['attributes'] ) ) {
271  foreach ( $info['attributes'] as $extName => $value ) {
272  $this->storeToArray( $path, $extName, $value, $this->extAttributes );
273  }
274  }
275  }
276 
277  public function getExtractedInfo() {
278  // Make sure the merge strategies are set
279  foreach ( $this->globals as $key => $val ) {
280  if ( isset( self::$mergeStrategies[$key] ) ) {
281  $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::$mergeStrategies[$key];
282  }
283  }
284 
285  // Merge $this->extAttributes into $this->attributes depending on what is loaded
286  foreach ( $this->extAttributes as $extName => $value ) {
287  // Only set the attribute if $extName is loaded (and hence present in credits)
288  if ( isset( $this->credits[$extName] ) ) {
289  foreach ( $value as $attrName => $attrValue ) {
290  $this->storeToArray(
291  '', // Don't provide a path since it's impossible to generate an error here
292  $extName . $attrName,
293  $attrValue,
294  $this->attributes
295  );
296  }
297  unset( $this->extAttributes[$extName] );
298  }
299  }
300 
301  return [
302  'globals' => $this->globals,
303  'config' => $this->config,
304  'defines' => $this->defines,
305  'callbacks' => $this->callbacks,
306  'credits' => $this->credits,
307  'attributes' => $this->attributes,
308  ];
309  }
310 
311  public function getRequirements( array $info, $includeDev ) {
312  // Quick shortcuts
313  if ( !$includeDev || !isset( $info['dev-requires'] ) ) {
314  return $info['requires'] ?? [];
315  }
316 
317  if ( !isset( $info['requires'] ) ) {
318  return $info['dev-requires'] ?? [];
319  }
320 
321  // OK, we actually have to merge everything
322  $merged = [];
323 
324  // Helper that combines version requirements by
325  // picking the non-null if one is, or combines
326  // the two. Note that it is not possible for
327  // both inputs to be null.
328  $pick = function ( $a, $b ) {
329  if ( $a === null ) {
330  return $b;
331  } elseif ( $b === null ) {
332  return $a;
333  } else {
334  return "$a $b";
335  }
336  };
337 
338  $req = $info['requires'];
339  $dev = $info['dev-requires'];
340  if ( isset( $req['MediaWiki'] ) || isset( $dev['MediaWiki'] ) ) {
341  $merged['MediaWiki'] = $pick(
342  $req['MediaWiki'] ?? null,
343  $dev['MediaWiki'] ?? null
344  );
345  }
346 
347  $platform = array_merge(
348  array_keys( $req['platform'] ?? [] ),
349  array_keys( $dev['platform'] ?? [] )
350  );
351  if ( $platform ) {
352  foreach ( $platform as $pkey ) {
353  if ( $pkey === 'php' ) {
354  $value = $pick(
355  $req['platform']['php'] ?? null,
356  $dev['platform']['php'] ?? null
357  );
358  } else {
359  // Prefer dev value, but these should be constant
360  // anyways (ext-* and ability-*)
361  $value = $dev['platform'][$pkey] ?? $req['platform'][$pkey];
362  }
363  $merged['platform'][$pkey] = $value;
364  }
365  }
366 
367  foreach ( [ 'extensions', 'skins' ] as $thing ) {
368  $things = array_merge(
369  array_keys( $req[$thing] ?? [] ),
370  array_keys( $dev[$thing] ?? [] )
371  );
372  foreach ( $things as $name ) {
373  $merged[$thing][$name] = $pick(
374  $req[$thing][$name] ?? null,
375  $dev[$thing][$name] ?? null
376  );
377  }
378  }
379 
380  return $merged;
381  }
382 
383  protected function extractHooks( array $info ) {
384  if ( isset( $info['Hooks'] ) ) {
385  foreach ( $info['Hooks'] as $name => $value ) {
386  if ( is_array( $value ) ) {
387  foreach ( $value as $callback ) {
388  $this->globals['wgHooks'][$name][] = $callback;
389  }
390  } else {
391  $this->globals['wgHooks'][$name][] = $value;
392  }
393  }
394  }
395  }
396 
402  protected function extractNamespaces( array $info ) {
403  if ( isset( $info['namespaces'] ) ) {
404  foreach ( $info['namespaces'] as $ns ) {
405  if ( defined( $ns['constant'] ) ) {
406  // If the namespace constant is already defined, use it.
407  // This allows namespace IDs to be overwritten locally.
408  $id = constant( $ns['constant'] );
409  } else {
410  $id = $ns['id'];
411  $this->defines[ $ns['constant'] ] = $id;
412  }
413 
414  if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
415  // If it is not conditional, register it
416  $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
417  }
418  if ( isset( $ns['gender'] ) ) {
419  $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
420  }
421  if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
422  $this->globals['wgNamespacesWithSubpages'][$id] = true;
423  }
424  if ( isset( $ns['content'] ) && $ns['content'] ) {
425  $this->globals['wgContentNamespaces'][] = $id;
426  }
427  if ( isset( $ns['defaultcontentmodel'] ) ) {
428  $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
429  }
430  if ( isset( $ns['protection'] ) ) {
431  $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
432  }
433  if ( isset( $ns['capitallinkoverride'] ) ) {
434  $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
435  }
436  }
437  }
438  }
439 
440  protected function extractResourceLoaderModules( $dir, array $info ) {
441  $defaultPaths = $info['ResourceFileModulePaths'] ?? false;
442  if ( isset( $defaultPaths['localBasePath'] ) ) {
443  if ( $defaultPaths['localBasePath'] === '' ) {
444  // Avoid double slashes (e.g. /extensions/Example//path)
445  $defaultPaths['localBasePath'] = $dir;
446  } else {
447  $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
448  }
449  }
450 
451  foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
452  if ( isset( $info[$setting] ) ) {
453  foreach ( $info[$setting] as $name => $data ) {
454  if ( isset( $data['localBasePath'] ) ) {
455  if ( $data['localBasePath'] === '' ) {
456  // Avoid double slashes (e.g. /extensions/Example//path)
457  $data['localBasePath'] = $dir;
458  } else {
459  $data['localBasePath'] = "$dir/{$data['localBasePath']}";
460  }
461  }
462  if ( $defaultPaths ) {
463  $data += $defaultPaths;
464  }
465  if ( $setting === 'OOUIThemePaths' ) {
466  $this->attributes[$setting][$name] = $data;
467  } else {
468  $this->globals["wg$setting"][$name] = $data;
469  }
470  }
471  }
472  }
473 
474  if ( isset( $info['QUnitTestModule'] ) ) {
475  $data = $info['QUnitTestModule'];
476  if ( isset( $data['localBasePath'] ) ) {
477  if ( $data['localBasePath'] === '' ) {
478  // Avoid double slashes (e.g. /extensions/Example//path)
479  $data['localBasePath'] = $dir;
480  } else {
481  $data['localBasePath'] = "$dir/{$data['localBasePath']}";
482  }
483  }
484  $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data;
485  }
486 
487  if ( isset( $info['MessagePosterModule'] ) ) {
488  $data = $info['MessagePosterModule'];
489  $basePath = $data['localBasePath'] ?? '';
490  $baseDir = $basePath === '' ? $dir : "$dir/$basePath";
491  foreach ( $data['scripts'] ?? [] as $scripts ) {
492  $this->attributes['MessagePosterModule']['scripts'][] = "$baseDir/$scripts";
493  }
494  foreach ( $data['dependencies'] ?? [] as $dependency ) {
495  $this->attributes['MessagePosterModule']['dependencies'][] = $dependency;
496  }
497  }
498  }
499 
500  protected function extractExtensionMessagesFiles( $dir, array $info ) {
501  if ( isset( $info['ExtensionMessagesFiles'] ) ) {
502  foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
503  $file = "$dir/$file";
504  }
505  $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
506  }
507  }
508 
516  protected function extractMessagesDirs( $dir, array $info ) {
517  if ( isset( $info['MessagesDirs'] ) ) {
518  foreach ( $info['MessagesDirs'] as $name => $files ) {
519  foreach ( (array)$files as $file ) {
520  $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
521  }
522  }
523  }
524  }
525 
532  protected function extractCredits( $path, array $info ) {
533  $credits = [
534  'path' => $path,
535  'type' => $info['type'] ?? 'other',
536  ];
537  foreach ( self::$creditsAttributes as $attr ) {
538  if ( isset( $info[$attr] ) ) {
539  $credits[$attr] = $info[$attr];
540  }
541  }
542 
543  $name = $credits['name'];
544 
545  // If someone is loading the same thing twice, throw
546  // a nice error (T121493)
547  if ( isset( $this->credits[$name] ) ) {
548  $firstPath = $this->credits[$name]['path'];
549  $secondPath = $credits['path'];
550  throw new Exception( "It was attempted to load $name twice, from $firstPath and $secondPath." );
551  }
552 
553  $this->credits[$name] = $credits;
554  $this->globals['wgExtensionCredits'][$credits['type']][] = $credits;
555 
556  return $name;
557  }
558 
565  protected function extractConfig1( array $info ) {
566  if ( isset( $info['config'] ) ) {
567  if ( isset( $info['config']['_prefix'] ) ) {
568  $prefix = $info['config']['_prefix'];
569  unset( $info['config']['_prefix'] );
570  } else {
571  $prefix = 'wg';
572  }
573  foreach ( $info['config'] as $key => $val ) {
574  if ( $key[0] !== '@' ) {
575  $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
576  }
577  }
578  }
579  }
580 
588  protected function extractConfig2( array $info, $dir ) {
589  $prefix = $info['config_prefix'] ?? 'wg';
590  if ( isset( $info['config'] ) ) {
591  foreach ( $info['config'] as $key => $data ) {
592  $value = $data['value'];
593  if ( isset( $data['path'] ) && $data['path'] ) {
594  $callback = function ( $value ) use ( $dir ) {
595  return "$dir/$value";
596  };
597  if ( is_array( $value ) ) {
598  $value = array_map( $callback, $value );
599  } else {
600  $value = $callback( $value );
601  }
602  }
603  if ( isset( $data['merge_strategy'] ) ) {
604  $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
605  }
606  $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
607  $data['providedby'] = $info['name'];
608  if ( isset( $info['ConfigRegistry'][0] ) ) {
609  $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0];
610  }
611  $this->config[$key] = $data;
612  }
613  }
614  }
615 
623  private function addConfigGlobal( $key, $value, $extName ) {
624  if ( array_key_exists( $key, $this->globals ) ) {
625  throw new RuntimeException(
626  "The configuration setting '$key' was already set by MediaWiki core or"
627  . " another extension, and cannot be set again by $extName." );
628  }
629  $this->globals[$key] = $value;
630  }
631 
632  protected function extractPathBasedGlobal( $global, $dir, $paths ) {
633  foreach ( $paths as $path ) {
634  $this->globals[$global][] = "$dir/$path";
635  }
636  }
637 
645  protected function storeToArray( $path, $name, $value, &$array ) {
646  if ( !is_array( $value ) ) {
647  throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
648  }
649  if ( isset( $array[$name] ) ) {
650  $array[$name] = array_merge_recursive( $array[$name], $value );
651  } else {
652  $array[$name] = $value;
653  }
654  }
655 
656  public function getExtraAutoloaderPaths( $dir, array $info ) {
657  $paths = [];
658  if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
659  $paths[] = "$dir/vendor/autoload.php";
660  }
661  return $paths;
662  }
663 }
ExtensionProcessor\$coreAttributes
static string[] $coreAttributes
Top-level attributes that come from MW core.
Definition: ExtensionProcessor.php:65
ExtensionProcessor\$callbacks
callable[] $callbacks
Things to be called once registration of these extensions are done keyed by the name of the extension...
Definition: ExtensionProcessor.php:164
ExtensionProcessor\$mergeStrategies
static array $mergeStrategies
Mapping of global settings to their specific merge strategies.
Definition: ExtensionProcessor.php:78
ExtensionProcessor\extractResourceLoaderModules
extractResourceLoaderModules( $dir, array $info)
Definition: ExtensionProcessor.php:440
ExtensionProcessor\extractConfig1
extractConfig1(array $info)
Set configuration settings for manifest_version == 1.
Definition: ExtensionProcessor.php:565
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ExtensionProcessor\storeToArray
storeToArray( $path, $name, $value, &$array)
Definition: ExtensionProcessor.php:645
Processor
Processors read associated arrays and register whatever is required.
Definition: Processor.php:9
ExtensionProcessor\addConfigGlobal
addConfigGlobal( $key, $value, $extName)
Helper function to set a value to a specific global, if it isn't set already.
Definition: ExtensionProcessor.php:623
ExtensionProcessor\extractExtensionMessagesFiles
extractExtensionMessagesFiles( $dir, array $info)
Definition: ExtensionProcessor.php:500
ExtensionRegistry\MERGE_STRATEGY
const MERGE_STRATEGY
Special key that defines the merge strategy.
Definition: ExtensionRegistry.php:54
ExtensionProcessor\extractHooks
extractHooks(array $info)
Definition: ExtensionProcessor.php:383
ExtensionProcessor
Definition: ExtensionProcessor.php:3
ExtensionProcessor\extractPathBasedGlobal
extractPathBasedGlobal( $global, $dir, $paths)
Definition: ExtensionProcessor.php:632
ExtensionProcessor\extractConfig2
extractConfig2(array $info, $dir)
Set configuration settings for manifest_version == 2.
Definition: ExtensionProcessor.php:588
ExtensionProcessor\$globals
array $globals
Stuff that is going to be set to $GLOBALS.
Definition: ExtensionProcessor.php:146
ExtensionProcessor\getExtraAutoloaderPaths
getExtraAutoloaderPaths( $dir, array $info)
Get the path for additional autoloaders, e.g.
Definition: ExtensionProcessor.php:656
ExtensionProcessor\$config
array $config
Definition: ExtensionProcessor.php:174
ExtensionProcessor\getRequirements
getRequirements(array $info, $includeDev)
Get the requirements for the provided info.
Definition: ExtensionProcessor.php:311
ExtensionProcessor\$credits
array $credits
Definition: ExtensionProcessor.php:169
ExtensionProcessor\extractNamespaces
extractNamespaces(array $info)
Register namespaces with the appropriate global settings.
Definition: ExtensionProcessor.php:402
ExtensionProcessor\getExtractedInfo
getExtractedInfo()
Definition: ExtensionProcessor.php:277
ExtensionProcessor\$extAttributes
array $extAttributes
Extension attributes, keyed by name => settings.
Definition: ExtensionProcessor.php:190
ExtensionProcessor\$globalSettings
static array $globalSettings
Keys that should be set to $GLOBALS.
Definition: ExtensionProcessor.php:10
ExtensionProcessor\$creditsAttributes
static array $creditsAttributes
Keys that are part of the extension credits.
Definition: ExtensionProcessor.php:99
ExtensionProcessor\extractAttributes
extractAttributes( $path, array $info)
Definition: ExtensionProcessor.php:269
ExtensionProcessor\extractCredits
extractCredits( $path, array $info)
Definition: ExtensionProcessor.php:532
$path
$path
Definition: NoLocalSettings.php:25
ExtensionProcessor\extractInfo
extractInfo( $path, array $info, $version)
Definition: ExtensionProcessor.php:197
ExtensionProcessor\$defines
array $defines
Things that should be define()'d.
Definition: ExtensionProcessor.php:156
$basePath
$basePath
Definition: addSite.php:5
ExtensionProcessor\$attributes
array $attributes
Any thing else in the $info that hasn't already been processed.
Definition: ExtensionProcessor.php:182
ExtensionProcessor\$notAttributes
static array $notAttributes
Things that are not 'attributes', and are not in $globalSettings or $creditsAttributes.
Definition: ExtensionProcessor.php:116
ExtensionProcessor\extractMessagesDirs
extractMessagesDirs( $dir, array $info)
Set message-related settings, which need to be expanded to use absolute paths.
Definition: ExtensionProcessor.php:516