3 use Composer\Semver\Semver;
6 use Wikimedia\ScopedCallback;
61 'SkinLessImportPaths',
137 if ( self::$instance ===
null ) {
138 self::$instance =
new self();
149 $this->checkDev = $check;
160 $this->loadTestClassesAndNamespaces = $load;
170 if ( $mtime ===
false ) {
172 $mtime = @filemtime(
$path );
174 if ( $mtime ===
false ) {
175 $err = error_get_last();
176 throw new Exception(
"Unable to open file $path: {$err['message']}" );
180 $this->queued[
$path] = $mtime;
201 "registration-$component",
204 md5( json_encode( $vary ) ),
214 if ( !$this->queued ) {
218 if ( $this->finished ) {
220 "The following paths tried to load late: "
221 . implode(
', ', array_keys( $this->queued ) )
228 $data =
$cache->get( $key );
250 foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
251 if ( isset( $data[
'attributes'][$attrib] ) ) {
252 $lazy[$attrib] = $data[
'attributes'][$attrib];
253 unset( $data[
'attributes'][$attrib] );
257 $cache->set( $mainKey, $data, self::CACHE_EXPIRY );
258 foreach ( $lazy as $attrib => $value ) {
291 $this->finished =
true;
300 'shell' => !Shell::isDisabled(),
318 PHP_MAJOR_VERSION .
'.' . PHP_MINOR_VERSION .
'.' . PHP_RELEASE_VERSION,
319 get_loaded_extensions(),
334 $autoloadClasses = [];
335 $autoloadNamespaces = [];
336 $autoloaderPaths = [];
339 $extDependencies = [];
342 $json = file_get_contents(
$path );
343 if ( $json ===
false ) {
344 throw new Exception(
"Unable to read $path, does it exist?" );
346 $info = json_decode( $json,
true );
347 if ( !is_array( $info ) ) {
348 throw new Exception(
"$path is not a valid JSON file." );
351 if ( !isset( $info[
'manifest_version'] ) ) {
353 "{$info['name']}'s extension.json or skin.json does not have manifest_version, " .
354 'this is deprecated since MediaWiki 1.29',
359 $info[
'manifest_version'] = 1;
361 $version = $info[
'manifest_version'];
362 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
363 throw new Exception(
"$path: unsupported manifest_version: {$version}" );
366 $dir = dirname(
$path );
374 if ( $this->loadTestClassesAndNamespaces ) {
384 $requires = $processor->getRequirements( $info, $this->checkDev );
387 if ( is_array( $requires ) && $requires && isset( $info[
'name'] ) ) {
388 $extDependencies[$info[
'name']] = $requires;
392 $autoloaderPaths = array_merge( $autoloaderPaths,
393 $processor->getExtraAutoloaderPaths( $dir, $info ) );
395 $processor->extractInfo(
$path, $info, $version );
397 $data = $processor->getExtractedInfo();
398 $data[
'warnings'] = $warnings;
401 $incompatible = $versionChecker
402 ->setLoadedExtensionsAndSkins( $data[
'credits'] )
403 ->checkArray( $extDependencies );
405 if ( $incompatible ) {
410 $data[
'globals'][
'wgAutoloadClasses'] = $autoloadClasses;
411 $data[
'autoloaderPaths'] = $autoloaderPaths;
412 $data[
'autoloaderNS'] = $autoloadNamespaces;
425 $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
427 if ( isset( $info[
'AutoloadClasses'] ) ) {
430 $GLOBALS[
'wgAutoloadClasses'] += $autoload;
431 $autoloadClasses += $autoload;
433 if ( isset( $info[
'AutoloadNamespaces'] ) ) {
449 $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
451 if ( isset( $info[
'TestAutoloadClasses'] ) ) {
453 $GLOBALS[
'wgAutoloadClasses'] += $autoload;
454 $autoloadClasses += $autoload;
456 if ( isset( $info[
'TestAutoloadNamespaces'] ) ) {
463 foreach ( $info[
'globals'] as $key => $val ) {
466 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
468 unset( $val[self::MERGE_STRATEGY] );
470 $mergeStrategy =
'array_merge';
475 if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
476 $GLOBALS[$key] = $val;
480 if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
485 switch ( $mergeStrategy ) {
486 case 'array_merge_recursive':
487 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
489 case 'array_replace_recursive':
490 $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
492 case 'array_plus_2d':
496 $GLOBALS[$key] += $val;
499 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
502 throw new UnexpectedValueException(
"Unknown merge strategy '$mergeStrategy'" );
506 if ( isset( $info[
'autoloaderNS'] ) ) {
510 foreach ( $info[
'defines'] as $name => $val ) {
511 if ( !defined( $name ) ) {
512 define( $name, $val );
513 } elseif ( constant( $name ) !== $val ) {
514 throw new UnexpectedValueException(
515 "$name cannot be re-defined with $val it has already been set with " . constant( $name )
520 foreach ( $info[
'autoloaderPaths'] as
$path ) {
521 if ( file_exists(
$path ) ) {
526 $this->loaded += $info[
'credits'];
527 if ( $info[
'attributes'] ) {
528 if ( !$this->attributes ) {
529 $this->attributes = $info[
'attributes'];
531 $this->attributes = array_merge_recursive( $this->attributes, $info[
'attributes'] );
535 foreach ( $info[
'callbacks'] as $name => $cb ) {
536 if ( !is_callable( $cb ) ) {
537 if ( is_array( $cb ) ) {
538 $cb =
'[ ' . implode(
', ', $cb ) .
' ]';
540 throw new UnexpectedValueException(
"callback '$cb' is not callable" );
542 $cb( $info[
'credits'][$name] );
554 public function isLoaded( $name, $constraint =
'*' ) {
555 $isLoaded = isset( $this->loaded[$name] );
556 if ( $constraint ===
'*' || !$isLoaded ) {
560 if ( !isset( $this->loaded[$name][
'version'] ) ) {
561 $msg =
"{$name} does not expose its version, but an extension or a skin"
562 .
" requires: {$constraint}.";
563 throw new LogicException( $msg );
566 return Semver::satisfies( $this->loaded[$name][
'version'], $constraint );
574 if ( isset( $this->testAttributes[$name] ) ) {
575 return $this->testAttributes[$name];
578 if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES,
true ) ) {
582 return $this->attributes[$name] ?? [];
592 if ( isset( $this->testAttributes[$name] ) ) {
593 return $this->testAttributes[$name];
599 $data =
$cache->get( $key );
600 if ( $data !==
false ) {
605 foreach ( $this->loaded as $info ) {
608 $paths[$info[
'path']] = 1;
612 $data = $result[
'attributes'][$name] ?? [];
628 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
629 throw new RuntimeException( __METHOD__ .
' can only be used in tests' );
632 if ( isset( $this->testAttributes[$name] ) ) {
633 throw new Exception(
"The attribute '$name' has already been overridden" );
635 $this->testAttributes[$name] = $value;
636 return new ScopedCallback(
function () use ( $name ) {
637 unset( $this->testAttributes[$name] );
659 foreach ( $files as &
$file ) {
660 $file =
"$dir/$file";