MediaWiki REL1_39
ExtensionRegistry.php
Go to the documentation of this file.
1<?php
2
3use Composer\Semver\Semver;
6use Wikimedia\ScopedCallback;
7
16
20 public const MEDIAWIKI_CORE = 'MediaWiki';
21
26 public const MANIFEST_VERSION = 2;
27
32 public const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
33
37 public const OLDEST_MANIFEST_VERSION = 1;
38
42 private const CACHE_VERSION = 8;
43
44 private const CACHE_EXPIRY = 60 * 60 * 24;
45
51 public const MERGE_STRATEGY = '_merge_strategy';
52
56 private const LAZY_LOADED_ATTRIBUTES = [
57 'TrackingCategories',
58 'QUnitTestModules',
59 'SkinLessImportPaths',
60 ];
61
73 private $loaded = [];
74
80 protected $queued = [];
81
87 private $finished = false;
88
95 protected $attributes = [];
96
102 protected $testAttributes = [];
103
109 protected $lazyAttributes = [];
110
116 private $varyHash;
117
123 protected $checkDev = false;
124
131
135 private static $instance;
136
140 private $cache = null;
141
146 public static function getInstance() {
147 if ( self::$instance === null ) {
148 self::$instance = new self();
149 }
150
151 return self::$instance;
152 }
153
162 public function setCache( BagOStuff $cache ): void {
163 $this->cache = $cache;
164 }
165
170 public function setCheckDevRequires( $check ) {
171 $this->checkDev = $check;
172 $this->invalidateProcessCache();
173 }
174
182 public function setLoadTestClassesAndNamespaces( $load ) {
183 $this->loadTestClassesAndNamespaces = $load;
184 }
185
189 public function queue( $path ) {
191
192 $mtime = $wgExtensionInfoMTime;
193 if ( $mtime === false ) {
194 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
195 $mtime = @filemtime( $path );
196 // @codeCoverageIgnoreStart
197 if ( $mtime === false ) {
198 $err = error_get_last();
199 throw new Exception( "Unable to open file $path: {$err['message']}" );
200 // @codeCoverageIgnoreEnd
201 }
202 }
203 $this->queued[$path] = $mtime;
204 $this->invalidateProcessCache();
205 }
206
207 private function getCache(): BagOStuff {
208 if ( !$this->cache ) {
209 // Can't call MediaWikiServices here, as we must not cause services
210 // to be instantiated before extensions have loaded.
211 return ObjectCache::makeLocalServerCache();
212 }
213
214 return $this->cache;
215 }
216
217 private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) {
218 // Allow reusing cached ExtensionRegistry metadata between wikis (T274648)
219 return $cache->makeGlobalKey(
220 "registration-$component",
221 $this->getVaryHash(),
222 ...$extra
223 );
224 }
225
231 private function getVaryHash() {
232 if ( $this->varyHash === null ) {
233 // We vary the cache on the current queue (what will be or already was loaded)
234 // plus various versions of stuff for VersionChecker
235 $vary = [
236 'registration' => self::CACHE_VERSION,
237 'mediawiki' => MW_VERSION,
238 'abilities' => $this->getAbilities(),
239 'checkDev' => $this->checkDev,
240 'queue' => $this->queued,
241 ];
242 $this->varyHash = md5( json_encode( $vary ) );
243 }
244 return $this->varyHash;
245 }
246
250 private function invalidateProcessCache() {
251 $this->varyHash = null;
252 $this->lazyAttributes = [];
253 }
254
259 public function loadFromQueue() {
260 if ( !$this->queued ) {
261 return;
262 }
263
264 if ( $this->finished ) {
265 throw new MWException(
266 "The following paths tried to load late: "
267 . implode( ', ', array_keys( $this->queued ) )
268 );
269 }
270
271 $cache = $this->getCache();
272 // See if this queue is in APC
273 $key = $this->makeCacheKey( $cache, 'main' );
274 $data = $cache->get( $key );
275 if ( !$data ) {
276 $data = $this->readFromQueue( $this->queued );
277 $this->saveToCache( $cache, $data );
278 }
279 $this->exportExtractedData( $data );
280 }
281
288 protected function saveToCache( BagOStuff $cache, array $data ) {
290 if ( $data['warnings'] && $wgDevelopmentWarnings ) {
291 // If warnings were shown, don't cache it
292 return;
293 }
294 $lazy = [];
295 // Cache lazy-loaded attributes separately
296 foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) {
297 if ( isset( $data['attributes'][$attrib] ) ) {
298 $lazy[$attrib] = $data['attributes'][$attrib];
299 unset( $data['attributes'][$attrib] );
300 }
301 }
302 $mainKey = $this->makeCacheKey( $cache, 'main' );
303 $cache->set( $mainKey, $data, self::CACHE_EXPIRY );
304 foreach ( $lazy as $attrib => $value ) {
305 $cache->set(
306 $this->makeCacheKey( $cache, 'lazy-attrib', $attrib ),
307 $value,
308 self::CACHE_EXPIRY
309 );
310 }
311 }
312
319 public function getQueue() {
320 return $this->queued;
321 }
322
327 public function clearQueue() {
328 $this->queued = [];
329 $this->invalidateProcessCache();
330 }
331
337 public function finish() {
338 $this->finished = true;
339 }
340
345 private function getAbilities() {
346 return [
347 'shell' => !Shell::isDisabled(),
348 ];
349 }
350
356 private function buildVersionChecker() {
357 // array to optionally specify more verbose error messages for
358 // missing abilities
359 $abilityErrors = [
360 'shell' => ( new ShellDisabledError() )->getMessage(),
361 ];
362
363 return new VersionChecker(
365 PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
366 get_loaded_extensions(),
367 $this->getAbilities(),
368 $abilityErrors
369 );
370 }
371
382 public function readFromQueue( array $queue ) {
383 $processor = new ExtensionProcessor();
384 $versionChecker = $this->buildVersionChecker();
385 $extDependencies = [];
386 $warnings = false;
387 foreach ( $queue as $path => $mtime ) {
388 $json = file_get_contents( $path );
389 if ( $json === false ) {
390 throw new Exception( "Unable to read $path, does it exist?" );
391 }
392 $info = json_decode( $json, /* $assoc = */ true );
393 if ( !is_array( $info ) ) {
394 throw new Exception( "$path is not a valid JSON file." );
395 }
396
397 if ( !isset( $info['manifest_version'] ) ) {
399 "{$info['name']}'s extension.json or skin.json does not have manifest_version, " .
400 'this is deprecated since MediaWiki 1.29',
401 '1.29', false, false
402 );
403 $warnings = true;
404 // For backwards-compatibility, assume a version of 1
405 $info['manifest_version'] = 1;
406 }
407 $version = $info['manifest_version'];
408 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
409 throw new Exception( "$path: unsupported manifest_version: {$version}" );
410 }
411
412 // get all requirements/dependencies for this extension
413 $requires = $processor->getRequirements( $info, $this->checkDev );
414
415 // validate the information needed and add the requirements
416 if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
417 $extDependencies[$info['name']] = $requires;
418 }
419
420 // Compatible, read and extract info
421 $processor->extractInfo( $path, $info, $version );
422 }
423 $data = $processor->getExtractedInfo( $this->loadTestClassesAndNamespaces );
424 $data['warnings'] = $warnings;
425
426 // check for incompatible extensions
427 $incompatible = $versionChecker
428 ->setLoadedExtensionsAndSkins( $data['credits'] )
429 ->checkArray( $extDependencies );
430
431 if ( $incompatible ) {
432 throw new ExtensionDependencyError( $incompatible );
433 }
434
435 return $data;
436 }
437
438 protected function exportExtractedData( array $info ) {
439 foreach ( $info['globals'] as $key => $val ) {
440 // If a merge strategy is set, read it and remove it from the value
441 // so it doesn't accidentally end up getting set.
442 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
443 $mergeStrategy = $val[self::MERGE_STRATEGY];
444 unset( $val[self::MERGE_STRATEGY] );
445 } else {
446 $mergeStrategy = 'array_merge';
447 }
448
449 if ( $mergeStrategy === 'provide_default' ) {
450 if ( !array_key_exists( $key, $GLOBALS ) ) {
451 $GLOBALS[$key] = $val;
452 }
453 continue;
454 }
455
456 // Optimistic: If the global is not set, or is an empty array, replace it entirely.
457 // Will be O(1) performance.
458 if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
459 $GLOBALS[$key] = $val;
460 continue;
461 }
462
463 if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
464 // config setting that has already been overridden, don't set it
465 continue;
466 }
467
468 switch ( $mergeStrategy ) {
469 case 'array_merge_recursive':
470 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
471 break;
472 case 'array_replace_recursive':
473 $GLOBALS[$key] = array_replace_recursive( $val, $GLOBALS[$key] );
474 break;
475 case 'array_plus_2d':
476 $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
477 break;
478 case 'array_plus':
479 $GLOBALS[$key] += $val;
480 break;
481 case 'array_merge':
482 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
483 break;
484 default:
485 throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
486 }
487 }
488
489 if ( isset( $info['autoloaderNS'] ) ) {
490 AutoLoader::registerNamespaces( $info['autoloaderNS'] );
491 }
492
493 if ( isset( $info['autoloaderClasses'] ) ) {
494 AutoLoader::registerClasses( $info['autoloaderClasses'] );
495 }
496
497 foreach ( $info['defines'] as $name => $val ) {
498 if ( !defined( $name ) ) {
499 define( $name, $val );
500 } elseif ( constant( $name ) !== $val ) {
501 throw new UnexpectedValueException(
502 "$name cannot be re-defined with $val it has already been set with " . constant( $name )
503 );
504 }
505 }
506
507 if ( isset( $info['autoloaderPaths'] ) ) {
508 AutoLoader::loadFiles( $info['autoloaderPaths'] );
509 }
510
511 $this->loaded += $info['credits'];
512 if ( $info['attributes'] ) {
513 if ( !$this->attributes ) {
514 $this->attributes = $info['attributes'];
515 } else {
516 $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
517 }
518 }
519
520 foreach ( $info['callbacks'] as $name => $cb ) {
521 if ( !is_callable( $cb ) ) {
522 if ( is_array( $cb ) ) {
523 $cb = '[ ' . implode( ', ', $cb ) . ' ]';
524 }
525 throw new UnexpectedValueException( "callback '$cb' is not callable" );
526 }
527 $cb( $info['credits'][$name] );
528 }
529 }
530
539 public function isLoaded( $name, $constraint = '*' ) {
540 $isLoaded = isset( $this->loaded[$name] );
541 if ( $constraint === '*' || !$isLoaded ) {
542 return $isLoaded;
543 }
544 // if a specific constraint is requested, but no version is set, throw an exception
545 if ( !isset( $this->loaded[$name]['version'] ) ) {
546 $msg = "{$name} does not expose its version, but an extension or a skin"
547 . " requires: {$constraint}.";
548 throw new LogicException( $msg );
549 }
550
551 return Semver::satisfies( $this->loaded[$name]['version'], $constraint );
552 }
553
558 public function getAttribute( $name ) {
559 if ( isset( $this->testAttributes[$name] ) ) {
560 return $this->testAttributes[$name];
561 }
562
563 if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES, true ) ) {
564 return $this->getLazyLoadedAttribute( $name );
565 }
566
567 return $this->attributes[$name] ?? [];
568 }
569
576 protected function getLazyLoadedAttribute( $name ) {
577 if ( isset( $this->testAttributes[$name] ) ) {
578 return $this->testAttributes[$name];
579 }
580 if ( isset( $this->lazyAttributes[$name] ) ) {
581 return $this->lazyAttributes[$name];
582 }
583
584 // See if it's in the cache
585 $cache = $this->getCache();
586 $key = $this->makeCacheKey( $cache, 'lazy-attrib', $name );
587 $data = $cache->get( $key );
588 if ( $data !== false ) {
589 $this->lazyAttributes[$name] = $data;
590 return $data;
591 }
592
593 $paths = [];
594 foreach ( $this->loaded as $info ) {
595 // mtime (array value) doesn't matter here since
596 // we're skipping cache, so use a dummy time
597 $paths[$info['path']] = 1;
598 }
599
600 $result = $this->readFromQueue( $paths );
601 $data = $result['attributes'][$name] ?? [];
602 $this->saveToCache( $cache, $result );
603 $this->lazyAttributes[$name] = $data;
604
605 return $data;
606 }
607
616 public function setAttributeForTest( $name, array $value ) {
617 // @codeCoverageIgnoreStart
618 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
619 throw new RuntimeException( __METHOD__ . ' can only be used in tests' );
620 }
621 // @codeCoverageIgnoreEnd
622 if ( isset( $this->testAttributes[$name] ) ) {
623 throw new Exception( "The attribute '$name' has already been overridden" );
624 }
625 $this->testAttributes[$name] = $value;
626 return new ScopedCallback( function () use ( $name ) {
627 unset( $this->testAttributes[$name] );
628 } );
629 }
630
636 public function getAllThings() {
637 return $this->loaded;
638 }
639
647 protected static function processAutoLoader( $dir, array $files ) {
648 // Make paths absolute, relative to the JSON file
649 foreach ( $files as &$file ) {
650 $file = "$dir/$file";
651 }
652 return $files;
653 }
654}
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfArrayPlus2d(array $baseArray, array $newValues)
Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
static registerClasses(array $files)
Register a file to load the given class from.
static loadFiles(array $files)
Batch version of loadFile()
static registerNamespaces(array $dirs)
Register a directory to load the classes of a given namespace from, per PSR4.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
Copyright (C) 2018 Kunal Mehta legoktm@debian.org
Utility class for loading extension manifests and aggregating their contents.
The Registry loads JSON files, and uses a Processor to extract information from them.
setLoadTestClassesAndNamespaces( $load)
Controls if classes and namespaces defined under the keys TestAutoloadClasses and TestAutoloadNamespa...
static processAutoLoader( $dir, array $files)
Fully expand autoloader paths.
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
const MERGE_STRATEGY
Special key that defines the merge strategy.
getQueue()
Get the current load queue.
getLazyLoadedAttribute( $name)
Get an attribute value that isn't cached by reading each extension.json file again.
saveToCache(BagOStuff $cache, array $data)
Save data in the cache.
const MANIFEST_VERSION
Version of the highest supported manifest version Note: Update MANIFEST_VERSION_MW_VERSION when chang...
setAttributeForTest( $name, array $value)
Force override the value of an attribute during tests.
const OLDEST_MANIFEST_VERSION
Version of the oldest supported manifest version.
array $testAttributes
Attributes for testing.
clearQueue()
Clear the current load queue.
array $lazyAttributes
Lazy-loaded attributes.
setCache(BagOStuff $cache)
Set the cache to use for extension info.
const MANIFEST_VERSION_MW_VERSION
MediaWiki version constraint representing what the current highest MANIFEST_VERSION is supported in.
int[] $queued
List of paths that should be loaded.
const MEDIAWIKI_CORE
"requires" key that applies to MediaWiki core
bool $loadTestClassesAndNamespaces
Whether test classes and namespaces should be added to the auto loader.
readFromQueue(array $queue)
Process a queue of extensions and return their extracted data.
exportExtractedData(array $info)
getAllThings()
Get credits information about all installed extensions and skins.
finish()
After this is called, no more extensions can be loaded.
array $attributes
Items in the JSON file that aren't being set as globals.
bool $checkDev
Whether to check dev-requires.
MediaWiki exception.
Executes shell commands.
Definition Shell.php:46
Provides functions to check a set of extensions with dependencies against a set of loaded extensions ...
$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.
$cache
Definition mcc.php:33
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42