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;
199 "registration-$component",
202 md5( json_encode( $vary ) ),
212 if ( !$this->queued ) {
216 if ( $this->finished ) {
218 "The following paths tried to load late: "
219 . implode(
', ', array_keys( $this->queued ) )
226 $data =
$cache->get( $key );
248 foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
249 if ( isset( $data[
'attributes'][$attrib] ) ) {
250 $lazy[$attrib] = $data[
'attributes'][$attrib];
251 unset( $data[
'attributes'][$attrib] );
255 $cache->set( $mainKey, $data, self::CACHE_EXPIRY );
256 foreach ( $lazy as $attrib => $value ) {
289 $this->finished =
true;
298 'shell' => !Shell::isDisabled(),
316 PHP_MAJOR_VERSION .
'.' . PHP_MINOR_VERSION .
'.' . PHP_RELEASE_VERSION,
317 get_loaded_extensions(),
332 $autoloadClasses = [];
333 $autoloadNamespaces = [];
334 $autoloaderPaths = [];
337 $extDependencies = [];
340 $json = file_get_contents(
$path );
341 if ( $json ===
false ) {
342 throw new Exception(
"Unable to read $path, does it exist?" );
344 $info = json_decode( $json,
true );
345 if ( !is_array( $info ) ) {
346 throw new Exception(
"$path is not a valid JSON file." );
349 if ( !isset( $info[
'manifest_version'] ) ) {
351 "{$info['name']}'s extension.json or skin.json does not have manifest_version, " .
352 'this is deprecated since MediaWiki 1.29',
357 $info[
'manifest_version'] = 1;
359 $version = $info[
'manifest_version'];
360 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
361 throw new Exception(
"$path: unsupported manifest_version: {$version}" );
364 $dir = dirname(
$path );
372 if ( $this->loadTestClassesAndNamespaces ) {
382 $requires = $processor->getRequirements( $info, $this->checkDev );
385 if ( is_array( $requires ) && $requires && isset( $info[
'name'] ) ) {
386 $extDependencies[$info[
'name']] = $requires;
390 $autoloaderPaths = array_merge( $autoloaderPaths,
391 $processor->getExtraAutoloaderPaths( $dir, $info ) );
393 $processor->extractInfo(
$path, $info, $version );
395 $data = $processor->getExtractedInfo();
396 $data[
'warnings'] = $warnings;
399 $incompatible = $versionChecker
400 ->setLoadedExtensionsAndSkins( $data[
'credits'] )
401 ->checkArray( $extDependencies );
403 if ( $incompatible ) {
408 $data[
'globals'][
'wgAutoloadClasses'] = $autoloadClasses;
409 $data[
'autoloaderPaths'] = $autoloaderPaths;
410 $data[
'autoloaderNS'] = $autoloadNamespaces;
423 $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
425 if ( isset( $info[
'AutoloadClasses'] ) ) {
428 $GLOBALS[
'wgAutoloadClasses'] += $autoload;
429 $autoloadClasses += $autoload;
431 if ( isset( $info[
'AutoloadNamespaces'] ) ) {
447 $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
449 if ( isset( $info[
'TestAutoloadClasses'] ) ) {
451 $GLOBALS[
'wgAutoloadClasses'] += $autoload;
452 $autoloadClasses += $autoload;
454 if ( isset( $info[
'TestAutoloadNamespaces'] ) ) {
461 foreach ( $info[
'globals'] as $key => $val ) {
464 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
466 unset( $val[self::MERGE_STRATEGY] );
468 $mergeStrategy =
'array_merge';
473 if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
474 $GLOBALS[$key] = $val;
478 if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
483 switch ( $mergeStrategy ) {
484 case 'array_merge_recursive':
485 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
487 case 'array_replace_recursive':
488 $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
490 case 'array_plus_2d':
494 $GLOBALS[$key] += $val;
497 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
500 throw new UnexpectedValueException(
"Unknown merge strategy '$mergeStrategy'" );
504 if ( isset( $info[
'autoloaderNS'] ) ) {
508 foreach ( $info[
'defines'] as $name => $val ) {
509 if ( !defined( $name ) ) {
510 define( $name, $val );
511 } elseif ( constant( $name ) !== $val ) {
512 throw new UnexpectedValueException(
513 "$name cannot be re-defined with $val it has already been set with " . constant( $name )
518 foreach ( $info[
'autoloaderPaths'] as
$path ) {
519 if ( file_exists(
$path ) ) {
524 $this->loaded += $info[
'credits'];
525 if ( $info[
'attributes'] ) {
526 if ( !$this->attributes ) {
527 $this->attributes = $info[
'attributes'];
529 $this->attributes = array_merge_recursive( $this->attributes, $info[
'attributes'] );
533 foreach ( $info[
'callbacks'] as $name => $cb ) {
534 if ( !is_callable( $cb ) ) {
535 if ( is_array( $cb ) ) {
536 $cb =
'[ ' . implode(
', ', $cb ) .
' ]';
538 throw new UnexpectedValueException(
"callback '$cb' is not callable" );
540 $cb( $info[
'credits'][$name] );
552 public function isLoaded( $name, $constraint =
'*' ) {
553 $isLoaded = isset( $this->loaded[$name] );
554 if ( $constraint ===
'*' || !$isLoaded ) {
558 if ( !isset( $this->loaded[$name][
'version'] ) ) {
559 $msg =
"{$name} does not expose its version, but an extension or a skin"
560 .
" requires: {$constraint}.";
561 throw new LogicException( $msg );
564 return Semver::satisfies( $this->loaded[$name][
'version'], $constraint );
572 if ( isset( $this->testAttributes[$name] ) ) {
573 return $this->testAttributes[$name];
576 if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES,
true ) ) {
580 return $this->attributes[$name] ?? [];
590 if ( isset( $this->testAttributes[$name] ) ) {
591 return $this->testAttributes[$name];
597 $data =
$cache->get( $key );
598 if ( $data !==
false ) {
603 foreach ( $this->loaded as $info ) {
606 $paths[$info[
'path']] = 1;
610 $data = $result[
'attributes'][$name] ?? [];
626 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
627 throw new RuntimeException( __METHOD__ .
' can only be used in tests' );
630 if ( isset( $this->testAttributes[$name] ) ) {
631 throw new Exception(
"The attribute '$name' has already been overridden" );
633 $this->testAttributes[$name] = $value;
634 return new ScopedCallback(
function () use ( $name ) {
635 unset( $this->testAttributes[$name] );
657 foreach ( $files as &
$file ) {
658 $file =
"$dir/$file";