3use Composer\Semver\Semver;
8use Wikimedia\ScopedCallback;
51 private const CACHE_VERSION = 8;
53 private const CACHE_EXPIRY = 60 * 60 * 24;
65 private const LAZY_LOADED_ATTRIBUTES = [
68 'SkinLessImportPaths',
96 private $finished =
false;
144 private static $instance;
149 private $cache =
null;
156 private static bool $accessDisabledForUnitTests =
false;
163 if ( self::$accessDisabledForUnitTests ) {
164 throw new RuntimeException(
'Access is disabled in unit tests' );
166 if ( self::$instance ===
null ) {
167 self::$instance =
new self();
170 return self::$instance;
177 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
178 throw new RuntimeException(
'Can only be called in tests' );
180 self::$accessDisabledForUnitTests =
true;
187 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
188 throw new RuntimeException(
'Can only be called in tests' );
190 self::$accessDisabledForUnitTests =
false;
202 $this->cache = $cache;
210 $this->checkDev = $check;
211 $this->invalidateProcessCache();
222 $this->loadTestClassesAndNamespaces = $load;
232 if ( $mtime ===
false ) {
234 $mtime = @filemtime(
$path );
236 if ( $mtime ===
false ) {
237 $err = error_get_last();
242 $this->queued[
$path] = $mtime;
243 $this->invalidateProcessCache();
247 if ( !$this->cache ) {
255 :
WikiMap::getCurrentWikiDbDomain()->getId();
256 return ObjectCache::makeLocalServerCache( $keyspace );
262 private function makeCacheKey(
BagOStuff $cache, $component, ...$extra ) {
265 "registration-$component",
266 $this->getVaryHash(),
276 private function getVaryHash() {
277 if ( $this->varyHash ===
null ) {
281 'registration' => self::CACHE_VERSION,
283 'abilities' => $this->getAbilities(),
284 'checkDev' => $this->checkDev,
285 'queue' => $this->queued,
287 $this->varyHash = md5( json_encode( $vary ) );
289 return $this->varyHash;
295 private function invalidateProcessCache() {
296 $this->varyHash =
null;
297 $this->lazyAttributes = [];
301 if ( !$this->queued ) {
305 if ( $this->finished ) {
306 throw new LogicException(
307 "The following paths tried to load late: "
308 . implode(
', ', array_keys( $this->queued ) )
312 $cache = $this->getCache();
314 $key = $this->makeCacheKey( $cache,
'main' );
315 $data = $cache->
get( $key );
317 $data = $this->readFromQueue( $this->queued );
318 $this->saveToCache( $cache, $data );
320 $this->exportExtractedData( $data );
337 foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
338 if ( isset( $data[
'attributes'][$attrib] ) ) {
339 $lazy[$attrib] = $data[
'attributes'][$attrib];
340 unset( $data[
'attributes'][$attrib] );
343 $mainKey = $this->makeCacheKey( $cache,
'main' );
344 $cache->
set( $mainKey, $data, self::CACHE_EXPIRY );
345 foreach ( $lazy as $attrib => $value ) {
347 $this->makeCacheKey( $cache,
'lazy-attrib', $attrib ),
361 return $this->queued;
370 $this->invalidateProcessCache();
379 $this->finished =
true;
386 private function getAbilities() {
388 'shell' => !Shell::isDisabled(),
397 private function buildVersionChecker() {
406 PHP_MAJOR_VERSION .
'.' . PHP_MINOR_VERSION .
'.' . PHP_RELEASE_VERSION,
407 get_loaded_extensions(),
408 $this->getAbilities(),
425 $versionChecker = $this->buildVersionChecker();
426 $extDependencies = [];
428 foreach ( $queue as
$path => $mtime ) {
429 $json = file_get_contents(
$path );
430 if ( $json ===
false ) {
431 throw new InvalidArgumentException(
"Unable to read $path, does it exist?" );
433 $info = json_decode( $json,
true );
434 if ( !is_array( $info ) ) {
435 throw new InvalidArgumentException(
"$path is not a valid JSON file." );
438 $version = $info[
'manifest_version'];
439 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
440 throw new InvalidArgumentException(
"$path: unsupported manifest_version: {$version}" );
444 $requires = $processor->getRequirements( $info, $this->checkDev );
447 if ( is_array( $requires ) && $requires && isset( $info[
'name'] ) ) {
448 $extDependencies[$info[
'name']] = $requires;
452 $processor->extractInfo(
$path, $info, $version );
454 $data = $processor->getExtractedInfo( $this->loadTestClassesAndNamespaces );
455 $data[
'warnings'] = $warnings;
458 $incompatible = $versionChecker
459 ->setLoadedExtensionsAndSkins( $data[
'credits'] )
460 ->checkArray( $extDependencies );
462 if ( $incompatible ) {
470 foreach ( $info[
'globals'] as $key => $val ) {
473 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
474 $mergeStrategy = $val[self::MERGE_STRATEGY];
475 unset( $val[self::MERGE_STRATEGY] );
477 $mergeStrategy =
'array_merge';
480 if ( $mergeStrategy ===
'provide_default' ) {
481 if ( !array_key_exists( $key, $GLOBALS ) ) {
482 $GLOBALS[$key] = $val;
489 if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
490 $GLOBALS[$key] = $val;
494 if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
499 switch ( $mergeStrategy ) {
500 case 'array_merge_recursive':
501 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
503 case 'array_replace_recursive':
504 $GLOBALS[$key] = array_replace_recursive( $val, $GLOBALS[$key] );
506 case 'array_plus_2d':
510 $GLOBALS[$key] += $val;
513 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
516 throw new UnexpectedValueException(
"Unknown merge strategy '$mergeStrategy'" );
520 if ( isset( $info[
'autoloaderNS'] ) ) {
521 AutoLoader::registerNamespaces( $info[
'autoloaderNS'] );
524 if ( isset( $info[
'autoloaderClasses'] ) ) {
525 AutoLoader::registerClasses( $info[
'autoloaderClasses'] );
528 foreach ( $info[
'defines'] as $name => $val ) {
529 if ( !defined( $name ) ) {
530 define( $name, $val );
531 } elseif ( constant( $name ) !== $val ) {
532 throw new UnexpectedValueException(
533 "$name cannot be re-defined with $val it has already been set with " . constant( $name )
538 if ( isset( $info[
'autoloaderPaths'] ) ) {
539 AutoLoader::loadFiles( $info[
'autoloaderPaths'] );
542 $this->loaded += $info[
'credits'];
543 if ( $info[
'attributes'] ) {
544 if ( !$this->attributes ) {
545 $this->attributes = $info[
'attributes'];
547 $this->attributes = array_merge_recursive( $this->attributes, $info[
'attributes'] );
552 $settings = $this->getSettingsBuilder();
554 foreach ( $info[
'callbacks'] as $name => $cb ) {
555 if ( !is_callable( $cb ) ) {
556 if ( is_array( $cb ) ) {
557 $cb =
'[ ' . implode(
', ', $cb ) .
' ]';
559 throw new UnexpectedValueException(
"callback '$cb' is not callable" );
561 $cb( $info[
'credits'][$name], $settings );
573 public function isLoaded( $name, $constraint =
'*' ) {
574 $isLoaded = isset( $this->loaded[$name] );
575 if ( $constraint ===
'*' || !$isLoaded ) {
579 if ( !isset( $this->loaded[$name][
'version'] ) ) {
580 $msg =
"{$name} does not expose its version, but an extension or a skin"
581 .
" requires: {$constraint}.";
582 throw new LogicException( $msg );
585 return Semver::satisfies( $this->loaded[$name][
'version'], $constraint );
593 if ( isset( $this->testAttributes[$name] ) ) {
594 return $this->testAttributes[$name];
597 if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES,
true ) ) {
598 return $this->getLazyLoadedAttribute( $name );
601 return $this->attributes[$name] ?? [];
611 if ( isset( $this->testAttributes[$name] ) ) {
612 return $this->testAttributes[$name];
614 if ( isset( $this->lazyAttributes[$name] ) ) {
615 return $this->lazyAttributes[$name];
619 $cache = $this->getCache();
620 $key = $this->makeCacheKey( $cache,
'lazy-attrib', $name );
621 $data = $cache->
get( $key );
622 if ( $data !==
false ) {
623 $this->lazyAttributes[$name] = $data;
628 foreach ( $this->loaded as $info ) {
631 $paths[$info[
'path']] = 1;
634 $result = $this->readFromQueue( $paths );
635 $data = $result[
'attributes'][$name] ?? [];
636 $this->saveToCache( $cache, $result );
637 $this->lazyAttributes[$name] = $data;
652 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
653 throw new LogicException( __METHOD__ .
' can only be used in tests' );
656 if ( isset( $this->testAttributes[$name] ) ) {
657 throw new InvalidArgumentException(
"The attribute '$name' has already been overridden" );
659 $this->testAttributes[$name] = $value;
660 return new ScopedCallback(
function () use ( $name ) {
661 unset( $this->testAttributes[$name] );
671 return $this->loaded;
683 foreach ( $files as &$file ) {
684 $file =
"$dir/$file";
694 $this->settingsBuilder = $settingsBuilder;
698 if ( $this->settingsBuilder === null ) {
699 $this->settingsBuilder = SettingsBuilder::getInstance();
701 return $this->settingsBuilder;
const MW_VERSION
The running version of MediaWiki.
wfArrayPlus2d(array $baseArray, array $newValues)
Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
if(!defined('MW_SETUP_CALLBACK'))
Class representing a cache/ephemeral data store.
get( $key, $flags=0)
Get an item.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
makeGlobalKey( $keygroup,... $components)
Make a cache key from the given components, in the "global" keyspace.
Load extension manifests and then aggregate their contents.
Load JSON files, and uses a Processor to extract information.
setLoadTestClassesAndNamespaces( $load)
Controls if classes and namespaces defined under the keys TestAutoloadClasses and TestAutoloadNamespa...
static processAutoLoader( $dir, array $files)
Fully expand autoloader paths.
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
const MERGE_STRATEGY
Special key that defines the merge strategy.
getQueue()
Get the current load queue.
getLazyLoadedAttribute( $name)
Get an attribute value that isn't cached by reading each extension.json file again.
setSettingsBuilder(SettingsBuilder $settingsBuilder)
saveToCache(BagOStuff $cache, array $data)
Save data in the cache.
const MANIFEST_VERSION
Version of the highest supported manifest version Note: Update MANIFEST_VERSION_MW_VERSION when chang...
setAttributeForTest( $name, array $value)
Force override the value of an attribute during tests.
const OLDEST_MANIFEST_VERSION
Version of the oldest supported manifest version.
array $testAttributes
Attributes for testing.
clearQueue()
Clear the current load queue.
array $lazyAttributes
Lazy-loaded attributes.
setCache(BagOStuff $cache)
Set the cache to use for extension info.
const MANIFEST_VERSION_MW_VERSION
MediaWiki version constraint representing what the current highest MANIFEST_VERSION is supported in.
int[] $queued
List of paths that should be loaded.
const MEDIAWIKI_CORE
"requires" key that applies to MediaWiki core
bool $loadTestClassesAndNamespaces
Whether test classes and namespaces should be added to the auto loader.
readFromQueue(array $queue)
Process a queue of extensions and return their extracted data.
exportExtractedData(array $info)
setCheckDevRequires( $check)
getAllThings()
Get credits information about all installed extensions and skins.
finish()
After this is called, no more extensions can be loaded.
array $attributes
Items in the JSON file that aren't being set as globals.
bool $checkDev
Whether to check dev-requires.
Thrown when ExtensionRegistry cannot open the extension.json or skin.json file.
Check whether extensions and their dependencies meet certain version requirements.
$wgCachePrefix
Config variable stub for the CachePrefix setting, for use by phpdoc and IDEs.
$wgExtensionInfoMTime
Config variable stub for the ExtensionInfoMTime setting, for use by phpdoc and IDEs.
$wgDevelopmentWarnings
Config variable stub for the DevelopmentWarnings setting, for use by phpdoc and IDEs.