MediaWiki  1.34.0
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  const MEDIAWIKI_CORE = 'MediaWiki';
24 
29  const MANIFEST_VERSION = 2;
30 
35  const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
36 
41 
45  const CACHE_VERSION = 7;
46 
52  const MERGE_STRATEGY = '_merge_strategy';
53 
59  private $loaded = [];
60 
66  protected $queued = [];
67 
73  private $finished = false;
74 
81  protected $attributes = [];
82 
88  protected $testAttributes = [];
89 
95  protected $checkDev = false;
96 
100  private static $instance;
101 
106  public static function getInstance() {
107  if ( self::$instance === null ) {
108  self::$instance = new self();
109  }
110 
111  return self::$instance;
112  }
113 
118  public function setCheckDevRequires( $check ) {
119  $this->checkDev = $check;
120  }
121 
125  public function queue( $path ) {
126  global $wgExtensionInfoMTime;
127 
128  $mtime = $wgExtensionInfoMTime;
129  if ( $mtime === false ) {
130  AtEase::suppressWarnings();
131  $mtime = filemtime( $path );
132  AtEase::restoreWarnings();
133  // @codeCoverageIgnoreStart
134  if ( $mtime === false ) {
135  $err = error_get_last();
136  throw new Exception( "Unable to open file $path: {$err['message']}" );
137  // @codeCoverageIgnoreEnd
138  }
139  }
140  $this->queued[$path] = $mtime;
141  }
142 
147  public function loadFromQueue() {
149  if ( !$this->queued ) {
150  return;
151  }
152 
153  if ( $this->finished ) {
154  throw new MWException(
155  "The following paths tried to load late: "
156  . implode( ', ', array_keys( $this->queued ) )
157  );
158  }
159 
160  // A few more things to vary the cache on
161  $versions = [
162  'registration' => self::CACHE_VERSION,
163  'mediawiki' => $wgVersion,
164  'abilities' => $this->getAbilities(),
165  'checkDev' => $this->checkDev,
166  ];
167 
168  // We use a try/catch because we don't want to fail here
169  // if $wgObjectCaches is not configured properly for APC setup
170  try {
171  // Avoid MediaWikiServices to prevent instantiating it before extensions have loaded
174  } catch ( InvalidArgumentException $e ) {
175  $cache = new EmptyBagOStuff();
176  }
177  // See if this queue is in APC
178  $key = $cache->makeKey(
179  'registration',
180  md5( json_encode( $this->queued + $versions ) )
181  );
182  $data = $cache->get( $key );
183  if ( $data ) {
184  $this->exportExtractedData( $data );
185  } else {
186  $data = $this->readFromQueue( $this->queued );
187  $this->exportExtractedData( $data );
188  // Do this late since we don't want to extract it since we already
189  // did that, but it should be cached
190  $data['globals']['wgAutoloadClasses'] += $data['autoload'];
191  unset( $data['autoload'] );
192  if ( !( $data['warnings'] && $wgDevelopmentWarnings ) ) {
193  // If there were no warnings that were shown, cache it
194  $cache->set( $key, $data, 60 * 60 * 24 );
195  }
196  }
197  $this->queued = [];
198  }
199 
206  public function getQueue() {
207  return $this->queued;
208  }
209 
214  public function clearQueue() {
215  $this->queued = [];
216  }
217 
223  public function finish() {
224  $this->finished = true;
225  }
226 
231  private function getAbilities() {
232  return [
233  'shell' => !Shell::isDisabled(),
234  ];
235  }
236 
242  private function buildVersionChecker() {
243  global $wgVersion;
244  // array to optionally specify more verbose error messages for
245  // missing abilities
246  $abilityErrors = [
247  'shell' => ( new ShellDisabledError() )->getMessage(),
248  ];
249 
250  return new VersionChecker(
251  $wgVersion,
252  PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
253  get_loaded_extensions(),
254  $this->getAbilities(),
255  $abilityErrors
256  );
257  }
258 
267  public function readFromQueue( array $queue ) {
268  $autoloadClasses = [];
269  $autoloadNamespaces = [];
270  $autoloaderPaths = [];
271  $processor = new ExtensionProcessor();
272  $versionChecker = $this->buildVersionChecker();
273  $extDependencies = [];
274  $incompatible = [];
275  $warnings = false;
276  foreach ( $queue as $path => $mtime ) {
277  $json = file_get_contents( $path );
278  if ( $json === false ) {
279  throw new Exception( "Unable to read $path, does it exist?" );
280  }
281  $info = json_decode( $json, /* $assoc = */ true );
282  if ( !is_array( $info ) ) {
283  throw new Exception( "$path is not a valid JSON file." );
284  }
285 
286  if ( !isset( $info['manifest_version'] ) ) {
287  wfDeprecated(
288  "{$info['name']}'s extension.json or skin.json does not have manifest_version",
289  '1.29'
290  );
291  $warnings = true;
292  // For backwards-compatability, assume a version of 1
293  $info['manifest_version'] = 1;
294  }
295  $version = $info['manifest_version'];
296  if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
297  $incompatible[] = "$path: unsupported manifest_version: {$version}";
298  }
299 
300  $dir = dirname( $path );
302  $dir,
303  $info,
304  $autoloadClasses,
305  $autoloadNamespaces
306  );
307 
308  // get all requirements/dependencies for this extension
309  $requires = $processor->getRequirements( $info, $this->checkDev );
310 
311  // validate the information needed and add the requirements
312  if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
313  $extDependencies[$info['name']] = $requires;
314  }
315 
316  // Get extra paths for later inclusion
317  $autoloaderPaths = array_merge( $autoloaderPaths,
318  $processor->getExtraAutoloaderPaths( $dir, $info ) );
319  // Compatible, read and extract info
320  $processor->extractInfo( $path, $info, $version );
321  }
322  $data = $processor->getExtractedInfo();
323  $data['warnings'] = $warnings;
324 
325  // check for incompatible extensions
326  $incompatible = array_merge(
327  $incompatible,
328  $versionChecker
329  ->setLoadedExtensionsAndSkins( $data['credits'] )
330  ->checkArray( $extDependencies )
331  );
332 
333  if ( $incompatible ) {
334  throw new ExtensionDependencyError( $incompatible );
335  }
336 
337  // Need to set this so we can += to it later
338  $data['globals']['wgAutoloadClasses'] = [];
339  $data['autoload'] = $autoloadClasses;
340  $data['autoloaderPaths'] = $autoloaderPaths;
341  $data['autoloaderNS'] = $autoloadNamespaces;
342  return $data;
343  }
344 
353  public static function exportAutoloadClassesAndNamespaces(
354  $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
355  ) {
356  if ( isset( $info['AutoloadClasses'] ) ) {
357  $autoload = self::processAutoLoader( $dir, $info['AutoloadClasses'] );
358  $GLOBALS['wgAutoloadClasses'] += $autoload;
359  $autoloadClasses += $autoload;
360  }
361  if ( isset( $info['AutoloadNamespaces'] ) ) {
362  $autoloadNamespaces += self::processAutoLoader( $dir, $info['AutoloadNamespaces'] );
363  AutoLoader::$psr4Namespaces += $autoloadNamespaces;
364  }
365  }
366 
367  protected function exportExtractedData( array $info ) {
368  foreach ( $info['globals'] as $key => $val ) {
369  // If a merge strategy is set, read it and remove it from the value
370  // so it doesn't accidentally end up getting set.
371  if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
372  $mergeStrategy = $val[self::MERGE_STRATEGY];
373  unset( $val[self::MERGE_STRATEGY] );
374  } else {
375  $mergeStrategy = 'array_merge';
376  }
377 
378  // Optimistic: If the global is not set, or is an empty array, replace it entirely.
379  // Will be O(1) performance.
380  if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
381  $GLOBALS[$key] = $val;
382  continue;
383  }
384 
385  if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
386  // config setting that has already been overridden, don't set it
387  continue;
388  }
389 
390  switch ( $mergeStrategy ) {
391  case 'array_merge_recursive':
392  $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
393  break;
394  case 'array_replace_recursive':
395  $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
396  break;
397  case 'array_plus_2d':
398  $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
399  break;
400  case 'array_plus':
401  $GLOBALS[$key] += $val;
402  break;
403  case 'array_merge':
404  $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
405  break;
406  default:
407  throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
408  }
409  }
410 
411  if ( isset( $info['autoloaderNS'] ) ) {
412  AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
413  }
414 
415  foreach ( $info['defines'] as $name => $val ) {
416  define( $name, $val );
417  }
418  foreach ( $info['autoloaderPaths'] as $path ) {
419  if ( file_exists( $path ) ) {
420  require_once $path;
421  }
422  }
423 
424  $this->loaded += $info['credits'];
425  if ( $info['attributes'] ) {
426  if ( !$this->attributes ) {
427  $this->attributes = $info['attributes'];
428  } else {
429  $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
430  }
431  }
432 
433  foreach ( $info['callbacks'] as $name => $cb ) {
434  if ( !is_callable( $cb ) ) {
435  if ( is_array( $cb ) ) {
436  $cb = '[ ' . implode( ', ', $cb ) . ' ]';
437  }
438  throw new UnexpectedValueException( "callback '$cb' is not callable" );
439  }
440  $cb( $info['credits'][$name] );
441  }
442  }
443 
453  public function load( $path ) {
454  wfDeprecated( __METHOD__, '1.34' );
455  $this->loadFromQueue(); // First clear the queue
456  $this->queue( $path );
457  $this->loadFromQueue();
458  }
459 
468  public function isLoaded( $name, $constraint = '*' ) {
469  $isLoaded = isset( $this->loaded[$name] );
470  if ( $constraint === '*' || !$isLoaded ) {
471  return $isLoaded;
472  }
473  // if a specific constraint is requested, but no version is set, throw an exception
474  if ( !isset( $this->loaded[$name]['version'] ) ) {
475  $msg = "{$name} does not expose its version, but an extension or a skin"
476  . " requires: {$constraint}.";
477  throw new LogicException( $msg );
478  }
479 
480  return SemVer::satisfies( $this->loaded[$name]['version'], $constraint );
481  }
482 
487  public function getAttribute( $name ) {
488  return $this->testAttributes[$name] ??
489  $this->attributes[$name] ?? [];
490  }
491 
500  public function setAttributeForTest( $name, array $value ) {
501  // @codeCoverageIgnoreStart
502  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
503  throw new RuntimeException( __METHOD__ . ' can only be used in tests' );
504  }
505  // @codeCoverageIgnoreEnd
506  if ( isset( $this->testAttributes[$name] ) ) {
507  throw new Exception( "The attribute '$name' has already been overridden" );
508  }
509  $this->testAttributes[$name] = $value;
510  return new ScopedCallback( function () use ( $name ) {
511  unset( $this->testAttributes[$name] );
512  } );
513  }
514 
520  public function getAllThings() {
521  return $this->loaded;
522  }
523 
531  protected static function processAutoLoader( $dir, array $files ) {
532  // Make paths absolute, relative to the JSON file
533  foreach ( $files as &$file ) {
534  $file = "$dir/$file";
535  }
536  return $files;
537  }
538 }
ExtensionRegistry\isLoaded
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
Definition: ExtensionRegistry.php:468
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:44
ExtensionRegistry\getQueue
getQueue()
Get the current load queue.
Definition: ExtensionRegistry.php:206
ExtensionRegistry\$finished
bool $finished
Whether we are done loading things.
Definition: ExtensionRegistry.php:73
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:125
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:3066
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:353
ObjectCache\detectLocalServerCache
static detectLocalServerCache()
Detects which local server cache library is present and returns a configuration for it.
Definition: ObjectCache.php:362
ExtensionRegistry\exportExtractedData
exportExtractedData(array $info)
Definition: ExtensionRegistry.php:367
ExtensionRegistry
ExtensionRegistry class.
Definition: ExtensionRegistry.php:18
$wgVersion
$wgVersion
MediaWiki version number.
Definition: DefaultSettings.php:75
ExtensionRegistry\getAllThings
getAllThings()
Get information about all things.
Definition: ExtensionRegistry.php:520
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ExtensionRegistry\$loaded
array $loaded
Array of loaded things, keyed by name, values are credits information.
Definition: ExtensionRegistry.php:59
ExtensionRegistry\setCheckDevRequires
setCheckDevRequires( $check)
Definition: ExtensionRegistry.php:118
ExtensionRegistry\getAbilities
getAbilities()
Get the list of abilities and their values.
Definition: ExtensionRegistry.php:231
ExtensionRegistry\setAttributeForTest
setAttributeForTest( $name, array $value)
Force override the value of an attribute during tests.
Definition: ExtensionRegistry.php:500
ExtensionRegistry\clearQueue
clearQueue()
Clear the current load queue.
Definition: ExtensionRegistry.php:214
ExtensionRegistry\MERGE_STRATEGY
const MERGE_STRATEGY
Special key that defines the merge strategy.
Definition: ExtensionRegistry.php:52
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:106
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:81
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:531
ExtensionRegistry\load
load( $path)
Loads and processes the given JSON file without delay.
Definition: ExtensionRegistry.php:453
$wgObjectCaches
$wgObjectCaches
Advanced object cache configuration.
Definition: DefaultSettings.php:2393
$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:223
ExtensionRegistry\loadFromQueue
loadFromQueue()
Definition: ExtensionRegistry.php:147
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:267
ExtensionRegistry\$queued
array $queued
List of paths that should be loaded.
Definition: ExtensionRegistry.php:66
ExtensionRegistry\$testAttributes
array $testAttributes
Attributes for testing.
Definition: ExtensionRegistry.php:88
$wgDevelopmentWarnings
$wgDevelopmentWarnings
If set to true MediaWiki will throw notices for some possible error conditions and for deprecated fun...
Definition: DefaultSettings.php:6351
MediaWiki\ShellDisabledError
Definition: ShellDisabledError.php:28
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:242
$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:2726
ObjectCache\newFromParams
static newFromParams( $params)
Create a new cache object from parameters.
Definition: ObjectCache.php:161
ExtensionRegistry\getAttribute
getAttribute( $name)
Definition: ExtensionRegistry.php:487
ExtensionRegistry\$checkDev
bool $checkDev
Whether to check dev-requires.
Definition: ExtensionRegistry.php:95
ExtensionRegistry\$instance
static ExtensionRegistry $instance
Definition: ExtensionRegistry.php:100
$GLOBALS
$GLOBALS['IP']
Definition: ComposerHookHandler.php:6