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  'Hooks',
119  'namespaces',
120  'ResourceFileModulePaths',
121  'ResourceModules',
122  'ResourceModuleSkinStyles',
123  'OOUIThemePaths',
124  'QUnitTestModule',
125  'ExtensionMessagesFiles',
126  'MessagesDirs',
127  'type',
128  'config',
129  'config_prefix',
130  'ServiceWiringFiles',
131  'ParserTestFiles',
132  'AutoloadClasses',
133  'manifest_version',
134  'load_composer_autoloader',
135  ];
136 
144  protected $globals = [
145  'wgExtensionMessagesFiles' => [],
146  'wgMessagesDirs' => [],
147  ];
148 
154  protected $defines = [];
155 
162  protected $callbacks = [];
163 
167  protected $credits = [];
168 
172  protected $config = [];
173 
180  protected $attributes = [];
181 
188  protected $extAttributes = [];
189 
195  public function extractInfo( $path, array $info, $version ) {
196  $dir = dirname( $path );
197  $this->extractHooks( $info );
198  $this->extractExtensionMessagesFiles( $dir, $info );
199  $this->extractMessagesDirs( $dir, $info );
200  $this->extractNamespaces( $info );
201  $this->extractResourceLoaderModules( $dir, $info );
202  if ( isset( $info['ServiceWiringFiles'] ) ) {
203  $this->extractPathBasedGlobal(
204  'wgServiceWiringFiles',
205  $dir,
206  $info['ServiceWiringFiles']
207  );
208  }
209  if ( isset( $info['ParserTestFiles'] ) ) {
210  $this->extractPathBasedGlobal(
211  'wgParserTestFiles',
212  $dir,
213  $info['ParserTestFiles']
214  );
215  }
216  $name = $this->extractCredits( $path, $info );
217  if ( isset( $info['callback'] ) ) {
218  $this->callbacks[$name] = $info['callback'];
219  }
220 
221  // config should be after all core globals are extracted,
222  // so duplicate setting detection will work fully
223  if ( $version === 2 ) {
224  $this->extractConfig2( $info, $dir );
225  } else {
226  // $version === 1
227  $this->extractConfig1( $info );
228  }
229 
230  if ( $version === 2 ) {
231  $this->extractAttributes( $path, $info );
232  }
233 
234  foreach ( $info as $key => $val ) {
235  // If it's a global setting,
236  if ( in_array( $key, self::$globalSettings ) ) {
237  $this->storeToArray( $path, "wg$key", $val, $this->globals );
238  continue;
239  }
240  // Ignore anything that starts with a @
241  if ( $key[0] === '@' ) {
242  continue;
243  }
244 
245  if ( $version === 2 ) {
246  // Only whitelisted attributes are set
247  if ( in_array( $key, self::$coreAttributes ) ) {
248  $this->storeToArray( $path, $key, $val, $this->attributes );
249  }
250  } else {
251  // version === 1
252  if ( !in_array( $key, self::$notAttributes )
253  && !in_array( $key, self::$creditsAttributes )
254  ) {
255  // If it's not blacklisted, it's an attribute
256  $this->storeToArray( $path, $key, $val, $this->attributes );
257  }
258  }
259 
260  }
261  }
262 
267  protected function extractAttributes( $path, array $info ) {
268  if ( isset( $info['attributes'] ) ) {
269  foreach ( $info['attributes'] as $extName => $value ) {
270  $this->storeToArray( $path, $extName, $value, $this->extAttributes );
271  }
272  }
273  }
274 
275  public function getExtractedInfo() {
276  // Make sure the merge strategies are set
277  foreach ( $this->globals as $key => $val ) {
278  if ( isset( self::$mergeStrategies[$key] ) ) {
279  $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::$mergeStrategies[$key];
280  }
281  }
282 
283  // Merge $this->extAttributes into $this->attributes depending on what is loaded
284  foreach ( $this->extAttributes as $extName => $value ) {
285  // Only set the attribute if $extName is loaded (and hence present in credits)
286  if ( isset( $this->credits[$extName] ) ) {
287  foreach ( $value as $attrName => $attrValue ) {
288  $this->storeToArray(
289  '', // Don't provide a path since it's impossible to generate an error here
290  $extName . $attrName,
291  $attrValue,
292  $this->attributes
293  );
294  }
295  unset( $this->extAttributes[$extName] );
296  }
297  }
298 
299  return [
300  'globals' => $this->globals,
301  'config' => $this->config,
302  'defines' => $this->defines,
303  'callbacks' => $this->callbacks,
304  'credits' => $this->credits,
305  'attributes' => $this->attributes,
306  ];
307  }
308 
309  public function getRequirements( array $info, $includeDev ) {
310  // Quick shortcuts
311  if ( !$includeDev || !isset( $info['dev-requires'] ) ) {
312  return $info['requires'] ?? [];
313  }
314 
315  if ( !isset( $info['requires'] ) ) {
316  return $info['dev-requires'] ?? [];
317  }
318 
319  // OK, we actually have to merge everything
320  $merged = [];
321 
322  // Helper that combines version requirements by
323  // picking the non-null if one is, or combines
324  // the two. Note that it is not possible for
325  // both inputs to be null.
326  $pick = function ( $a, $b ) {
327  if ( $a === null ) {
328  return $b;
329  } elseif ( $b === null ) {
330  return $a;
331  } else {
332  return "$a $b";
333  }
334  };
335 
336  $req = $info['requires'];
337  $dev = $info['dev-requires'];
338  if ( isset( $req['MediaWiki'] ) || isset( $dev['MediaWiki'] ) ) {
339  $merged['MediaWiki'] = $pick(
340  $req['MediaWiki'] ?? null,
341  $dev['MediaWiki'] ?? null
342  );
343  }
344 
345  $platform = array_merge(
346  array_keys( $req['platform'] ?? [] ),
347  array_keys( $dev['platform'] ?? [] )
348  );
349  if ( $platform ) {
350  foreach ( $platform as $pkey ) {
351  if ( $pkey === 'php' ) {
352  $value = $pick(
353  $req['platform']['php'] ?? null,
354  $dev['platform']['php'] ?? null
355  );
356  } else {
357  // Prefer dev value, but these should be constant
358  // anyways (ext-* and ability-*)
359  $value = $dev['platform'][$pkey] ?? $req['platform'][$pkey];
360  }
361  $merged['platform'][$pkey] = $value;
362  }
363  }
364 
365  foreach ( [ 'extensions', 'skins' ] as $thing ) {
366  $things = array_merge(
367  array_keys( $req[$thing] ?? [] ),
368  array_keys( $dev[$thing] ?? [] )
369  );
370  foreach ( $things as $name ) {
371  $merged[$thing][$name] = $pick(
372  $req[$thing][$name] ?? null,
373  $dev[$thing][$name] ?? null
374  );
375  }
376  }
377 
378  return $merged;
379  }
380 
381  protected function extractHooks( array $info ) {
382  if ( isset( $info['Hooks'] ) ) {
383  foreach ( $info['Hooks'] as $name => $value ) {
384  if ( is_array( $value ) ) {
385  foreach ( $value as $callback ) {
386  $this->globals['wgHooks'][$name][] = $callback;
387  }
388  } else {
389  $this->globals['wgHooks'][$name][] = $value;
390  }
391  }
392  }
393  }
394 
400  protected function extractNamespaces( array $info ) {
401  if ( isset( $info['namespaces'] ) ) {
402  foreach ( $info['namespaces'] as $ns ) {
403  if ( defined( $ns['constant'] ) ) {
404  // If the namespace constant is already defined, use it.
405  // This allows namespace IDs to be overwritten locally.
406  $id = constant( $ns['constant'] );
407  } else {
408  $id = $ns['id'];
409  $this->defines[ $ns['constant'] ] = $id;
410  }
411 
412  if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) {
413  // If it is not conditional, register it
414  $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
415  }
416  if ( isset( $ns['gender'] ) ) {
417  $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
418  }
419  if ( isset( $ns['subpages'] ) && $ns['subpages'] ) {
420  $this->globals['wgNamespacesWithSubpages'][$id] = true;
421  }
422  if ( isset( $ns['content'] ) && $ns['content'] ) {
423  $this->globals['wgContentNamespaces'][] = $id;
424  }
425  if ( isset( $ns['defaultcontentmodel'] ) ) {
426  $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel'];
427  }
428  if ( isset( $ns['protection'] ) ) {
429  $this->globals['wgNamespaceProtection'][$id] = $ns['protection'];
430  }
431  if ( isset( $ns['capitallinkoverride'] ) ) {
432  $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride'];
433  }
434  }
435  }
436  }
437 
438  protected function extractResourceLoaderModules( $dir, array $info ) {
439  $defaultPaths = $info['ResourceFileModulePaths'] ?? false;
440  if ( isset( $defaultPaths['localBasePath'] ) ) {
441  if ( $defaultPaths['localBasePath'] === '' ) {
442  // Avoid double slashes (e.g. /extensions/Example//path)
443  $defaultPaths['localBasePath'] = $dir;
444  } else {
445  $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}";
446  }
447  }
448 
449  foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
450  if ( isset( $info[$setting] ) ) {
451  foreach ( $info[$setting] as $name => $data ) {
452  if ( isset( $data['localBasePath'] ) ) {
453  if ( $data['localBasePath'] === '' ) {
454  // Avoid double slashes (e.g. /extensions/Example//path)
455  $data['localBasePath'] = $dir;
456  } else {
457  $data['localBasePath'] = "$dir/{$data['localBasePath']}";
458  }
459  }
460  if ( $defaultPaths ) {
461  $data += $defaultPaths;
462  }
463  if ( $setting === 'OOUIThemePaths' ) {
464  $this->attributes[$setting][$name] = $data;
465  } else {
466  $this->globals["wg$setting"][$name] = $data;
467  }
468  }
469  }
470  }
471 
472  if ( isset( $info['QUnitTestModule'] ) ) {
473  $data = $info['QUnitTestModule'];
474  if ( isset( $data['localBasePath'] ) ) {
475  if ( $data['localBasePath'] === '' ) {
476  // Avoid double slashes (e.g. /extensions/Example//path)
477  $data['localBasePath'] = $dir;
478  } else {
479  $data['localBasePath'] = "$dir/{$data['localBasePath']}";
480  }
481  }
482  $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data;
483  }
484  }
485 
486  protected function extractExtensionMessagesFiles( $dir, array $info ) {
487  if ( isset( $info['ExtensionMessagesFiles'] ) ) {
488  foreach ( $info['ExtensionMessagesFiles'] as &$file ) {
489  $file = "$dir/$file";
490  }
491  $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles'];
492  }
493  }
494 
502  protected function extractMessagesDirs( $dir, array $info ) {
503  if ( isset( $info['MessagesDirs'] ) ) {
504  foreach ( $info['MessagesDirs'] as $name => $files ) {
505  foreach ( (array)$files as $file ) {
506  $this->globals["wgMessagesDirs"][$name][] = "$dir/$file";
507  }
508  }
509  }
510  }
511 
518  protected function extractCredits( $path, array $info ) {
519  $credits = [
520  'path' => $path,
521  'type' => $info['type'] ?? 'other',
522  ];
523  foreach ( self::$creditsAttributes as $attr ) {
524  if ( isset( $info[$attr] ) ) {
525  $credits[$attr] = $info[$attr];
526  }
527  }
528 
529  $name = $credits['name'];
530 
531  // If someone is loading the same thing twice, throw
532  // a nice error (T121493)
533  if ( isset( $this->credits[$name] ) ) {
534  $firstPath = $this->credits[$name]['path'];
535  $secondPath = $credits['path'];
536  throw new Exception( "It was attempted to load $name twice, from $firstPath and $secondPath." );
537  }
538 
539  $this->credits[$name] = $credits;
540  $this->globals['wgExtensionCredits'][$credits['type']][] = $credits;
541 
542  return $name;
543  }
544 
551  protected function extractConfig1( array $info ) {
552  if ( isset( $info['config'] ) ) {
553  if ( isset( $info['config']['_prefix'] ) ) {
554  $prefix = $info['config']['_prefix'];
555  unset( $info['config']['_prefix'] );
556  } else {
557  $prefix = 'wg';
558  }
559  foreach ( $info['config'] as $key => $val ) {
560  if ( $key[0] !== '@' ) {
561  $this->addConfigGlobal( "$prefix$key", $val, $info['name'] );
562  }
563  }
564  }
565  }
566 
574  protected function extractConfig2( array $info, $dir ) {
575  $prefix = $info['config_prefix'] ?? 'wg';
576  if ( isset( $info['config'] ) ) {
577  foreach ( $info['config'] as $key => $data ) {
578  $value = $data['value'];
579  if ( isset( $data['path'] ) && $data['path'] ) {
580  $callback = function ( $value ) use ( $dir ) {
581  return "$dir/$value";
582  };
583  if ( is_array( $value ) ) {
584  $value = array_map( $callback, $value );
585  } else {
586  $value = $callback( $value );
587  }
588  }
589  if ( isset( $data['merge_strategy'] ) ) {
590  $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy'];
591  }
592  $this->addConfigGlobal( "$prefix$key", $value, $info['name'] );
593  $data['providedby'] = $info['name'];
594  if ( isset( $info['ConfigRegistry'][0] ) ) {
595  $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0];
596  }
597  $this->config[$key] = $data;
598  }
599  }
600  }
601 
609  private function addConfigGlobal( $key, $value, $extName ) {
610  if ( array_key_exists( $key, $this->globals ) ) {
611  throw new RuntimeException(
612  "The configuration setting '$key' was already set by MediaWiki core or"
613  . " another extension, and cannot be set again by $extName." );
614  }
615  $this->globals[$key] = $value;
616  }
617 
618  protected function extractPathBasedGlobal( $global, $dir, $paths ) {
619  foreach ( $paths as $path ) {
620  $this->globals[$global][] = "$dir/$path";
621  }
622  }
623 
631  protected function storeToArray( $path, $name, $value, &$array ) {
632  if ( !is_array( $value ) ) {
633  throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" );
634  }
635  if ( isset( $array[$name] ) ) {
636  $array[$name] = array_merge_recursive( $array[$name], $value );
637  } else {
638  $array[$name] = $value;
639  }
640  }
641 
642  public function getExtraAutoloaderPaths( $dir, array $info ) {
643  $paths = [];
644  if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) {
645  $paths[] = "$dir/vendor/autoload.php";
646  }
647  return $paths;
648  }
649 }
extractCredits( $path, array $info)
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
extractInfo( $path, array $info, $version)
array $globals
Stuff that is going to be set to $GLOBALS.
static array $mergeStrategies
Mapping of global settings to their specific merge strategies.
extractMessagesDirs( $dir, array $info)
Set message-related settings, which need to be expanded to use absolute paths.
const MERGE_STRATEGY
Special key that defines the merge strategy.
extractAttributes( $path, array $info)
static array $notAttributes
Things that are not &#39;attributes&#39;, and are not in $globalSettings or $creditsAttributes.
callable [] $callbacks
Things to be called once registration of these extensions are done keyed by the name of the extension...
addConfigGlobal( $key, $value, $extName)
Helper function to set a value to a specific global, if it isn&#39;t set already.
static array $globalSettings
Keys that should be set to $GLOBALS.
array $attributes
Any thing else in the $info that hasn&#39;t already been processed.
static array static array $creditsAttributes
Keys that are part of the extension credits.
extractResourceLoaderModules( $dir, array $info)
getRequirements(array $info, $includeDev)
Get the requirements for the provided info.
extractExtensionMessagesFiles( $dir, array $info)
array array $defines
Things that should be define()&#39;d.
storeToArray( $path, $name, $value, &$array)
getExtraAutoloaderPaths( $dir, array $info)
Get the path for additional autoloaders, e.g.
extractConfig1(array $info)
Set configuration settings for manifest_version == 1.
Processors read associated arrays and register whatever is required.
Definition: Processor.php:9
array $extAttributes
Extension attributes, keyed by name => settings.
extractConfig2(array $info, $dir)
Set configuration settings for manifest_version == 2.
extractPathBasedGlobal( $global, $dir, $paths)
static string [] $coreAttributes
Top-level attributes that come from MW core.
extractNamespaces(array $info)
Register namespaces with the appropriate global settings.