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;
167 private static bool $accessDisabledForUnitTests =
false;
174 if ( self::$accessDisabledForUnitTests ) {
175 throw new RuntimeException(
'Access is disabled in unit tests' );
177 if ( self::$instance ===
null ) {
178 self::$instance =
new self();
181 return self::$instance;
188 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
189 throw new RuntimeException(
'Can only be called in tests' );
191 self::$accessDisabledForUnitTests =
true;
198 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
199 throw new RuntimeException(
'Can only be called in tests' );
201 self::$accessDisabledForUnitTests =
false;
213 $this->cache = $cache;
222 $this->checkDev = $check;
223 $this->invalidateProcessCache();
235 $this->loadTestClassesAndNamespaces = $load;
245 if ( $mtime ===
false ) {
247 $mtime = @filemtime(
$path );
249 if ( $mtime ===
false ) {
250 $err = error_get_last();
255 $this->queued[
$path] = $mtime;
256 $this->invalidateProcessCache();
260 if ( !$this->cache ) {
268 :
WikiMap::getCurrentWikiDbDomain()->getId();
270 return ObjectCacheFactory::makeLocalServerCache( $keyspace );
276 private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) {
278 return $cache->makeGlobalKey(
279 "registration-$component",
280 $this->getVaryHash(),
290 private function getVaryHash() {
291 if ( $this->varyHash ===
null ) {
295 'registration' => self::CACHE_VERSION,
297 'abilities' => $this->getAbilities(),
298 'checkDev' => $this->checkDev,
299 'queue' => $this->queued,
301 $this->varyHash = md5( json_encode( $vary ) );
304 return $this->varyHash;
310 private function invalidateProcessCache() {
311 $this->varyHash =
null;
312 $this->lazyAttributes = [];
316 if ( !$this->queued ) {
320 if ( $this->finished ) {
321 throw new LogicException(
322 "The following paths tried to load late: "
323 . implode(
', ', array_keys( $this->queued ) )
327 $cache = $this->getCache();
329 $key = $this->makeCacheKey( $cache,
'main' );
330 $data = $cache->get( $key );
332 $data = $this->readFromQueue( $this->queued );
333 $this->saveToCache( $cache, $data );
335 $this->exportExtractedData( $data );
352 foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
353 if ( isset( $data[
'attributes'][$attrib] ) ) {
354 $lazy[$attrib] = $data[
'attributes'][$attrib];
355 unset( $data[
'attributes'][$attrib] );
358 $mainKey = $this->makeCacheKey( $cache,
'main' );
359 $cache->
set( $mainKey, $data, self::CACHE_EXPIRY );
360 foreach ( $lazy as $attrib => $value ) {
362 $this->makeCacheKey( $cache,
'lazy-attrib', $attrib ),
376 return $this->queued;
385 $this->invalidateProcessCache();
394 $this->finished =
true;
402 private function getAbilities() {
404 'shell' => !Shell::isDisabled(),
413 private function buildVersionChecker() {
420 return new VersionChecker(
422 PHP_MAJOR_VERSION .
'.' . PHP_MINOR_VERSION .
'.' . PHP_RELEASE_VERSION,
423 get_loaded_extensions(),
424 $this->getAbilities(),
442 $versionChecker = $this->buildVersionChecker();
443 $extDependencies = [];
445 foreach ( $queue as
$path => $mtime ) {
446 $json = file_get_contents(
$path );
447 if ( $json ===
false ) {
448 throw new InvalidArgumentException(
"Unable to read $path, does it exist?" );
450 $info = json_decode( $json,
true );
451 if ( !is_array( $info ) ) {
452 throw new InvalidArgumentException(
"$path is not a valid JSON file." );
455 $version = $info[
'manifest_version'];
456 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
457 throw new InvalidArgumentException(
"$path: unsupported manifest_version: {$version}" );
461 $requires = $processor->getRequirements( $info, $this->checkDev );
464 if ( is_array( $requires ) && $requires && isset( $info[
'name'] ) ) {
465 $extDependencies[$info[
'name']] = $requires;
469 $processor->extractInfo(
$path, $info, $version );
471 $data = $processor->getExtractedInfo( $this->loadTestClassesAndNamespaces );
472 $data[
'warnings'] = $warnings;
475 $incompatible = $versionChecker
476 ->setLoadedExtensionsAndSkins( $data[
'credits'] )
477 ->checkArray( $extDependencies );
479 if ( $incompatible ) {
487 if ( $info[
'globals'] ) {
491 $knownGlobals = array_fill_keys( array_keys( $GLOBALS ),
true );
493 foreach ( $info[
'globals'] as $key => $val ) {
496 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
497 $mergeStrategy = $val[self::MERGE_STRATEGY];
498 unset( $val[self::MERGE_STRATEGY] );
500 $mergeStrategy =
'array_merge';
503 if ( $mergeStrategy ===
'provide_default' ) {
504 if ( !isset( $knownGlobals[$key] ) ) {
505 $GLOBALS[$key] = $val;
506 $knownGlobals[$key] =
true;
512 if ( !isset( $knownGlobals[$key] ) ) {
513 $GLOBALS[$key] = $val;
514 $knownGlobals[$key] =
true;
516 } elseif ( !is_array( $val ) || !is_array( $GLOBALS[$key] ) ) {
520 } elseif ( !$GLOBALS[$key] ) {
522 $GLOBALS[$key] = $val;
526 switch ( $mergeStrategy ) {
527 case 'array_merge_recursive':
528 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
530 case 'array_replace_recursive':
531 $GLOBALS[$key] = array_replace_recursive( $val, $GLOBALS[$key] );
533 case 'array_plus_2d':
537 $GLOBALS[$key] += $val;
540 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
543 throw new UnexpectedValueException(
"Unknown merge strategy '$mergeStrategy'" );
548 if ( isset( $info[
'autoloaderNS'] ) ) {
549 AutoLoader::registerNamespaces( $info[
'autoloaderNS'] );
552 if ( isset( $info[
'autoloaderClasses'] ) ) {
553 AutoLoader::registerClasses( $info[
'autoloaderClasses'] );
556 foreach ( $info[
'defines'] as $name => $val ) {
557 if ( !defined( $name ) ) {
558 define( $name, $val );
559 } elseif ( constant( $name ) !== $val ) {
560 throw new UnexpectedValueException(
561 "$name cannot be re-defined with $val it has already been set with " . constant( $name )
566 if ( isset( $info[
'autoloaderPaths'] ) ) {
567 AutoLoader::loadFiles( $info[
'autoloaderPaths'] );
570 $this->loaded += $info[
'credits'];
571 if ( $info[
'attributes'] ) {
572 if ( !$this->attributes ) {
573 $this->attributes = $info[
'attributes'];
575 $this->attributes = array_merge_recursive( $this->attributes, $info[
'attributes'] );
580 $settings = $this->getSettingsBuilder();
582 foreach ( $info[
'callbacks'] as $name => $cb ) {
583 if ( !is_callable( $cb ) ) {
584 if ( is_array( $cb ) ) {
585 $cb =
'[ ' . implode(
', ', $cb ) .
' ]';
587 throw new UnexpectedValueException(
"callback '$cb' is not callable" );
589 $cb( $info[
'credits'][$name], $settings );
600 public function isLoaded( $name, $constraint =
'*' ) {
601 $isLoaded = isset( $this->loaded[$name] );
602 if ( $constraint ===
'*' || !$isLoaded ) {
606 if ( !isset( $this->loaded[$name][
'version'] ) ) {
607 $msg =
"{$name} does not expose its version, but an extension or a skin"
608 .
" requires: {$constraint}.";
609 throw new LogicException( $msg );
612 return Semver::satisfies( $this->loaded[$name][
'version'], $constraint );
621 if ( isset( $this->testAttributes[$name] ) ) {
622 return $this->testAttributes[$name];
625 if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES,
true ) ) {
626 return $this->getLazyLoadedAttribute( $name );
629 return $this->attributes[$name] ?? [];
638 foreach ( $this->getAttribute(
'DomainEventSubscribers' ) as $subscriber ) {
652 if ( isset( $this->testAttributes[$name] ) ) {
653 return $this->testAttributes[$name];
655 if ( isset( $this->lazyAttributes[$name] ) ) {
656 return $this->lazyAttributes[$name];
660 $cache = $this->getCache();
661 $key = $this->makeCacheKey( $cache,
'lazy-attrib', $name );
662 $data = $cache->get( $key );
663 if ( $data !==
false ) {
664 $this->lazyAttributes[$name] = $data;
670 foreach ( $this->loaded as $info ) {
673 $paths[$info[
'path']] = 1;
676 $result = $this->readFromQueue( $paths );
677 $data = $result[
'attributes'][$name] ?? [];
678 $this->saveToCache( $cache, $result );
679 $this->lazyAttributes[$name] = $data;
695 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
696 throw new LogicException( __METHOD__ .
' can only be used in tests' );
699 if ( isset( $this->testAttributes[$name] ) ) {
700 throw new InvalidArgumentException(
"The attribute '$name' has already been overridden" );
702 $this->testAttributes[$name] = $value;
704 return new ScopedCallback(
function () use ( $name ) {
705 unset( $this->testAttributes[$name] );
715 return $this->loaded;
728 foreach ( $files as &$file ) {
729 $file =
"$dir/$file";
741 $this->settingsBuilder = $settingsBuilder;
745 if ( $this->settingsBuilder === null ) {
746 $this->settingsBuilder = SettingsBuilder::getInstance();
749 return $this->settingsBuilder;
754class_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.