MediaWiki  master
ExtensionRegistry.php
Go to the documentation of this file.
1 <?php
2 
3 use Composer\Semver\Semver;
6 use Wikimedia\ScopedCallback;
7 
16 
20  public const MEDIAWIKI_CORE = 'MediaWiki';
21 
26  public const MANIFEST_VERSION = 2;
27 
32  public const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
33 
37  public const OLDEST_MANIFEST_VERSION = 1;
38 
42  private const CACHE_VERSION = 7;
43 
44  private const CACHE_EXPIRY = 60 * 60 * 24;
45 
51  public const MERGE_STRATEGY = '_merge_strategy';
52 
56  private const LAZY_LOADED_ATTRIBUTES = [
57  'TrackingCategories',
58  'QUnitTestModules',
59  'SkinLessImportPaths',
60  ];
61 
73  private $loaded = [];
74 
80  protected $queued = [];
81 
87  private $finished = false;
88 
95  protected $attributes = [];
96 
102  protected $testAttributes = [];
103 
109  protected $lazyAttributes = [];
110 
116  protected $checkDev = false;
117 
123  protected $loadTestClassesAndNamespaces = false;
124 
128  private static $instance;
129 
134  public static function getInstance() {
135  if ( self::$instance === null ) {
136  self::$instance = new self();
137  }
138 
139  return self::$instance;
140  }
141 
146  public function setCheckDevRequires( $check ) {
147  $this->checkDev = $check;
148  }
149 
157  public function setLoadTestClassesAndNamespaces( $load ) {
158  $this->loadTestClassesAndNamespaces = $load;
159  }
160 
164  public function queue( $path ) {
165  global $wgExtensionInfoMTime;
166 
167  $mtime = $wgExtensionInfoMTime;
168  if ( $mtime === false ) {
169  // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
170  $mtime = @filemtime( $path );
171  // @codeCoverageIgnoreStart
172  if ( $mtime === false ) {
173  $err = error_get_last();
174  throw new Exception( "Unable to open file $path: {$err['message']}" );
175  // @codeCoverageIgnoreEnd
176  }
177  }
178  $this->queued[$path] = $mtime;
179  }
180 
181  private function getCache(): BagOStuff {
182  // Can't call MediaWikiServices here, as we must not cause services
183  // to be instantiated before extensions have loaded.
185  }
186 
187  private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) {
188  // Everything we vary the cache on
189  $vary = [
190  'registration' => self::CACHE_VERSION,
191  'mediawiki' => MW_VERSION,
192  'abilities' => $this->getAbilities(),
193  'checkDev' => $this->checkDev,
194  'queue' => $this->queued,
195  ];
196 
197  // Allow reusing cached ExtensionRegistry metadata between wikis (T274648)
198  return $cache->makeGlobalKey(
199  "registration-$component",
200  // We vary the cache on the current queue (what will be or already was loaded)
201  // plus various versions of stuff for VersionChecker
202  md5( json_encode( $vary ) ),
203  ...$extra
204  );
205  }
206 
211  public function loadFromQueue() {
212  if ( !$this->queued ) {
213  return;
214  }
215 
216  if ( $this->finished ) {
217  throw new MWException(
218  "The following paths tried to load late: "
219  . implode( ', ', array_keys( $this->queued ) )
220  );
221  }
222 
223  $cache = $this->getCache();
224  // See if this queue is in APC
225  $key = $this->makeCacheKey( $cache, 'main' );
226  $data = $cache->get( $key );
227  if ( !$data ) {
228  $data = $this->readFromQueue( $this->queued );
229  $this->saveToCache( $cache, $data );
230  }
231  $this->exportExtractedData( $data );
232  }
233 
240  protected function saveToCache( BagOStuff $cache, array $data ) {
241  global $wgDevelopmentWarnings;
242  if ( $data['warnings'] && $wgDevelopmentWarnings ) {
243  // If warnings were shown, don't cache it
244  return;
245  }
246  $lazy = [];
247  // Cache lazy-loaded attributes separately
248  foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
249  if ( isset( $data['attributes'][$attrib] ) ) {
250  $lazy[$attrib] = $data['attributes'][$attrib];
251  unset( $data['attributes'][$attrib] );
252  }
253  }
254  $mainKey = $this->makeCacheKey( $cache, 'main' );
255  $cache->set( $mainKey, $data, self::CACHE_EXPIRY );
256  foreach ( $lazy as $attrib => $value ) {
257  $cache->set(
258  $this->makeCacheKey( $cache, 'lazy-attrib', $attrib ),
259  $value,
260  self::CACHE_EXPIRY
261  );
262  }
263  }
264 
271  public function getQueue() {
272  return $this->queued;
273  }
274 
279  public function clearQueue() {
280  $this->queued = [];
281  }
282 
288  public function finish() {
289  $this->finished = true;
290  }
291 
296  private function getAbilities() {
297  return [
298  'shell' => !Shell::isDisabled(),
299  ];
300  }
301 
307  private function buildVersionChecker() {
308  // array to optionally specify more verbose error messages for
309  // missing abilities
310  $abilityErrors = [
311  'shell' => ( new ShellDisabledError() )->getMessage(),
312  ];
313 
314  return new VersionChecker(
315  MW_VERSION,
316  PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
317  get_loaded_extensions(),
318  $this->getAbilities(),
319  $abilityErrors
320  );
321  }
322 
331  public function readFromQueue( array $queue ) {
332  $autoloadClasses = [];
333  $autoloadNamespaces = [];
334  $autoloaderPaths = [];
335  $processor = new ExtensionProcessor();
336  $versionChecker = $this->buildVersionChecker();
337  $extDependencies = [];
338  $warnings = false;
339  foreach ( $queue as $path => $mtime ) {
340  $json = file_get_contents( $path );
341  if ( $json === false ) {
342  throw new Exception( "Unable to read $path, does it exist?" );
343  }
344  $info = json_decode( $json, /* $assoc = */ true );
345  if ( !is_array( $info ) ) {
346  throw new Exception( "$path is not a valid JSON file." );
347  }
348 
349  if ( !isset( $info['manifest_version'] ) ) {
351  "{$info['name']}'s extension.json or skin.json does not have manifest_version, " .
352  'this is deprecated since MediaWiki 1.29',
353  '1.29', false, false
354  );
355  $warnings = true;
356  // For backwards-compatibility, assume a version of 1
357  $info['manifest_version'] = 1;
358  }
359  $version = $info['manifest_version'];
360  if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
361  throw new Exception( "$path: unsupported manifest_version: {$version}" );
362  }
363 
364  $dir = dirname( $path );
366  $dir,
367  $info,
368  $autoloadClasses,
369  $autoloadNamespaces
370  );
371 
372  if ( $this->loadTestClassesAndNamespaces ) {
374  $dir,
375  $info,
376  $autoloadClasses,
377  $autoloadNamespaces
378  );
379  }
380 
381  // get all requirements/dependencies for this extension
382  $requires = $processor->getRequirements( $info, $this->checkDev );
383 
384  // validate the information needed and add the requirements
385  if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
386  $extDependencies[$info['name']] = $requires;
387  }
388 
389  // Get extra paths for later inclusion
390  $autoloaderPaths = array_merge( $autoloaderPaths,
391  $processor->getExtraAutoloaderPaths( $dir, $info ) );
392  // Compatible, read and extract info
393  $processor->extractInfo( $path, $info, $version );
394  }
395  $data = $processor->getExtractedInfo();
396  $data['warnings'] = $warnings;
397 
398  // check for incompatible extensions
399  $incompatible = $versionChecker
400  ->setLoadedExtensionsAndSkins( $data['credits'] )
401  ->checkArray( $extDependencies );
402 
403  if ( $incompatible ) {
404  throw new ExtensionDependencyError( $incompatible );
405  }
406 
407  // FIXME: It was a design mistake to handle autoloading separately (T240535)
408  $data['globals']['wgAutoloadClasses'] = $autoloadClasses;
409  $data['autoloaderPaths'] = $autoloaderPaths;
410  $data['autoloaderNS'] = $autoloadNamespaces;
411  return $data;
412  }
413 
422  public static function exportAutoloadClassesAndNamespaces(
423  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
424  ) {
425  if ( isset( $info['AutoloadClasses'] ) ) {
426  $autoload = self::processAutoLoader( $dir, $info['AutoloadClasses'] );
427  $GLOBALS['wgAutoloadClasses'] += $autoload;
428  $autoloadClasses += $autoload;
429  }
430  if ( isset( $info['AutoloadNamespaces'] ) ) {
431  $autoloadNamespaces += self::processAutoLoader( $dir, $info['AutoloadNamespaces'] );
432  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
433  }
434  }
435 
446  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
447  ) {
448  if ( isset( $info['TestAutoloadClasses'] ) ) {
449  $autoload = self::processAutoLoader( $dir, $info['TestAutoloadClasses'] );
450  $GLOBALS['wgAutoloadClasses'] += $autoload;
451  $autoloadClasses += $autoload;
452  }
453  if ( isset( $info['TestAutoloadNamespaces'] ) ) {
454  $autoloadNamespaces += self::processAutoLoader( $dir, $info['TestAutoloadNamespaces'] );
455  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
456  }
457  }
458 
459  protected function exportExtractedData( array $info ) {
460  foreach ( $info['globals'] as $key => $val ) {
461  // If a merge strategy is set, read it and remove it from the value
462  // so it doesn't accidentally end up getting set.
463  if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
464  $mergeStrategy = $val[self::MERGE_STRATEGY];
465  unset( $val[self::MERGE_STRATEGY] );
466  } else {
467  $mergeStrategy = 'array_merge';
468  }
469 
470  if ( $mergeStrategy === 'provide_default' ) {
471  if ( !array_key_exists( $key, $GLOBALS ) ) {
472  $GLOBALS[$key] = $val;
473  }
474  continue;
475  }
476 
477  // Optimistic: If the global is not set, or is an empty array, replace it entirely.
478  // Will be O(1) performance.
479  if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
480  $GLOBALS[$key] = $val;
481  continue;
482  }
483 
484  if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
485  // config setting that has already been overridden, don't set it
486  continue;
487  }
488 
489  switch ( $mergeStrategy ) {
490  case 'array_merge_recursive':
491  $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
492  break;
493  case 'array_replace_recursive':
494  $GLOBALS[$key] = array_replace_recursive( $val, $GLOBALS[$key] );
495  break;
496  case 'array_plus_2d':
497  $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
498  break;
499  case 'array_plus':
500  $GLOBALS[$key] += $val;
501  break;
502  case 'array_merge':
503  $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
504  break;
505  default:
506  throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
507  }
508  }
509 
510  if ( isset( $info['autoloaderNS'] ) ) {
511  AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
512  }
513 
514  foreach ( $info['defines'] as $name => $val ) {
515  if ( !defined( $name ) ) {
516  define( $name, $val );
517  } elseif ( constant( $name ) !== $val ) {
518  throw new UnexpectedValueException(
519  "$name cannot be re-defined with $val it has already been set with " . constant( $name )
520  );
521  }
522  }
523 
524  foreach ( $info['autoloaderPaths'] as $path ) {
525  if ( file_exists( $path ) ) {
526  require_once $path;
527  }
528  }
529 
530  $this->loaded += $info['credits'];
531  if ( $info['attributes'] ) {
532  if ( !$this->attributes ) {
533  $this->attributes = $info['attributes'];
534  } else {
535  $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
536  }
537  }
538 
539  foreach ( $info['callbacks'] as $name => $cb ) {
540  if ( !is_callable( $cb ) ) {
541  if ( is_array( $cb ) ) {
542  $cb = '[ ' . implode( ', ', $cb ) . ' ]';
543  }
544  throw new UnexpectedValueException( "callback '$cb' is not callable" );
545  }
546  $cb( $info['credits'][$name] );
547  }
548  }
549 
558  public function isLoaded( $name, $constraint = '*' ) {
559  $isLoaded = isset( $this->loaded[$name] );
560  if ( $constraint === '*' || !$isLoaded ) {
561  return $isLoaded;
562  }
563  // if a specific constraint is requested, but no version is set, throw an exception
564  if ( !isset( $this->loaded[$name]['version'] ) ) {
565  $msg = "{$name} does not expose its version, but an extension or a skin"
566  . " requires: {$constraint}.";
567  throw new LogicException( $msg );
568  }
569 
570  return Semver::satisfies( $this->loaded[$name]['version'], $constraint );
571  }
572 
577  public function getAttribute( $name ) {
578  if ( isset( $this->testAttributes[$name] ) ) {
579  return $this->testAttributes[$name];
580  }
581 
582  if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES, true ) ) {
583  return $this->getLazyLoadedAttribute( $name );
584  }
585 
586  return $this->attributes[$name] ?? [];
587  }
588 
595  protected function getLazyLoadedAttribute( $name ) {
596  if ( isset( $this->testAttributes[$name] ) ) {
597  return $this->testAttributes[$name];
598  }
599 
600  // See if it's in the cache
601  $cache = $this->getCache();
602  $key = $this->makeCacheKey( $cache, 'lazy-attrib', $name );
603  $data = $cache->get( $key );
604  if ( $data !== false ) {
605  return $data;
606  }
607 
608  $paths = [];
609  foreach ( $this->loaded as $info ) {
610  // mtime (array value) doesn't matter here since
611  // we're skipping cache, so use a dummy time
612  $paths[$info['path']] = 1;
613  }
614 
615  $result = $this->readFromQueue( $paths );
616  $data = $result['attributes'][$name] ?? [];
617  $this->saveToCache( $cache, $result );
618 
619  return $data;
620  }
621 
630  public function setAttributeForTest( $name, array $value ) {
631  // @codeCoverageIgnoreStart
632  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
633  throw new RuntimeException( __METHOD__ . ' can only be used in tests' );
634  }
635  // @codeCoverageIgnoreEnd
636  if ( isset( $this->testAttributes[$name] ) ) {
637  throw new Exception( "The attribute '$name' has already been overridden" );
638  }
639  $this->testAttributes[$name] = $value;
640  return new ScopedCallback( function () use ( $name ) {
641  unset( $this->testAttributes[$name] );
642  } );
643  }
644 
650  public function getAllThings() {
651  return $this->loaded;
652  }
653 
661  protected static function processAutoLoader( $dir, array $files ) {
662  // Make paths absolute, relative to the JSON file
663  foreach ( $files as &$file ) {
664  $file = "$dir/$file";
665  }
666  return $files;
667  }
668 }
ExtensionRegistry\isLoaded
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
Definition: ExtensionRegistry.php:558
ExtensionRegistry\setLoadTestClassesAndNamespaces
setLoadTestClassesAndNamespaces( $load)
Controls if classes and namespaces defined under the keys TestAutoloadClasses and TestAutoloadNamespa...
Definition: ExtensionRegistry.php:157
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:45
ExtensionRegistry\getQueue
getQueue()
Get the current load queue.
Definition: ExtensionRegistry.php:271
ExtensionRegistry\$finished
bool $finished
Whether we are done loading things.
Definition: ExtensionRegistry.php:87
ExtensionRegistry\MANIFEST_VERSION
const MANIFEST_VERSION
Version of the highest supported manifest version Note: Update MANIFEST_VERSION_MW_VERSION when chang...
Definition: ExtensionRegistry.php:26
ExtensionRegistry\queue
queue( $path)
Definition: ExtensionRegistry.php:164
wfArrayPlus2d
wfArrayPlus2d(array $baseArray, array $newValues)
Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
Definition: GlobalFunctions.php:2627
ExtensionDependencyError
Copyright (C) 2018 Kunal Mehta legoktm@debian.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:422
ObjectCache\makeLocalServerCache
static makeLocalServerCache()
Create a new BagOStuff instance for local-server caching.
Definition: ObjectCache.php:300
ExtensionRegistry\exportExtractedData
exportExtractedData(array $info)
Definition: ExtensionRegistry.php:459
ExtensionRegistry\makeCacheKey
makeCacheKey(BagOStuff $cache, $component,... $extra)
Definition: ExtensionRegistry.php:187
ExtensionRegistry
The Registry loads JSON files, and uses a Processor to extract information from them.
Definition: ExtensionRegistry.php:15
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:36
ExtensionRegistry\$loadTestClassesAndNamespaces
bool $loadTestClassesAndNamespaces
Whether test classes and namespaces should be added to the auto loader.
Definition: ExtensionRegistry.php:123
ExtensionRegistry\getLazyLoadedAttribute
getLazyLoadedAttribute( $name)
Get an attribute value that isn't cached by reading each extension.json file again.
Definition: ExtensionRegistry.php:595
ExtensionRegistry\getAllThings
getAllThings()
Get credits information about all installed extensions and skins.
Definition: ExtensionRegistry.php:650
$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:86
ExtensionRegistry\setCheckDevRequires
setCheckDevRequires( $check)
Definition: ExtensionRegistry.php:146
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:445
ExtensionRegistry\getCache
getCache()
Definition: ExtensionRegistry.php:181
ExtensionRegistry\getAbilities
getAbilities()
Get the list of abilities and their values.
Definition: ExtensionRegistry.php:296
ExtensionRegistry\setAttributeForTest
setAttributeForTest( $name, array $value)
Force override the value of an attribute during tests.
Definition: ExtensionRegistry.php:630
ExtensionRegistry\clearQueue
clearQueue()
Clear the current load queue.
Definition: ExtensionRegistry.php:279
ExtensionRegistry\MERGE_STRATEGY
const MERGE_STRATEGY
Special key that defines the merge strategy.
Definition: ExtensionRegistry.php:51
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:134
ExtensionProcessor
Definition: ExtensionProcessor.php:3
ExtensionRegistry\CACHE_VERSION
const CACHE_VERSION
Bump whenever the registration cache needs resetting.
Definition: ExtensionRegistry.php:42
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1028
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:95
ExtensionRegistry\processAutoLoader
static processAutoLoader( $dir, array $files)
Fully expand autoloader paths.
Definition: ExtensionRegistry.php:661
$queue
$queue
Definition: mergeMessageFileList.php:176
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:32
AutoLoader\$psr4Namespaces
static string[] $psr4Namespaces
Definition: AutoLoader.php:38
ExtensionRegistry\finish
finish()
After this is called, no more extensions can be loaded.
Definition: ExtensionRegistry.php:288
ExtensionRegistry\loadFromQueue
loadFromQueue()
Definition: ExtensionRegistry.php:211
ExtensionRegistry\$queued
int[] $queued
List of paths that should be loaded.
Definition: ExtensionRegistry.php:80
ExtensionRegistry\OLDEST_MANIFEST_VERSION
const OLDEST_MANIFEST_VERSION
Version of the oldest supported manifest version.
Definition: ExtensionRegistry.php:37
ExtensionRegistry\MEDIAWIKI_CORE
const MEDIAWIKI_CORE
"requires" key that applies to MediaWiki core
Definition: ExtensionRegistry.php:20
ExtensionRegistry\readFromQueue
readFromQueue(array $queue)
Process a queue of extensions and return their extracted data.
Definition: ExtensionRegistry.php:331
ExtensionRegistry\LAZY_LOADED_ATTRIBUTES
const LAZY_LOADED_ATTRIBUTES
Attributes that should be lazy-loaded.
Definition: ExtensionRegistry.php:56
ExtensionRegistry\$testAttributes
array $testAttributes
Attributes for testing.
Definition: ExtensionRegistry.php:102
$wgDevelopmentWarnings
$wgDevelopmentWarnings
If set to true MediaWiki will throw notices for some possible error conditions and for deprecated fun...
Definition: DefaultSettings.php:7448
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:73
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:307
ExtensionRegistry\$lazyAttributes
array $lazyAttributes
Lazy-loaded attributes.
Definition: ExtensionRegistry.php:109
$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:3190
ExtensionRegistry\CACHE_EXPIRY
const CACHE_EXPIRY
Definition: ExtensionRegistry.php:44
ExtensionRegistry\getAttribute
getAttribute( $name)
Definition: ExtensionRegistry.php:577
ExtensionRegistry\$checkDev
bool $checkDev
Whether to check dev-requires.
Definition: ExtensionRegistry.php:116
BagOStuff\makeGlobalKey
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.
ExtensionRegistry\$instance
static ExtensionRegistry $instance
Definition: ExtensionRegistry.php:128
ExtensionRegistry\saveToCache
saveToCache(BagOStuff $cache, array $data)
Save data in the cache.
Definition: ExtensionRegistry.php:240