MediaWiki REL1_34
ExtensionRegistry.php
Go to the documentation of this file.
1<?php
2
3use Composer\Semver\Semver;
4use Wikimedia\AtEase\AtEase;
5use Wikimedia\ScopedCallback;
8
19
23 const MEDIAWIKI_CORE = 'MediaWiki';
24
30
35 const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0';
36
41
45 const CACHE_VERSION = 7;
46
52 const MERGE_STRATEGY = '_merge_strategy';
53
59 private $loaded = [];
60
66 protected $queued = [];
67
73 private $finished = false;
74
81 protected $attributes = [];
82
88 protected $testAttributes = [];
89
95 protected $checkDev = false;
96
100 private static $instance;
101
106 public static function getInstance() {
107 if ( self::$instance === null ) {
108 self::$instance = new self();
109 }
110
111 return self::$instance;
112 }
113
118 public function setCheckDevRequires( $check ) {
119 $this->checkDev = $check;
120 }
121
125 public function queue( $path ) {
127
128 $mtime = $wgExtensionInfoMTime;
129 if ( $mtime === false ) {
130 AtEase::suppressWarnings();
131 $mtime = filemtime( $path );
132 AtEase::restoreWarnings();
133 // @codeCoverageIgnoreStart
134 if ( $mtime === false ) {
135 $err = error_get_last();
136 throw new Exception( "Unable to open file $path: {$err['message']}" );
137 // @codeCoverageIgnoreEnd
138 }
139 }
140 $this->queued[$path] = $mtime;
141 }
142
147 public function loadFromQueue() {
149 if ( !$this->queued ) {
150 return;
151 }
152
153 if ( $this->finished ) {
154 throw new MWException(
155 "The following paths tried to load late: "
156 . implode( ', ', array_keys( $this->queued ) )
157 );
158 }
159
160 // A few more things to vary the cache on
161 $versions = [
162 'registration' => self::CACHE_VERSION,
163 'mediawiki' => $wgVersion,
164 'abilities' => $this->getAbilities(),
165 'checkDev' => $this->checkDev,
166 ];
167
168 // We use a try/catch because we don't want to fail here
169 // if $wgObjectCaches is not configured properly for APC setup
170 try {
171 // Avoid MediaWikiServices to prevent instantiating it before extensions have loaded
172 $cacheId = ObjectCache::detectLocalServerCache();
173 $cache = ObjectCache::newFromParams( $wgObjectCaches[$cacheId] );
174 } catch ( InvalidArgumentException $e ) {
175 $cache = new EmptyBagOStuff();
176 }
177 // See if this queue is in APC
178 $key = $cache->makeKey(
179 'registration',
180 md5( json_encode( $this->queued + $versions ) )
181 );
182 $data = $cache->get( $key );
183 if ( $data ) {
184 $this->exportExtractedData( $data );
185 } else {
186 $data = $this->readFromQueue( $this->queued );
187 $this->exportExtractedData( $data );
188 // Do this late since we don't want to extract it since we already
189 // did that, but it should be cached
190 $data['globals']['wgAutoloadClasses'] += $data['autoload'];
191 unset( $data['autoload'] );
192 if ( !( $data['warnings'] && $wgDevelopmentWarnings ) ) {
193 // If there were no warnings that were shown, cache it
194 $cache->set( $key, $data, 60 * 60 * 24 );
195 }
196 }
197 $this->queued = [];
198 }
199
206 public function getQueue() {
207 return $this->queued;
208 }
209
214 public function clearQueue() {
215 $this->queued = [];
216 }
217
223 public function finish() {
224 $this->finished = true;
225 }
226
231 private function getAbilities() {
232 return [
233 'shell' => !Shell::isDisabled(),
234 ];
235 }
236
242 private function buildVersionChecker() {
243 global $wgVersion;
244 // array to optionally specify more verbose error messages for
245 // missing abilities
246 $abilityErrors = [
247 'shell' => ( new ShellDisabledError() )->getMessage(),
248 ];
249
250 return new VersionChecker(
252 PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
253 get_loaded_extensions(),
254 $this->getAbilities(),
255 $abilityErrors
256 );
257 }
258
267 public function readFromQueue( array $queue ) {
268 $autoloadClasses = [];
269 $autoloadNamespaces = [];
270 $autoloaderPaths = [];
271 $processor = new ExtensionProcessor();
272 $versionChecker = $this->buildVersionChecker();
273 $extDependencies = [];
274 $incompatible = [];
275 $warnings = false;
276 foreach ( $queue as $path => $mtime ) {
277 $json = file_get_contents( $path );
278 if ( $json === false ) {
279 throw new Exception( "Unable to read $path, does it exist?" );
280 }
281 $info = json_decode( $json, /* $assoc = */ true );
282 if ( !is_array( $info ) ) {
283 throw new Exception( "$path is not a valid JSON file." );
284 }
285
286 if ( !isset( $info['manifest_version'] ) ) {
288 "{$info['name']}'s extension.json or skin.json does not have manifest_version",
289 '1.29'
290 );
291 $warnings = true;
292 // For backwards-compatability, assume a version of 1
293 $info['manifest_version'] = 1;
294 }
295 $version = $info['manifest_version'];
296 if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
297 $incompatible[] = "$path: unsupported manifest_version: {$version}";
298 }
299
300 $dir = dirname( $path );
301 self::exportAutoloadClassesAndNamespaces(
302 $dir,
303 $info,
304 $autoloadClasses,
305 $autoloadNamespaces
306 );
307
308 // get all requirements/dependencies for this extension
309 $requires = $processor->getRequirements( $info, $this->checkDev );
310
311 // validate the information needed and add the requirements
312 if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
313 $extDependencies[$info['name']] = $requires;
314 }
315
316 // Get extra paths for later inclusion
317 $autoloaderPaths = array_merge( $autoloaderPaths,
318 $processor->getExtraAutoloaderPaths( $dir, $info ) );
319 // Compatible, read and extract info
320 $processor->extractInfo( $path, $info, $version );
321 }
322 $data = $processor->getExtractedInfo();
323 $data['warnings'] = $warnings;
324
325 // check for incompatible extensions
326 $incompatible = array_merge(
327 $incompatible,
328 $versionChecker
329 ->setLoadedExtensionsAndSkins( $data['credits'] )
330 ->checkArray( $extDependencies )
331 );
332
333 if ( $incompatible ) {
334 throw new ExtensionDependencyError( $incompatible );
335 }
336
337 // Need to set this so we can += to it later
338 $data['globals']['wgAutoloadClasses'] = [];
339 $data['autoload'] = $autoloadClasses;
340 $data['autoloaderPaths'] = $autoloaderPaths;
341 $data['autoloaderNS'] = $autoloadNamespaces;
342 return $data;
343 }
344
354 $dir, $info, &$autoloadClasses = [], &$autoloadNamespaces = []
355 ) {
356 if ( isset( $info['AutoloadClasses'] ) ) {
357 $autoload = self::processAutoLoader( $dir, $info['AutoloadClasses'] );
358 $GLOBALS['wgAutoloadClasses'] += $autoload;
359 $autoloadClasses += $autoload;
360 }
361 if ( isset( $info['AutoloadNamespaces'] ) ) {
362 $autoloadNamespaces += self::processAutoLoader( $dir, $info['AutoloadNamespaces'] );
363 AutoLoader::$psr4Namespaces += $autoloadNamespaces;
364 }
365 }
366
367 protected function exportExtractedData( array $info ) {
368 foreach ( $info['globals'] as $key => $val ) {
369 // If a merge strategy is set, read it and remove it from the value
370 // so it doesn't accidentally end up getting set.
371 if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
372 $mergeStrategy = $val[self::MERGE_STRATEGY];
373 unset( $val[self::MERGE_STRATEGY] );
374 } else {
375 $mergeStrategy = 'array_merge';
376 }
377
378 // Optimistic: If the global is not set, or is an empty array, replace it entirely.
379 // Will be O(1) performance.
380 if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
381 $GLOBALS[$key] = $val;
382 continue;
383 }
384
385 if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
386 // config setting that has already been overridden, don't set it
387 continue;
388 }
389
390 switch ( $mergeStrategy ) {
391 case 'array_merge_recursive':
392 $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
393 break;
394 case 'array_replace_recursive':
395 $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
396 break;
397 case 'array_plus_2d':
398 $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
399 break;
400 case 'array_plus':
401 $GLOBALS[$key] += $val;
402 break;
403 case 'array_merge':
404 $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
405 break;
406 default:
407 throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
408 }
409 }
410
411 if ( isset( $info['autoloaderNS'] ) ) {
412 AutoLoader::$psr4Namespaces += $info['autoloaderNS'];
413 }
414
415 foreach ( $info['defines'] as $name => $val ) {
416 define( $name, $val );
417 }
418 foreach ( $info['autoloaderPaths'] as $path ) {
419 if ( file_exists( $path ) ) {
420 require_once $path;
421 }
422 }
423
424 $this->loaded += $info['credits'];
425 if ( $info['attributes'] ) {
426 if ( !$this->attributes ) {
427 $this->attributes = $info['attributes'];
428 } else {
429 $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
430 }
431 }
432
433 foreach ( $info['callbacks'] as $name => $cb ) {
434 if ( !is_callable( $cb ) ) {
435 if ( is_array( $cb ) ) {
436 $cb = '[ ' . implode( ', ', $cb ) . ' ]';
437 }
438 throw new UnexpectedValueException( "callback '$cb' is not callable" );
439 }
440 $cb( $info['credits'][$name] );
441 }
442 }
443
453 public function load( $path ) {
454 wfDeprecated( __METHOD__, '1.34' );
455 $this->loadFromQueue(); // First clear the queue
456 $this->queue( $path );
457 $this->loadFromQueue();
458 }
459
468 public function isLoaded( $name, $constraint = '*' ) {
469 $isLoaded = isset( $this->loaded[$name] );
470 if ( $constraint === '*' || !$isLoaded ) {
471 return $isLoaded;
472 }
473 // if a specific constraint is requested, but no version is set, throw an exception
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 );
478 }
479
480 return SemVer::satisfies( $this->loaded[$name]['version'], $constraint );
481 }
482
487 public function getAttribute( $name ) {
488 return $this->testAttributes[$name] ??
489 $this->attributes[$name] ?? [];
490 }
491
500 public function setAttributeForTest( $name, array $value ) {
501 // @codeCoverageIgnoreStart
502 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
503 throw new RuntimeException( __METHOD__ . ' can only be used in tests' );
504 }
505 // @codeCoverageIgnoreEnd
506 if ( isset( $this->testAttributes[$name] ) ) {
507 throw new Exception( "The attribute '$name' has already been overridden" );
508 }
509 $this->testAttributes[$name] = $value;
510 return new ScopedCallback( function () use ( $name ) {
511 unset( $this->testAttributes[$name] );
512 } );
513 }
514
520 public function getAllThings() {
521 return $this->loaded;
522 }
523
531 protected static function processAutoLoader( $dir, array $files ) {
532 // Make paths absolute, relative to the JSON file
533 foreach ( $files as &$file ) {
534 $file = "$dir/$file";
535 }
536 return $files;
537 }
538}
$GLOBALS['IP']
$wgObjectCaches
Advanced object cache configuration.
int bool $wgExtensionInfoMTime
When loading extensions through the extension registration system, this can be used to invalidate the...
$wgVersion
MediaWiki version number.
$wgDevelopmentWarnings
If set to true MediaWiki will throw notices for some possible error conditions and for deprecated fun...
wfArrayPlus2d(array $baseArray, array $newValues)
Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static string[] $psr4Namespaces
A BagOStuff object with no objects in it.
Copyright (C) 2018 Kunal Mehta legoktm@member.fsf.org
ExtensionRegistry class.
static processAutoLoader( $dir, array $files)
Fully expand autoloader paths.
isLoaded( $name, $constraint=' *')
Whether a thing has been loaded.
array $queued
List of paths that should be loaded.
const MERGE_STRATEGY
Special key that defines the merge strategy.
getQueue()
Get the current load queue.
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.
getAbilities()
Get the list of abilities and their values.
array $loaded
Array of loaded things, keyed by name, values are credits information.
const CACHE_VERSION
Bump whenever the registration cache needs resetting.
array $testAttributes
Attributes for testing.
clearQueue()
Clear the current load queue.
static exportAutoloadClassesAndNamespaces( $dir, $info, &$autoloadClasses=[], &$autoloadNamespaces=[])
Export autoload classes and namespaces for a given directory and parsed JSON info file.
static ExtensionRegistry $instance
const MANIFEST_VERSION_MW_VERSION
MediaWiki version constraint representing what the current highest MANIFEST_VERSION is supported in.
load( $path)
Loads and processes the given JSON file without delay.
const MEDIAWIKI_CORE
"requires" key that applies to MediaWiki core/$wgVersion
readFromQueue(array $queue)
Process a queue of extensions and return their extracted data.
exportExtractedData(array $info)
bool $finished
Whether we are done loading things.
getAllThings()
Get information about all things.
finish()
After this is called, no more extensions can be loaded.
buildVersionChecker()
Queries information about the software environment and constructs an appropiate version checker.
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:44
Provides functions to check a set of extensions with dependencies against a set of loaded extensions ...
$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