6use Composer\Semver\Semver;
7use InvalidArgumentException;
17use UnexpectedValueException;
19use Wikimedia\ScopedCallback;
62 private const CACHE_VERSION = 8;
64 private const CACHE_EXPIRY = 60 * 60 * 24;
76 private const LAZY_LOADED_ATTRIBUTES = [
79 'SkinLessImportPaths',
107 private $finished =
false;
155 private static $instance;
160 private $cache =
null;
164 private static bool $accessDisabledForUnitTests =
false;
171 if ( self::$accessDisabledForUnitTests ) {
172 throw new RuntimeException(
'Access is disabled in unit tests' );
174 if ( self::$instance ===
null ) {
175 self::$instance =
new self();
178 return self::$instance;
185 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
186 throw new RuntimeException(
'Can only be called in tests' );
188 self::$accessDisabledForUnitTests =
true;
195 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
196 throw new RuntimeException(
'Can only be called in tests' );
198 self::$accessDisabledForUnitTests =
false;
210 $this->cache = $cache;
219 $this->checkDev = $check;
220 $this->invalidateProcessCache();
232 $this->loadTestClassesAndNamespaces = $load;
242 if ( $mtime ===
false ) {
244 $mtime = @filemtime(
$path );
246 if ( $mtime ===
false ) {
247 $err = error_get_last();
252 $this->queued[
$path] = $mtime;
253 $this->invalidateProcessCache();
257 if ( !$this->cache ) {
265 :
WikiMap::getCurrentWikiDbDomain()->getId();
267 return ObjectCacheFactory::makeLocalServerCache( $keyspace );
273 private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) {
275 return $cache->makeGlobalKey(
276 "registration-$component",
277 $this->getVaryHash(),
287 private function getVaryHash() {
288 if ( $this->varyHash ===
null ) {
292 'registration' => self::CACHE_VERSION,
294 'abilities' => $this->getAbilities(),
295 'checkDev' => $this->checkDev,
296 'queue' => $this->queued,
298 $this->varyHash = md5( json_encode( $vary ) );
301 return $this->varyHash;
307 private function invalidateProcessCache() {
308 $this->varyHash =
null;
309 $this->lazyAttributes = [];
313 if ( !$this->queued ) {
317 if ( $this->finished ) {
318 throw new LogicException(
319 "The following paths tried to load late: "
320 . implode(
', ', array_keys( $this->queued ) )
324 $cache = $this->getCache();
326 $key = $this->makeCacheKey( $cache,
'main' );
327 $data = $cache->get( $key );
329 $data = $this->readFromQueue( $this->queued );
330 $this->saveToCache( $cache, $data );
332 $this->exportExtractedData( $data );
349 foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
350 if ( isset( $data[
'attributes'][$attrib] ) ) {
351 $lazy[$attrib] = $data[
'attributes'][$attrib];
352 unset( $data[
'attributes'][$attrib] );
355 $mainKey = $this->makeCacheKey( $cache,
'main' );
356 $cache->
set( $mainKey, $data, self::CACHE_EXPIRY );
357 foreach ( $lazy as $attrib => $value ) {
359 $this->makeCacheKey( $cache,
'lazy-attrib', $attrib ),
373 return $this->queued;
382 $this->invalidateProcessCache();
391 $this->finished =
true;
399 private function getAbilities() {
401 'shell' => !Shell::isDisabled(),
410 private function buildVersionChecker() {
417 return new VersionChecker(
419 PHP_MAJOR_VERSION .
'.' . PHP_MINOR_VERSION .
'.' . PHP_RELEASE_VERSION,
420 get_loaded_extensions(),
421 $this->getAbilities(),
439 $versionChecker = $this->buildVersionChecker();
440 $extDependencies = [];
442 foreach ( $queue as
$path => $mtime ) {
443 $json = file_get_contents(
$path );
444 if ( $json ===
false ) {
445 throw new InvalidArgumentException(
"Unable to read $path, does it exist?" );
447 $info = json_decode( $json,
true );
448 if ( !is_array( $info ) ) {
449 throw new InvalidArgumentException(
"$path is not a valid JSON file." );
452 $version = $info[
'manifest_version'];
453 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
454 throw new InvalidArgumentException(
"$path: unsupported manifest_version: {$version}" );
458 $requires = $processor->getRequirements( $info, $this->checkDev );
461 if ( is_array( $requires ) && $requires && isset( $info[
'name'] ) ) {
462 $extDependencies[$info[
'name']] = $requires;
466 $processor->extractInfo(
$path, $info, $version );
468 $data = $processor->getExtractedInfo( $this->loadTestClassesAndNamespaces );
469 $data[
'warnings'] = $warnings;
472 $incompatible = $versionChecker
473 ->setLoadedExtensionsAndSkins( $data[
'credits'] )
474 ->checkArray( $extDependencies );
476 if ( $incompatible ) {
484 if ( $info[
'globals'] ) {
488 $knownGlobals = array_fill_keys( array_keys( $GLOBALS ),
true );
490 foreach ( $info[
'globals'] as $key => $val ) {
493 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
494 $mergeStrategy = $val[self::MERGE_STRATEGY];
495 unset( $val[self::MERGE_STRATEGY] );
497 $mergeStrategy =
'array_merge';
500 if ( $mergeStrategy ===
'provide_default' ) {
501 if ( !isset( $knownGlobals[$key] ) ) {
502 $GLOBALS[$key] = $val;
503 $knownGlobals[$key] =
true;
509 if ( !isset( $knownGlobals[$key] ) ) {
510 $GLOBALS[$key] = $val;
511 $knownGlobals[$key] =
true;
513 } elseif ( !is_array( $val ) || !is_array( $GLOBALS[$key] ) ) {
517 } elseif ( !$GLOBALS[$key] ) {
519 $GLOBALS[$key] = $val;
523 switch ( $mergeStrategy ) {
524 case 'array_merge_recursive':
525 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
527 case 'array_replace_recursive':
528 $GLOBALS[$key] = array_replace_recursive( $val, $GLOBALS[$key] );
530 case 'array_plus_2d':
534 $GLOBALS[$key] += $val;
537 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
540 throw new UnexpectedValueException(
"Unknown merge strategy '$mergeStrategy'" );
545 if ( isset( $info[
'autoloaderNS'] ) ) {
546 AutoLoader::registerNamespaces( $info[
'autoloaderNS'] );
549 if ( isset( $info[
'autoloaderClasses'] ) ) {
550 AutoLoader::registerClasses( $info[
'autoloaderClasses'] );
553 foreach ( $info[
'defines'] as $name => $val ) {
554 if ( !defined( $name ) ) {
555 define( $name, $val );
556 } elseif ( constant( $name ) !== $val ) {
557 throw new UnexpectedValueException(
558 "$name cannot be re-defined with $val it has already been set with " . constant( $name )
563 if ( isset( $info[
'autoloaderPaths'] ) ) {
564 AutoLoader::loadFiles( $info[
'autoloaderPaths'] );
567 $this->loaded += $info[
'credits'];
568 if ( $info[
'attributes'] ) {
569 if ( !$this->attributes ) {
570 $this->attributes = $info[
'attributes'];
572 $this->attributes = array_merge_recursive( $this->attributes, $info[
'attributes'] );
577 $settings = $this->getSettingsBuilder();
579 foreach ( $info[
'callbacks'] as $name => $cb ) {
580 if ( !is_callable( $cb ) ) {
581 if ( is_array( $cb ) ) {
582 $cb =
'[ ' . implode(
', ', $cb ) .
' ]';
584 throw new UnexpectedValueException(
"callback '$cb' is not callable" );
586 $cb( $info[
'credits'][$name], $settings );
597 public function isLoaded( $name, $constraint =
'*' ) {
598 $isLoaded = isset( $this->loaded[$name] );
599 if ( $constraint ===
'*' || !$isLoaded ) {
603 if ( !isset( $this->loaded[$name][
'version'] ) ) {
604 $msg =
"{$name} does not expose its version, but an extension or a skin"
605 .
" requires: {$constraint}.";
606 throw new LogicException( $msg );
609 return Semver::satisfies( $this->loaded[$name][
'version'], $constraint );
618 if ( isset( $this->testAttributes[$name] ) ) {
619 return $this->testAttributes[$name];
622 if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES,
true ) ) {
623 return $this->getLazyLoadedAttribute( $name );
626 return $this->attributes[$name] ?? [];
635 $subscribers = $this->getAttribute(
'DomainEventIngresses' );
637 foreach ( $subscribers as $subscriber ) {
651 if ( isset( $this->testAttributes[$name] ) ) {
652 return $this->testAttributes[$name];
654 if ( isset( $this->lazyAttributes[$name] ) ) {
655 return $this->lazyAttributes[$name];
659 $cache = $this->getCache();
660 $key = $this->makeCacheKey( $cache,
'lazy-attrib', $name );
661 $data = $cache->get( $key );
662 if ( $data !==
false ) {
663 $this->lazyAttributes[$name] = $data;
669 foreach ( $this->loaded as $info ) {
672 $paths[$info[
'path']] = 1;
675 $result = $this->readFromQueue( $paths );
676 $data = $result[
'attributes'][$name] ?? [];
677 $this->saveToCache( $cache, $result );
678 $this->lazyAttributes[$name] = $data;
694 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
695 throw new LogicException( __METHOD__ .
' can only be used in tests' );
698 if ( isset( $this->testAttributes[$name] ) ) {
699 throw new InvalidArgumentException(
"The attribute '$name' has already been overridden" );
701 $this->testAttributes[$name] = $value;
703 return new ScopedCallback(
function () use ( $name ) {
704 unset( $this->testAttributes[$name] );
714 return $this->loaded;
727 foreach ( $files as &$file ) {
728 $file =
"$dir/$file";
740 $this->settingsBuilder = $settingsBuilder;
744 if ( $this->settingsBuilder === null ) {
745 $this->settingsBuilder = SettingsBuilder::getInstance();
748 return $this->settingsBuilder;
753class_alias( ExtensionRegistry::class,
'ExtensionRegistry' );
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'))
Locations of core classes Extension classes are specified with $wgAutoloadClasses.
Factory for cache objects as configured in the ObjectCaches setting.
$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.
Service object for registering listeners for domain events.
registerSubscriber( $subscriber)
Register the given subscriber to this event source.
Objects implementing DomainEventSubscriber represent a collection of related event listeners.