3 use Composer\Semver\Semver;
4 use Wikimedia\AtEase\AtEase;
5 use Wikimedia\ScopedCallback;
107 if ( self::$instance ===
null ) {
108 self::$instance =
new self();
119 $this->checkDev = $check;
129 if ( $mtime ===
false ) {
130 AtEase::suppressWarnings();
131 $mtime = filemtime(
$path );
132 AtEase::restoreWarnings();
134 if ( $mtime ===
false ) {
135 $err = error_get_last();
136 throw new Exception(
"Unable to open file $path: {$err['message']}" );
140 $this->queued[
$path] = $mtime;
149 if ( !$this->queued ) {
153 if ( $this->finished ) {
155 "The following paths tried to load late: "
156 . implode(
', ', array_keys( $this->queued ) )
174 }
catch ( InvalidArgumentException $e ) {
180 md5( json_encode( $this->queued + $versions ) )
182 $data =
$cache->get( $key );
190 $data[
'globals'][
'wgAutoloadClasses'] += $data[
'autoload'];
191 unset( $data[
'autoload'] );
194 $cache->set( $key, $data, 60 * 60 * 24 );
224 $this->finished =
true;
233 'shell' => !Shell::isDisabled(),
252 PHP_MAJOR_VERSION .
'.' . PHP_MINOR_VERSION .
'.' . PHP_RELEASE_VERSION,
253 get_loaded_extensions(),
268 $autoloadClasses = [];
269 $autoloadNamespaces = [];
270 $autoloaderPaths = [];
273 $extDependencies = [];
277 $json = file_get_contents(
$path );
278 if ( $json ===
false ) {
279 throw new Exception(
"Unable to read $path, does it exist?" );
281 $info = json_decode( $json,
true );
282 if ( !is_array( $info ) ) {
283 throw new Exception(
"$path is not a valid JSON file." );
286 if ( !isset( $info[
'manifest_version'] ) ) {
288 "{$info['name']}'s extension.json or skin.json does not have manifest_version",
293 $info[
'manifest_version'] = 1;
295 $version = $info[
'manifest_version'];
296 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
297 $incompatible[] =
"$path: unsupported manifest_version: {$version}";
300 $dir = dirname(
$path );
309 $requires = $processor->getRequirements( $info, $this->checkDev );
312 if ( is_array( $requires ) && $requires && isset( $info[
'name'] ) ) {
313 $extDependencies[$info[
'name']] = $requires;
317 $autoloaderPaths = array_merge( $autoloaderPaths,
318 $processor->getExtraAutoloaderPaths( $dir, $info ) );
320 $processor->extractInfo(
$path, $info, $version );
322 $data = $processor->getExtractedInfo();
323 $data[
'warnings'] = $warnings;
326 $incompatible = array_merge(
329 ->setLoadedExtensionsAndSkins( $data[
'credits'] )
330 ->checkArray( $extDependencies )
333 if ( $incompatible ) {
338 $data[
'globals'][
'wgAutoloadClasses'] = [];
339 $data[
'autoload'] = $autoloadClasses;
340 $data[
'autoloaderPaths'] = $autoloaderPaths;
341 $data[
'autoloaderNS'] = $autoloadNamespaces;
354 $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
356 if ( isset( $info[
'AutoloadClasses'] ) ) {
358 $GLOBALS[
'wgAutoloadClasses'] += $autoload;
359 $autoloadClasses += $autoload;
361 if ( isset( $info[
'AutoloadNamespaces'] ) ) {
368 foreach ( $info[
'globals'] as $key => $val ) {
371 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
373 unset( $val[self::MERGE_STRATEGY] );
375 $mergeStrategy =
'array_merge';
385 if ( !is_array(
$GLOBALS[$key] ) || !is_array( $val ) ) {
390 switch ( $mergeStrategy ) {
391 case 'array_merge_recursive':
394 case 'array_replace_recursive':
397 case 'array_plus_2d':
407 throw new UnexpectedValueException(
"Unknown merge strategy '$mergeStrategy'" );
411 if ( isset( $info[
'autoloaderNS'] ) ) {
415 foreach ( $info[
'defines'] as $name => $val ) {
416 define( $name, $val );
418 foreach ( $info[
'autoloaderPaths'] as
$path ) {
419 if ( file_exists(
$path ) ) {
424 $this->loaded += $info[
'credits'];
425 if ( $info[
'attributes'] ) {
426 if ( !$this->attributes ) {
427 $this->attributes = $info[
'attributes'];
429 $this->attributes = array_merge_recursive( $this->attributes, $info[
'attributes'] );
433 foreach ( $info[
'callbacks'] as $name => $cb ) {
434 if ( !is_callable( $cb ) ) {
435 if ( is_array( $cb ) ) {
436 $cb =
'[ ' . implode(
', ', $cb ) .
' ]';
438 throw new UnexpectedValueException(
"callback '$cb' is not callable" );
440 $cb( $info[
'credits'][$name] );
468 public function isLoaded( $name, $constraint =
'*' ) {
469 $isLoaded = isset( $this->loaded[$name] );
470 if ( $constraint ===
'*' || !$isLoaded ) {
474 if ( !isset( $this->loaded[$name][
'version'] ) ) {
475 $msg =
"{$name} does not expose its version, but an extension or a skin"
476 .
" requires: {$constraint}.";
477 throw new LogicException( $msg );
480 return SemVer::satisfies( $this->loaded[$name][
'version'], $constraint );
488 return $this->testAttributes[$name] ??
489 $this->attributes[$name] ?? [];
502 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
503 throw new RuntimeException( __METHOD__ .
' can only be used in tests' );
506 if ( isset( $this->testAttributes[$name] ) ) {
507 throw new Exception(
"The attribute '$name' has already been overridden" );
509 $this->testAttributes[$name] = $value;
510 return new ScopedCallback(
function () use ( $name ) {
511 unset( $this->testAttributes[$name] );
533 foreach ( $files as &
$file ) {
534 $file =
"$dir/$file";