MediaWiki  master
ExtensionRegistry.php
Go to the documentation of this file.
1 <?php
2 
3 use Composer\Semver\Semver;
6 use Wikimedia\AtEase\AtEase;
7 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 
47  private const CACHE_EXPIRY = 60 * 60 * 24;
48 
54  public const MERGE_STRATEGY = '_merge_strategy';
55 
59  private const LAZY_LOADED_ATTRIBUTES = [
60  'TrackingCategories',
61  'QUnitTestModules',
62  ];
63 
75  private $loaded = [];
76 
82  protected $queued = [];
83 
89  private $finished = false;
90 
97  protected $attributes = [];
98 
104  protected $testAttributes = [];
105 
111  protected $lazyAttributes = [];
112 
118  protected $checkDev = false;
119 
125  protected $loadTestClassesAndNamespaces = false;
126 
130  private static $instance;
131 
136  public static function getInstance() {
137  if ( self::$instance === null ) {
138  self::$instance = new self();
139  }
140 
141  return self::$instance;
142  }
143 
148  public function setCheckDevRequires( $check ) {
149  $this->checkDev = $check;
150  }
151 
159  public function setLoadTestClassesAndNamespaces( $load ) {
160  $this->loadTestClassesAndNamespaces = $load;
161  }
162 
166  public function queue( $path ) {
167  global $wgExtensionInfoMTime;
168 
169  $mtime = $wgExtensionInfoMTime;
170  if ( $mtime === false ) {
171  AtEase::suppressWarnings();
172  $mtime = filemtime( $path );
173  AtEase::restoreWarnings();
174  // @codeCoverageIgnoreStart
175  if ( $mtime === false ) {
176  $err = error_get_last();
177  throw new Exception( "Unable to open file $path: {$err['message']}" );
178  // @codeCoverageIgnoreEnd
179  }
180  }
181  $this->queued[$path] = $mtime;
182  }
183 
184  private function getCache() : BagOStuff {
185  // Can't call MediaWikiServices here, as we must not cause services
186  // to be instantiated before extensions have loaded.
188  }
189 
190  private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) {
191  // Everything we vary the cache on
192  $vary = [
193  'registration' => self::CACHE_VERSION,
194  'mediawiki' => MW_VERSION,
195  'abilities' => $this->getAbilities(),
196  'checkDev' => $this->checkDev,
197  'queue' => $this->queued,
198  ];
199  return $cache->makeKey(
200  "registration-$component",
201  // We vary the cache on the current queue (what will be or already was loaded)
202  // plus various versions of stuff for VersionChecker
203  md5( json_encode( $vary ) ),
204  ...$extra
205  );
206  }
207 
212  public function loadFromQueue() {
213  if ( !$this->queued ) {
214  return;
215  }
216 
217  if ( $this->finished ) {
218  throw new MWException(
219  "The following paths tried to load late: "
220  . implode( ', ', array_keys( $this->queued ) )
221  );
222  }
223 
224  $cache = $this->getCache();
225  // See if this queue is in APC
226  $key = $this->makeCacheKey( $cache, 'main' );
227  $data = $cache->get( $key );
228  if ( !$data ) {
229  $data = $this->readFromQueue( $this->queued );
230  $this->saveToCache( $cache, $data );
231  }
232  $this->exportExtractedData( $data );
233  }
234 
241  protected function saveToCache( BagOStuff $cache, array $data ) {
242  global $wgDevelopmentWarnings;
243  if ( $data['warnings'] && $wgDevelopmentWarnings ) {
244  // If warnings were shown, don't cache it
245  return;
246  }
247  $lazy = [];
248  // Cache lazy-loaded attributes separately
249  foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
250  if ( isset( $data['attributes'][$attrib] ) ) {
251  $lazy[$attrib] = $data['attributes'][$attrib];
252  unset( $data['attributes'][$attrib] );
253  }
254  }
255  $mainKey = $this->makeCacheKey( $cache, 'main' );
256  $cache->set( $mainKey, $data, self::CACHE_EXPIRY );
257  foreach ( $lazy as $attrib => $value ) {
258  $cache->set(
259  $this->makeCacheKey( $cache, 'lazy-attrib', $attrib ),
260  $value,
261  self::CACHE_EXPIRY
262  );
263  }
264  }
265 
272  public function getQueue() {
273  return $this->queued;
274  }
275 
280  public function clearQueue() {
281  $this->queued = [];
282  }
283 
289  public function finish() {
290  $this->finished = true;
291  }
292 
297  private function getAbilities() {
298  return [
299  'shell' => !Shell::isDisabled(),
300  ];
301  }
302 
308  private function buildVersionChecker() {
309  // array to optionally specify more verbose error messages for
310  // missing abilities
311  $abilityErrors = [
312  'shell' => ( new ShellDisabledError() )->getMessage(),
313  ];
314 
315  return new VersionChecker(
316  MW_VERSION,
317  PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
318  get_loaded_extensions(),
319  $this->getAbilities(),
320  $abilityErrors
321  );
322  }
323 
332  public function readFromQueue( array $queue ) {
333  $autoloadClasses = [];
334  $autoloadNamespaces = [];
335  $autoloaderPaths = [];
336  $processor = new ExtensionProcessor();
337  $versionChecker = $this->buildVersionChecker();
338  $extDependencies = [];
339  $warnings = false;
340  foreach ( $queue as $path => $mtime ) {
341  $json = file_get_contents( $path );
342  if ( $json === false ) {
343  throw new Exception( "Unable to read $path, does it exist?" );
344  }
345  $info = json_decode( $json, /* $assoc = */ true );
346  if ( !is_array( $info ) ) {
347  throw new Exception( "$path is not a valid JSON file." );
348  }
349 
350  if ( !isset( $info['manifest_version'] ) ) {
352  "{$info['name']}'s extension.json or skin.json does not have manifest_version, " .
353  'this is deprecated since MediaWiki 1.29',
354  '1.29', false, false
355  );
356  $warnings = true;
357  // For backwards-compatibility, assume a version of 1
358  $info['manifest_version'] = 1;
359  }
360  $version = $info['manifest_version'];
361  if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
362  throw new Exception( "$path: unsupported manifest_version: {$version}" );
363  }
364 
365  $dir = dirname( $path );
367  $dir,
368  $info,
369  $autoloadClasses,
370  $autoloadNamespaces
371  );
372 
373  if ( $this->loadTestClassesAndNamespaces ) {
375  $dir,
376  $info,
377  $autoloadClasses,
378  $autoloadNamespaces
379  );
380  }
381 
382  // get all requirements/dependencies for this extension
383  $requires = $processor->getRequirements( $info, $this->checkDev );
384 
385  // validate the information needed and add the requirements
386  if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
387  $extDependencies[$info['name']] = $requires;
388  }
389 
390  // Get extra paths for later inclusion
391  $autoloaderPaths = array_merge( $autoloaderPaths,
392  $processor->getExtraAutoloaderPaths( $dir, $info ) );
393  // Compatible, read and extract info
394  $processor->extractInfo( $path, $info, $version );
395  }
396  $data = $processor->getExtractedInfo();
397  $data['warnings'] = $warnings;
398 
399  // check for incompatible extensions
400  $incompatible = $versionChecker
401  ->setLoadedExtensionsAndSkins( $data['credits'] )
402  ->checkArray( $extDependencies );
403 
404  if ( $incompatible ) {
405  throw new ExtensionDependencyError( $incompatible );
406  }
407 
408  // FIXME: It was a design mistake to handle autoloading separately (T240535)
409  $data['globals']['wgAutoloadClasses'] = $autoloadClasses;
410  $data['autoloaderPaths'] = $autoloaderPaths;
411  $data['autoloaderNS'] = $autoloadNamespaces;
412  return $data;
413  }
414 
423  public static function exportAutoloadClassesAndNamespaces(
424  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
425  ) {
426  if ( isset( $info['AutoloadClasses'] ) ) {
427  $autoload = self::processAutoLoader( $dir, $info['AutoloadClasses'] );
428  // @phan-suppress-next-line PhanUndeclaredVariableAssignOp
429  $GLOBALS['wgAutoloadClasses'] += $autoload;
430  $autoloadClasses += $autoload;
431  }
432  if ( isset( $info['AutoloadNamespaces'] ) ) {
433  $autoloadNamespaces += self::processAutoLoader( $dir, $info['AutoloadNamespaces'] );
434  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
435  }
436  }
437 
448  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
449  ) {
450  if ( isset( $info['TestAutoloadClasses'] ) ) {
451  $autoload = self::processAutoLoader( $dir, $info['TestAutoloadClasses'] );
452  $GLOBALS['wgAutoloadClasses'] += $autoload;
453  $autoloadClasses += $autoload;
454  }
455  if ( isset( $info['TestAutoloadNamespaces'] ) ) {
456  $autoloadNamespaces += self::processAutoLoader( $dir, $info['TestAutoloadNamespaces'] );
457  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
458  }
459  }
460 
461  protected function exportExtractedData( array $info ) {
462  foreach ( $info['globals'] as $key => $val ) {
463  // If a merge strategy is set, read it and remove it from the value
464  // so it doesn't accidentally end up getting set.
465  if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
466  $mergeStrategy = $val[self::MERGE_STRATEGY];
467  unset( $val[self::MERGE_STRATEGY] );
468  } else {
469  $mergeStrategy = 'array_merge';
470  }
471 
472  // Optimistic: If the global is not set, or is an empty array, replace it entirely.
473  // Will be O(1) performance.
474  if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
475  $GLOBALS[$key] = $val;
476  continue;
477  }
478 
479  if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
480  // config setting that has already been overridden, don't set it
481  continue;
482  }
483 
484  switch ( $mergeStrategy ) {
485  case 'array_merge_recursive':
486  $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
487  break;
488  case 'array_replace_recursive':
489  $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
490  break;
491  case 'array_plus_2d':
492  $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
493  break;
494  case 'array_plus':
495  $GLOBALS[$key] += $val;
496  break;
497  case 'array_merge':
498  $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
499  break;
500  default:
501  throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
502  }
503  }
504 
505  if ( isset( $info['autoloaderNS'] ) ) {
506  AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
507  }
508 
509  foreach ( $info['defines'] as $name => $val ) {
510  if ( !defined( $name ) ) {
511  define( $name, $val );
512  } elseif ( constant( $name ) !== $val ) {
513  throw new UnexpectedValueException(
514  "$name cannot be re-defined with $val it has already been set with " . constant( $name )
515  );
516  }
517  }
518 
519  foreach ( $info['autoloaderPaths'] as $path ) {
520  if ( file_exists( $path ) ) {
521  require_once $path;
522  }
523  }
524 
525  $this->loaded += $info['credits'];
526  if ( $info['attributes'] ) {
527  if ( !$this->attributes ) {
528  $this->attributes = $info['attributes'];
529  } else {
530  $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
531  }
532  }
533 
534  foreach ( $info['callbacks'] as $name => $cb ) {
535  if ( !is_callable( $cb ) ) {
536  if ( is_array( $cb ) ) {
537  $cb = '[ ' . implode( ', ', $cb ) . ' ]';
538  }
539  throw new UnexpectedValueException( "callback '$cb' is not callable" );
540  }
541  $cb( $info['credits'][$name] );
542  }
543  }
544 
553  public function isLoaded( $name, $constraint = '*' ) {
554  $isLoaded = isset( $this->loaded[$name] );
555  if ( $constraint === '*' || !$isLoaded ) {
556  return $isLoaded;
557  }
558  // if a specific constraint is requested, but no version is set, throw an exception
559  if ( !isset( $this->loaded[$name]['version'] ) ) {
560  $msg = "{$name} does not expose its version, but an extension or a skin"
561  . " requires: {$constraint}.";
562  throw new LogicException( $msg );
563  }
564 
565  return Semver::satisfies( $this->loaded[$name]['version'], $constraint );
566  }
567 
572  public function getAttribute( $name ) {
573  if ( isset( $this->testAttributes[$name] ) ) {
574  return $this->testAttributes[$name];
575  }
576 
577  if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES, true ) ) {
578  return $this->getLazyLoadedAttribute( $name );
579  }
580 
581  return $this->attributes[$name] ?? [];
582  }
583 
590  protected function getLazyLoadedAttribute( $name ) {
591  if ( isset( $this->testAttributes[$name] ) ) {
592  return $this->testAttributes[$name];
593  }
594 
595  // See if it's in the cache
596  $cache = $this->getCache();
597  $key = $this->makeCacheKey( $cache, 'lazy-attrib', $name );
598  $data = $cache->get( $key );
599  if ( $data !== false ) {
600  return $data;
601  }
602 
603  $paths = [];
604  foreach ( $this->loaded as $info ) {
605  // mtime (array value) doesn't matter here since
606  // we're skipping cache, so use a dummy time
607  $paths[$info['path']] = 1;
608  }
609 
610  $result = $this->readFromQueue( $paths );
611  $data = $result['attributes'][$name] ?? [];
612  $this->saveToCache( $cache, $result );
613 
614  return $data;
615  }
616 
625  public function setAttributeForTest( $name, array $value ) {
626  // @codeCoverageIgnoreStart
627  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
628  throw new RuntimeException( __METHOD__ . ' can only be used in tests' );
629  }
630  // @codeCoverageIgnoreEnd
631  if ( isset( $this->testAttributes[$name] ) ) {
632  throw new Exception( "The attribute '$name' has already been overridden" );
633  }
634  $this->testAttributes[$name] = $value;
635  return new ScopedCallback( function () use ( $name ) {
636  unset( $this->testAttributes[$name] );
637  } );
638  }
639 
645  public function getAllThings() {
646  return $this->loaded;
647  }
648 
656  protected static function processAutoLoader( $dir, array $files ) {
657  // Make paths absolute, relative to the JSON file
658  foreach ( $files as &$file ) {
659  $file = "$dir/$file";
660  }
661  return $files;
662  }
663 }
ExtensionRegistry\isLoaded
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
Definition: ExtensionRegistry.php:553
ExtensionRegistry\setLoadTestClassesAndNamespaces
setLoadTestClassesAndNamespaces( $load)
Controls if classes and namespaces defined under the keys TestAutoloadClasses and TestAutoloadNamespa...
Definition: ExtensionRegistry.php:159
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:44
ExtensionRegistry\getQueue
getQueue()
Get the current load queue.
Definition: ExtensionRegistry.php:272
ExtensionRegistry\$finished
bool $finished
Whether we are done loading things.
Definition: ExtensionRegistry.php:89
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:166
wfArrayPlus2d
wfArrayPlus2d(array $baseArray, array $newValues)
Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
Definition: GlobalFunctions.php:2890
ExtensionDependencyError
Copyright (C) 2018 Kunal Mehta legoktm@member.fsf.org
Definition: ExtensionDependencyError.php:25
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:423
ObjectCache\makeLocalServerCache
static makeLocalServerCache()
Create a new BagOStuff instance for local-server caching.
Definition: ObjectCache.php:299
ExtensionRegistry\exportExtractedData
exportExtractedData(array $info)
Definition: ExtensionRegistry.php:461
ExtensionRegistry\makeCacheKey
makeCacheKey(BagOStuff $cache, $component,... $extra)
Definition: ExtensionRegistry.php:190
ExtensionRegistry
ExtensionRegistry class.
Definition: ExtensionRegistry.php:18
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:39
ExtensionRegistry\$loadTestClassesAndNamespaces
bool $loadTestClassesAndNamespaces
Whether test classes and namespaces should be added to the auto loader.
Definition: ExtensionRegistry.php:125
ExtensionRegistry\getLazyLoadedAttribute
getLazyLoadedAttribute( $name)
Get an attribute value that isn't cached by reading each extension.json file again.
Definition: ExtensionRegistry.php:590
ExtensionRegistry\getAllThings
getAllThings()
Get credits information about all installed extensions and skins.
Definition: ExtensionRegistry.php:645
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:71
ExtensionRegistry\setCheckDevRequires
setCheckDevRequires( $check)
Definition: ExtensionRegistry.php:148
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:447
ExtensionRegistry\getCache
getCache()
Definition: ExtensionRegistry.php:184
ExtensionRegistry\getAbilities
getAbilities()
Get the list of abilities and their values.
Definition: ExtensionRegistry.php:297
ExtensionRegistry\setAttributeForTest
setAttributeForTest( $name, array $value)
Force override the value of an attribute during tests.
Definition: ExtensionRegistry.php:625
ExtensionRegistry\clearQueue
clearQueue()
Clear the current load queue.
Definition: ExtensionRegistry.php:280
ExtensionRegistry\MERGE_STRATEGY
const MERGE_STRATEGY
Special key that defines the merge strategy.
Definition: ExtensionRegistry.php:54
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:136
ExtensionProcessor
Definition: ExtensionProcessor.php:3
ExtensionRegistry\CACHE_VERSION
const CACHE_VERSION
Bump whenever the registration cache needs resetting.
Definition: ExtensionRegistry.php:45
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1058
MWException
MediaWiki exception.
Definition: MWException.php:29
ExtensionRegistry\$attributes
array $attributes
Items in the JSON file that aren't being set as globals.
Definition: ExtensionRegistry.php:97
ExtensionRegistry\processAutoLoader
static processAutoLoader( $dir, array $files)
Fully expand autoloader paths.
Definition: ExtensionRegistry.php:656
$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:289
ExtensionRegistry\loadFromQueue
loadFromQueue()
Definition: ExtensionRegistry.php:212
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
Definition: ExtensionRegistry.php:23
ExtensionRegistry\readFromQueue
readFromQueue(array $queue)
Process a queue of extensions and return their extracted data.
Definition: ExtensionRegistry.php:332
ExtensionRegistry\$queued
array $queued
List of paths that should be loaded.
Definition: ExtensionRegistry.php:82
ExtensionRegistry\LAZY_LOADED_ATTRIBUTES
const LAZY_LOADED_ATTRIBUTES
Attributes that should be lazy-loaded.
Definition: ExtensionRegistry.php:59
ExtensionRegistry\$testAttributes
array $testAttributes
Attributes for testing.
Definition: ExtensionRegistry.php:104
$wgDevelopmentWarnings
$wgDevelopmentWarnings
If set to true MediaWiki will throw notices for some possible error conditions and for deprecated fun...
Definition: DefaultSettings.php:6782
MediaWiki\ShellDisabledError
@newable
Definition: ShellDisabledError.php:29
ExtensionRegistry\$loaded
array[] $loaded
Array of loaded things, keyed by name, values are credits information.
Definition: ExtensionRegistry.php:75
BagOStuff\makeKey
makeKey( $class,... $components)
Make a cache key, scoped to this instance's keyspace.
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:308
ExtensionRegistry\$lazyAttributes
array $lazyAttributes
Lazy-loaded attributes.
Definition: ExtensionRegistry.php:111
$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:2863
ExtensionRegistry\CACHE_EXPIRY
const CACHE_EXPIRY
Definition: ExtensionRegistry.php:47
ExtensionRegistry\getAttribute
getAttribute( $name)
Definition: ExtensionRegistry.php:572
ExtensionRegistry\$checkDev
bool $checkDev
Whether to check dev-requires.
Definition: ExtensionRegistry.php:118
ExtensionRegistry\$instance
static ExtensionRegistry $instance
Definition: ExtensionRegistry.php:130
$GLOBALS
$GLOBALS['IP']
Definition: ComposerHookHandler.php:6
ExtensionRegistry\saveToCache
saveToCache(BagOStuff $cache, array $data)
Save data in the cache.
Definition: ExtensionRegistry.php:241