MediaWiki  master
ExtensionRegistry.php
Go to the documentation of this file.
1 <?php
2 
3 use Composer\Semver\Semver;
4 use Wikimedia\AtEase\AtEase;
5 use Wikimedia\ScopedCallback;
8 
19 
23  public const MEDIAWIKI_CORE = 'MediaWiki';
24 
29  public const MANIFEST_VERSION = 2;
30 
35  public const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
36 
40  public const OLDEST_MANIFEST_VERSION = 1;
41 
45  private const CACHE_VERSION = 7;
46 
52  public const MERGE_STRATEGY = '_merge_strategy';
53 
57  private const LAZY_LOADED_ATTRIBUTES = [
58  'QUnitTestModules',
59  ];
60 
66  private $loaded = [];
67 
73  protected $queued = [];
74 
80  private $finished = false;
81 
88  protected $attributes = [];
89 
95  protected $testAttributes = [];
96 
102  protected $lazyAttributes = [];
103 
109  protected $checkDev = false;
110 
116  protected $loadTestClassesAndNamespaces = false;
117 
121  private static $instance;
122 
127  public static function getInstance() {
128  if ( self::$instance === null ) {
129  self::$instance = new self();
130  }
131 
132  return self::$instance;
133  }
134 
139  public function setCheckDevRequires( $check ) {
140  $this->checkDev = $check;
141  }
142 
150  public function setLoadTestClassesAndNamespaces( $load ) {
151  $this->loadTestClassesAndNamespaces = $load;
152  }
153 
157  public function queue( $path ) {
158  global $wgExtensionInfoMTime;
159 
160  $mtime = $wgExtensionInfoMTime;
161  if ( $mtime === false ) {
162  AtEase::suppressWarnings();
163  $mtime = filemtime( $path );
164  AtEase::restoreWarnings();
165  // @codeCoverageIgnoreStart
166  if ( $mtime === false ) {
167  $err = error_get_last();
168  throw new Exception( "Unable to open file $path: {$err['message']}" );
169  // @codeCoverageIgnoreEnd
170  }
171  }
172  $this->queued[$path] = $mtime;
173  }
174 
179  public function loadFromQueue() {
181  if ( !$this->queued ) {
182  return;
183  }
184 
185  if ( $this->finished ) {
186  throw new MWException(
187  "The following paths tried to load late: "
188  . implode( ', ', array_keys( $this->queued ) )
189  );
190  }
191 
192  // A few more things to vary the cache on
193  $versions = [
194  'registration' => self::CACHE_VERSION,
195  'mediawiki' => $wgVersion,
196  'abilities' => $this->getAbilities(),
197  'checkDev' => $this->checkDev,
198  ];
199 
200  // We use a try/catch because we don't want to fail here
201  // if $wgObjectCaches is not configured properly for APC setup
202  try {
203  // Avoid MediaWikiServices to prevent instantiating it before extensions have loaded
206  } catch ( InvalidArgumentException $e ) {
207  $cache = new EmptyBagOStuff();
208  }
209  // See if this queue is in APC
210  $key = $cache->makeKey(
211  'registration',
212  md5( json_encode( $this->queued + $versions ) )
213  );
214  $data = $cache->get( $key );
215  if ( $data ) {
216  $this->exportExtractedData( $data );
217  } else {
218  $data = $this->readFromQueue( $this->queued );
219  $this->exportExtractedData( $data );
220  $this->prepForCache( $data );
221  if ( !( $data['warnings'] && $wgDevelopmentWarnings ) ) {
222  // If there were no warnings that were shown, cache it
223  $cache->set( $key, $data, 60 * 60 * 24 );
224  }
225  }
226  $this->queued = [];
227  }
228 
234  protected function prepForCache( array &$data ) {
235  // Do this late since we don't want to extract it since we already
236  // did that, but it should be cached
237  $data['globals']['wgAutoloadClasses'] += $data['autoload'];
238  unset( $data['autoload'] );
239  // Don't cache any lazy-loaded attributes
240  foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
241  unset( $data['attributes'][$attrib] );
242  }
243  }
244 
251  public function getQueue() {
252  return $this->queued;
253  }
254 
259  public function clearQueue() {
260  $this->queued = [];
261  }
262 
268  public function finish() {
269  $this->finished = true;
270  }
271 
276  private function getAbilities() {
277  return [
278  'shell' => !Shell::isDisabled(),
279  ];
280  }
281 
287  private function buildVersionChecker() {
288  global $wgVersion;
289  // array to optionally specify more verbose error messages for
290  // missing abilities
291  $abilityErrors = [
292  'shell' => ( new ShellDisabledError() )->getMessage(),
293  ];
294 
295  return new VersionChecker(
296  $wgVersion,
297  PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
298  get_loaded_extensions(),
299  $this->getAbilities(),
300  $abilityErrors
301  );
302  }
303 
312  public function readFromQueue( array $queue ) {
313  $autoloadClasses = [];
314  $autoloadNamespaces = [];
315  $autoloaderPaths = [];
316  $processor = new ExtensionProcessor();
317  $versionChecker = $this->buildVersionChecker();
318  $extDependencies = [];
319  $warnings = false;
320  foreach ( $queue as $path => $mtime ) {
321  $json = file_get_contents( $path );
322  if ( $json === false ) {
323  throw new Exception( "Unable to read $path, does it exist?" );
324  }
325  $info = json_decode( $json, /* $assoc = */ true );
326  if ( !is_array( $info ) ) {
327  throw new Exception( "$path is not a valid JSON file." );
328  }
329 
330  if ( !isset( $info['manifest_version'] ) ) {
331  wfDeprecated(
332  "{$info['name']}'s extension.json or skin.json does not have manifest_version",
333  '1.29'
334  );
335  $warnings = true;
336  // For backwards-compatability, assume a version of 1
337  $info['manifest_version'] = 1;
338  }
339  $version = $info['manifest_version'];
340  if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
341  throw new Exception( "$path: unsupported manifest_version: {$version}" );
342  }
343 
344  $dir = dirname( $path );
346  $dir,
347  $info,
348  $autoloadClasses,
349  $autoloadNamespaces
350  );
351 
352  if ( $this->loadTestClassesAndNamespaces ) {
354  $dir,
355  $info,
356  $autoloadClasses,
357  $autoloadNamespaces
358  );
359  }
360 
361  // get all requirements/dependencies for this extension
362  $requires = $processor->getRequirements( $info, $this->checkDev );
363 
364  // validate the information needed and add the requirements
365  if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
366  $extDependencies[$info['name']] = $requires;
367  }
368 
369  // Get extra paths for later inclusion
370  $autoloaderPaths = array_merge( $autoloaderPaths,
371  $processor->getExtraAutoloaderPaths( $dir, $info ) );
372  // Compatible, read and extract info
373  $processor->extractInfo( $path, $info, $version );
374  }
375  $data = $processor->getExtractedInfo();
376  $data['warnings'] = $warnings;
377 
378  // check for incompatible extensions
379  $incompatible = $versionChecker
380  ->setLoadedExtensionsAndSkins( $data['credits'] )
381  ->checkArray( $extDependencies );
382 
383  if ( $incompatible ) {
384  throw new ExtensionDependencyError( $incompatible );
385  }
386 
387  // Need to set this so we can += to it later
388  $data['globals']['wgAutoloadClasses'] = [];
389  $data['autoload'] = $autoloadClasses;
390  $data['autoloaderPaths'] = $autoloaderPaths;
391  $data['autoloaderNS'] = $autoloadNamespaces;
392  return $data;
393  }
394 
403  public static function exportAutoloadClassesAndNamespaces(
404  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
405  ) {
406  if ( isset( $info['AutoloadClasses'] ) ) {
407  $autoload = self::processAutoLoader( $dir, $info['AutoloadClasses'] );
408  $GLOBALS['wgAutoloadClasses'] += $autoload;
409  $autoloadClasses += $autoload;
410  }
411  if ( isset( $info['AutoloadNamespaces'] ) ) {
412  $autoloadNamespaces += self::processAutoLoader( $dir, $info['AutoloadNamespaces'] );
413  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
414  }
415  }
416 
427  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
428  ) {
429  if ( isset( $info['TestAutoloadClasses'] ) ) {
430  $autoload = self::processAutoLoader( $dir, $info['TestAutoloadClasses'] );
431  $GLOBALS['wgAutoloadClasses'] += $autoload;
432  $autoloadClasses += $autoload;
433  }
434  if ( isset( $info['TestAutoloadNamespaces'] ) ) {
435  $autoloadNamespaces += self::processAutoLoader( $dir, $info['TestAutoloadNamespaces'] );
436  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
437  }
438  }
439 
440  protected function exportExtractedData( array $info ) {
441  foreach ( $info['globals'] as $key => $val ) {
442  // If a merge strategy is set, read it and remove it from the value
443  // so it doesn't accidentally end up getting set.
444  if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
445  $mergeStrategy = $val[self::MERGE_STRATEGY];
446  unset( $val[self::MERGE_STRATEGY] );
447  } else {
448  $mergeStrategy = 'array_merge';
449  }
450 
451  // Optimistic: If the global is not set, or is an empty array, replace it entirely.
452  // Will be O(1) performance.
453  if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
454  $GLOBALS[$key] = $val;
455  continue;
456  }
457 
458  if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
459  // config setting that has already been overridden, don't set it
460  continue;
461  }
462 
463  switch ( $mergeStrategy ) {
464  case 'array_merge_recursive':
465  $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
466  break;
467  case 'array_replace_recursive':
468  $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
469  break;
470  case 'array_plus_2d':
471  $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
472  break;
473  case 'array_plus':
474  $GLOBALS[$key] += $val;
475  break;
476  case 'array_merge':
477  $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
478  break;
479  default:
480  throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
481  }
482  }
483 
484  if ( isset( $info['autoloaderNS'] ) ) {
485  AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
486  }
487 
488  foreach ( $info['defines'] as $name => $val ) {
489  define( $name, $val );
490  }
491  foreach ( $info['autoloaderPaths'] as $path ) {
492  if ( file_exists( $path ) ) {
493  require_once $path;
494  }
495  }
496 
497  $this->loaded += $info['credits'];
498  if ( $info['attributes'] ) {
499  if ( !$this->attributes ) {
500  $this->attributes = $info['attributes'];
501  } else {
502  $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
503  }
504  }
505 
506  foreach ( $info['callbacks'] as $name => $cb ) {
507  if ( !is_callable( $cb ) ) {
508  if ( is_array( $cb ) ) {
509  $cb = '[ ' . implode( ', ', $cb ) . ' ]';
510  }
511  throw new UnexpectedValueException( "callback '$cb' is not callable" );
512  }
513  $cb( $info['credits'][$name] );
514  }
515  }
516 
526  public function load( $path ) {
527  wfDeprecated( __METHOD__, '1.34' );
528  $this->loadFromQueue(); // First clear the queue
529  $this->queue( $path );
530  $this->loadFromQueue();
531  }
532 
541  public function isLoaded( $name, $constraint = '*' ) {
542  $isLoaded = isset( $this->loaded[$name] );
543  if ( $constraint === '*' || !$isLoaded ) {
544  return $isLoaded;
545  }
546  // if a specific constraint is requested, but no version is set, throw an exception
547  if ( !isset( $this->loaded[$name]['version'] ) ) {
548  $msg = "{$name} does not expose its version, but an extension or a skin"
549  . " requires: {$constraint}.";
550  throw new LogicException( $msg );
551  }
552 
553  return SemVer::satisfies( $this->loaded[$name]['version'], $constraint );
554  }
555 
560  public function getAttribute( $name ) {
561  return $this->testAttributes[$name] ??
562  $this->attributes[$name] ?? [];
563  }
564 
572  public function getLazyLoadedAttribute( $name ) {
573  if ( isset( $this->testAttributes[$name] ) ) {
574  return $this->testAttributes[$name];
575  }
576 
577  $paths = [];
578  foreach ( $this->loaded as $info ) {
579  // mtime (array value) doesn't matter here since
580  // we're skipping cache, so use a dummy time
581  $paths[$info['path']] = 1;
582  }
583 
584  $result = $this->readFromQueue( $paths );
585  return $result['attributes'][$name] ?? [];
586  }
587 
596  public function setAttributeForTest( $name, array $value ) {
597  // @codeCoverageIgnoreStart
598  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
599  throw new RuntimeException( __METHOD__ . ' can only be used in tests' );
600  }
601  // @codeCoverageIgnoreEnd
602  if ( isset( $this->testAttributes[$name] ) ) {
603  throw new Exception( "The attribute '$name' has already been overridden" );
604  }
605  $this->testAttributes[$name] = $value;
606  return new ScopedCallback( function () use ( $name ) {
607  unset( $this->testAttributes[$name] );
608  } );
609  }
610 
616  public function getAllThings() {
617  return $this->loaded;
618  }
619 
627  protected static function processAutoLoader( $dir, array $files ) {
628  // Make paths absolute, relative to the JSON file
629  foreach ( $files as &$file ) {
630  $file = "$dir/$file";
631  }
632  return $files;
633  }
634 }
ExtensionRegistry\isLoaded
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
Definition: ExtensionRegistry.php:541
ExtensionRegistry\setLoadTestClassesAndNamespaces
setLoadTestClassesAndNamespaces( $load)
Controls if classes and namespaces defined under the keys TestAutoloadClasses and TestAutloadNamespac...
Definition: ExtensionRegistry.php:150
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:44
ExtensionRegistry\getQueue
getQueue()
Get the current load queue.
Definition: ExtensionRegistry.php:251
ExtensionRegistry\$finished
bool $finished
Whether we are done loading things.
Definition: ExtensionRegistry.php:80
ExtensionRegistry\MANIFEST_VERSION
const MANIFEST_VERSION
Version of the highest supported manifest version Note: Update MANIFEST_VERSION_MW_VERSION when chang...
Definition: ExtensionRegistry.php:29
ExtensionRegistry\queue
queue( $path)
Definition: ExtensionRegistry.php:157
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
wfArrayPlus2d
wfArrayPlus2d(array $baseArray, array $newValues)
Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
Definition: GlobalFunctions.php:3071
ExtensionDependencyError
Copyright (C) 2018 Kunal Mehta legoktm@member.fsf.org
Definition: ExtensionDependencyError.php:24
ExtensionRegistry\exportAutoloadClassesAndNamespaces
static exportAutoloadClassesAndNamespaces( $dir, $info, &$autoloadClasses=[], &$autoloadNamespaces=[])
Export autoload classes and namespaces for a given directory and parsed JSON info file.
Definition: ExtensionRegistry.php:403
ObjectCache\detectLocalServerCache
static detectLocalServerCache()
Detects which local server cache library is present and returns a configuration for it.
Definition: ObjectCache.php:286
ExtensionRegistry\exportExtractedData
exportExtractedData(array $info)
Definition: ExtensionRegistry.php:440
ExtensionRegistry
ExtensionRegistry class.
Definition: ExtensionRegistry.php:18
$wgVersion
$wgVersion
MediaWiki version number.
Definition: DefaultSettings.php:75
ExtensionRegistry\$loadTestClassesAndNamespaces
bool $loadTestClassesAndNamespaces
Whether test classes and namespaces should be added to the auto loader.
Definition: ExtensionRegistry.php:116
ExtensionRegistry\getLazyLoadedAttribute
getLazyLoadedAttribute( $name)
Get an attribute value that isn't cached by reading each extension.json file again.
Definition: ExtensionRegistry.php:572
ExtensionRegistry\getAllThings
getAllThings()
Get information about all things.
Definition: ExtensionRegistry.php:616
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ExtensionRegistry\setCheckDevRequires
setCheckDevRequires( $check)
Definition: ExtensionRegistry.php:139
ExtensionRegistry\exportTestAutoloadClassesAndNamespaces
static exportTestAutoloadClassesAndNamespaces( $dir, $info, &$autoloadClasses=[], &$autoloadNamespaces=[])
Export test autoload classes and namespaces for a given directory and parsed JSON info file.
Definition: ExtensionRegistry.php:426
ExtensionRegistry\getAbilities
getAbilities()
Get the list of abilities and their values.
Definition: ExtensionRegistry.php:276
ExtensionRegistry\setAttributeForTest
setAttributeForTest( $name, array $value)
Force override the value of an attribute during tests.
Definition: ExtensionRegistry.php:596
ExtensionRegistry\clearQueue
clearQueue()
Clear the current load queue.
Definition: ExtensionRegistry.php:259
ExtensionRegistry\MERGE_STRATEGY
const MERGE_STRATEGY
Special key that defines the merge strategy.
Definition: ExtensionRegistry.php:52
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:127
ExtensionProcessor
Definition: ExtensionProcessor.php:3
ExtensionRegistry\CACHE_VERSION
const CACHE_VERSION
Bump whenever the registration cache needs resetting.
Definition: ExtensionRegistry.php:45
MWException
MediaWiki exception.
Definition: MWException.php:26
ExtensionRegistry\$attributes
array $attributes
Items in the JSON file that aren't being set as globals.
Definition: ExtensionRegistry.php:88
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1044
ExtensionRegistry\processAutoLoader
static processAutoLoader( $dir, array $files)
Fully expand autoloader paths.
Definition: ExtensionRegistry.php:627
ExtensionRegistry\load
load( $path)
Loads and processes the given JSON file without delay.
Definition: ExtensionRegistry.php:526
$wgObjectCaches
$wgObjectCaches
Advanced object cache configuration.
Definition: DefaultSettings.php:2388
$queue
$queue
Definition: mergeMessageFileList.php:157
ExtensionRegistry\MANIFEST_VERSION_MW_VERSION
const MANIFEST_VERSION_MW_VERSION
MediaWiki version constraint representing what the current highest MANIFEST_VERSION is supported in.
Definition: ExtensionRegistry.php:35
AutoLoader\$psr4Namespaces
static string[] $psr4Namespaces
Definition: AutoLoader.php:37
ExtensionRegistry\finish
finish()
After this is called, no more extensions can be loaded.
Definition: ExtensionRegistry.php:268
ExtensionRegistry\loadFromQueue
loadFromQueue()
Definition: ExtensionRegistry.php:179
ExtensionRegistry\OLDEST_MANIFEST_VERSION
const OLDEST_MANIFEST_VERSION
Version of the oldest supported manifest version.
Definition: ExtensionRegistry.php:40
ExtensionRegistry\MEDIAWIKI_CORE
const MEDIAWIKI_CORE
"requires" key that applies to MediaWiki core/$wgVersion
Definition: ExtensionRegistry.php:23
ExtensionRegistry\readFromQueue
readFromQueue(array $queue)
Process a queue of extensions and return their extracted data.
Definition: ExtensionRegistry.php:312
ExtensionRegistry\$queued
array $queued
List of paths that should be loaded.
Definition: ExtensionRegistry.php:73
ExtensionRegistry\LAZY_LOADED_ATTRIBUTES
const LAZY_LOADED_ATTRIBUTES
Attributes that should be lazy-loaded and not cached.
Definition: ExtensionRegistry.php:57
ExtensionRegistry\$testAttributes
array $testAttributes
Attributes for testing.
Definition: ExtensionRegistry.php:95
$wgDevelopmentWarnings
$wgDevelopmentWarnings
If set to true MediaWiki will throw notices for some possible error conditions and for deprecated fun...
Definition: DefaultSettings.php:6349
MediaWiki\ShellDisabledError
Definition: ShellDisabledError.php:28
ExtensionRegistry\prepForCache
prepForCache(array &$data)
Adjust data before it gets cached.
Definition: ExtensionRegistry.php:234
ExtensionRegistry\$loaded
array[] $loaded
Array of loaded things, keyed by name, values are credits information.
Definition: ExtensionRegistry.php:66
VersionChecker
Provides functions to check a set of extensions with dependencies against a set of loaded extensions ...
Definition: VersionChecker.php:32
$cache
$cache
Definition: mcc.php:33
ExtensionRegistry\buildVersionChecker
buildVersionChecker()
Queries information about the software environment and constructs an appropiate version checker.
Definition: ExtensionRegistry.php:287
ExtensionRegistry\$lazyAttributes
array $lazyAttributes
Lazy-loaded attributes.
Definition: ExtensionRegistry.php:102
$path
$path
Definition: NoLocalSettings.php:25
$wgExtensionInfoMTime
int bool $wgExtensionInfoMTime
When loading extensions through the extension registration system, this can be used to invalidate the...
Definition: DefaultSettings.php:2718
ObjectCache\newFromParams
static newFromParams( $params)
Create a new cache object from parameters.
Definition: ObjectCache.php:142
ExtensionRegistry\getAttribute
getAttribute( $name)
Definition: ExtensionRegistry.php:560
ExtensionRegistry\$checkDev
bool $checkDev
Whether to check dev-requires.
Definition: ExtensionRegistry.php:109
ExtensionRegistry\$instance
static ExtensionRegistry $instance
Definition: ExtensionRegistry.php:121
$GLOBALS
$GLOBALS['IP']
Definition: ComposerHookHandler.php:6