Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.81% |
176 / 210 |
|
59.26% |
16 / 27 |
CRAP | |
0.00% |
0 / 1 |
ExtensionRegistry | |
83.81% |
176 / 210 |
|
59.26% |
16 / 27 |
133.30 | |
0.00% |
0 / 1 |
getInstance | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
disableForTest | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
enableForTest | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCheckDevRequires | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setLoadTestClassesAndNamespaces | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
queue | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getCache | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
makeCacheKey | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getVaryHash | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
invalidateProcessCache | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
loadFromQueue | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
saveToCache | |
46.67% |
7 / 15 |
|
0.00% |
0 / 1 |
11.46 | |||
getQueue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clearQueue | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
finish | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAbilities | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
buildVersionChecker | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
readFromQueue | |
84.62% |
22 / 26 |
|
0.00% |
0 / 1 |
10.36 | |||
exportExtractedData | |
90.91% |
50 / 55 |
|
0.00% |
0 / 1 |
28.59 | |||
isLoaded | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getAttribute | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getLazyLoadedAttribute | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
5.12 | |||
setAttributeForTest | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getAllThings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
processAutoLoader | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setSettingsBuilder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSettingsBuilder | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | use Composer\Semver\Semver; |
4 | use MediaWiki\Settings\SettingsBuilder; |
5 | use MediaWiki\Shell\Shell; |
6 | use MediaWiki\ShellDisabledError; |
7 | use MediaWiki\WikiMap\WikiMap; |
8 | use Wikimedia\ScopedCallback; |
9 | |
10 | /** |
11 | * @defgroup ExtensionRegistry ExtensionRegistry |
12 | * |
13 | * For higher level documentation, see <https://www.mediawiki.org/wiki/Manual:Extension_registration/Architecture>. |
14 | */ |
15 | |
16 | /** |
17 | * Load JSON files, and uses a Processor to extract information. |
18 | * |
19 | * This also adds the extension's classes to the AutoLoader. |
20 | * |
21 | * @ingroup ExtensionRegistry |
22 | * @since 1.25 |
23 | */ |
24 | class ExtensionRegistry { |
25 | |
26 | /** |
27 | * "requires" key that applies to MediaWiki core |
28 | */ |
29 | public const MEDIAWIKI_CORE = 'MediaWiki'; |
30 | |
31 | /** |
32 | * Version of the highest supported manifest version |
33 | * Note: Update MANIFEST_VERSION_MW_VERSION when changing this |
34 | */ |
35 | public const MANIFEST_VERSION = 2; |
36 | |
37 | /** |
38 | * MediaWiki version constraint representing what the current |
39 | * highest MANIFEST_VERSION is supported in |
40 | */ |
41 | public const MANIFEST_VERSION_MW_VERSION = '>= 1.29.0'; |
42 | |
43 | /** |
44 | * Version of the oldest supported manifest version |
45 | */ |
46 | public const OLDEST_MANIFEST_VERSION = 1; |
47 | |
48 | /** |
49 | * Bump whenever the registration cache needs resetting |
50 | */ |
51 | private const CACHE_VERSION = 8; |
52 | |
53 | private const CACHE_EXPIRY = 60 * 60 * 24; |
54 | |
55 | /** |
56 | * Special key that defines the merge strategy |
57 | * |
58 | * @since 1.26 |
59 | */ |
60 | public const MERGE_STRATEGY = '_merge_strategy'; |
61 | |
62 | /** |
63 | * Attributes that should be lazy-loaded |
64 | */ |
65 | private const LAZY_LOADED_ATTRIBUTES = [ |
66 | 'TrackingCategories', |
67 | 'QUnitTestModules', |
68 | 'SkinLessImportPaths', |
69 | ]; |
70 | |
71 | /** |
72 | * Array of loaded things, keyed by name, values are credits information. |
73 | * |
74 | * The keys that the credit info arrays may have is defined |
75 | * by ExtensionProcessor::CREDIT_ATTRIBS (plus a 'path' key that |
76 | * points to the skin or extension JSON file). |
77 | * |
78 | * This info may be accessed via ExtensionRegistry::getAllThings. |
79 | * |
80 | * @var array[] |
81 | */ |
82 | private $loaded = []; |
83 | |
84 | /** |
85 | * List of paths that should be loaded |
86 | * |
87 | * @var int[] |
88 | */ |
89 | protected $queued = []; |
90 | |
91 | /** |
92 | * Whether we are done loading things |
93 | * |
94 | * @var bool |
95 | */ |
96 | private $finished = false; |
97 | |
98 | /** |
99 | * Items in the JSON file that aren't being |
100 | * set as globals |
101 | * |
102 | * @var array |
103 | */ |
104 | protected $attributes = []; |
105 | |
106 | /** |
107 | * Attributes for testing |
108 | * |
109 | * @var array |
110 | */ |
111 | protected $testAttributes = []; |
112 | |
113 | /** |
114 | * Lazy-loaded attributes |
115 | * |
116 | * @var array |
117 | */ |
118 | protected $lazyAttributes = []; |
119 | |
120 | /** |
121 | * The hash of cache-varying options, lazy-initialised |
122 | * |
123 | * @var string|null |
124 | */ |
125 | private $varyHash; |
126 | |
127 | /** |
128 | * Whether to check dev-requires |
129 | * |
130 | * @var bool |
131 | */ |
132 | protected $checkDev = false; |
133 | |
134 | /** |
135 | * Whether test classes and namespaces should be added to the auto loader |
136 | * |
137 | * @var bool |
138 | */ |
139 | protected $loadTestClassesAndNamespaces = false; |
140 | |
141 | /** |
142 | * @var ExtensionRegistry |
143 | */ |
144 | private static $instance; |
145 | |
146 | /** |
147 | * @var ?BagOStuff |
148 | */ |
149 | private $cache = null; |
150 | |
151 | /** |
152 | * @var ?SettingsBuilder |
153 | */ |
154 | private ?SettingsBuilder $settingsBuilder = null; |
155 | |
156 | private static bool $accessDisabledForUnitTests = false; |
157 | |
158 | /** |
159 | * @codeCoverageIgnore |
160 | * @return ExtensionRegistry |
161 | */ |
162 | public static function getInstance() { |
163 | if ( self::$accessDisabledForUnitTests ) { |
164 | throw new RuntimeException( 'Access is disabled in unit tests' ); |
165 | } |
166 | if ( self::$instance === null ) { |
167 | self::$instance = new self(); |
168 | } |
169 | |
170 | return self::$instance; |
171 | } |
172 | |
173 | /** |
174 | * @internal |
175 | */ |
176 | public static function disableForTest(): void { |
177 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
178 | throw new RuntimeException( 'Can only be called in tests' ); |
179 | } |
180 | self::$accessDisabledForUnitTests = true; |
181 | } |
182 | |
183 | /** |
184 | * @internal |
185 | */ |
186 | public static function enableForTest(): void { |
187 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
188 | throw new RuntimeException( 'Can only be called in tests' ); |
189 | } |
190 | self::$accessDisabledForUnitTests = false; |
191 | } |
192 | |
193 | /** |
194 | * Set the cache to use for extension info. |
195 | * Intended for use during testing. |
196 | * |
197 | * @internal |
198 | * |
199 | * @param BagOStuff $cache |
200 | */ |
201 | public function setCache( BagOStuff $cache ): void { |
202 | $this->cache = $cache; |
203 | } |
204 | |
205 | /** |
206 | * @since 1.34 |
207 | * @param bool $check |
208 | */ |
209 | public function setCheckDevRequires( $check ) { |
210 | $this->checkDev = $check; |
211 | $this->invalidateProcessCache(); |
212 | } |
213 | |
214 | /** |
215 | * Controls if classes and namespaces defined under the keys TestAutoloadClasses and |
216 | * TestAutoloadNamespaces should be added to the autoloader. |
217 | * |
218 | * @since 1.35 |
219 | * @param bool $load |
220 | */ |
221 | public function setLoadTestClassesAndNamespaces( $load ) { |
222 | $this->loadTestClassesAndNamespaces = $load; |
223 | } |
224 | |
225 | /** |
226 | * @param string $path Absolute path to the JSON file |
227 | */ |
228 | public function queue( $path ) { |
229 | global $wgExtensionInfoMTime; |
230 | |
231 | $mtime = $wgExtensionInfoMTime; |
232 | if ( $mtime === false ) { |
233 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
234 | $mtime = @filemtime( $path ); |
235 | // @codeCoverageIgnoreStart |
236 | if ( $mtime === false ) { |
237 | $err = error_get_last(); |
238 | throw new MissingExtensionException( $path, $err['message'] ); |
239 | // @codeCoverageIgnoreEnd |
240 | } |
241 | } |
242 | $this->queued[$path] = $mtime; |
243 | $this->invalidateProcessCache(); |
244 | } |
245 | |
246 | private function getCache(): BagOStuff { |
247 | if ( !$this->cache ) { |
248 | // NOTE: Copy of ObjectCacheFactory::getDefaultKeyspace |
249 | // |
250 | // Can't call MediaWikiServices here, as we must not cause services |
251 | // to be instantiated before extensions have loaded. |
252 | global $wgCachePrefix; |
253 | $keyspace = ( is_string( $wgCachePrefix ) && $wgCachePrefix !== '' ) |
254 | ? $wgCachePrefix |
255 | : WikiMap::getCurrentWikiDbDomain()->getId(); |
256 | return ObjectCache::makeLocalServerCache( $keyspace ); |
257 | } |
258 | |
259 | return $this->cache; |
260 | } |
261 | |
262 | private function makeCacheKey( BagOStuff $cache, $component, ...$extra ) { |
263 | // Allow reusing cached ExtensionRegistry metadata between wikis (T274648) |
264 | return $cache->makeGlobalKey( |
265 | "registration-$component", |
266 | $this->getVaryHash(), |
267 | ...$extra |
268 | ); |
269 | } |
270 | |
271 | /** |
272 | * Get the cache varying hash |
273 | * |
274 | * @return string |
275 | */ |
276 | private function getVaryHash() { |
277 | if ( $this->varyHash === null ) { |
278 | // We vary the cache on the current queue (what will be or already was loaded) |
279 | // plus various versions of stuff for VersionChecker |
280 | $vary = [ |
281 | 'registration' => self::CACHE_VERSION, |
282 | 'mediawiki' => MW_VERSION, |
283 | 'abilities' => $this->getAbilities(), |
284 | 'checkDev' => $this->checkDev, |
285 | 'queue' => $this->queued, |
286 | ]; |
287 | $this->varyHash = md5( json_encode( $vary ) ); |
288 | } |
289 | return $this->varyHash; |
290 | } |
291 | |
292 | /** |
293 | * Invalidate the cache of the vary hash and the lazy options. |
294 | */ |
295 | private function invalidateProcessCache() { |
296 | $this->varyHash = null; |
297 | $this->lazyAttributes = []; |
298 | } |
299 | |
300 | public function loadFromQueue() { |
301 | if ( !$this->queued ) { |
302 | return; |
303 | } |
304 | |
305 | if ( $this->finished ) { |
306 | throw new LogicException( |
307 | "The following paths tried to load late: " |
308 | . implode( ', ', array_keys( $this->queued ) ) |
309 | ); |
310 | } |
311 | |
312 | $cache = $this->getCache(); |
313 | // See if this queue is in APC |
314 | $key = $this->makeCacheKey( $cache, 'main' ); |
315 | $data = $cache->get( $key ); |
316 | if ( !$data ) { |
317 | $data = $this->readFromQueue( $this->queued ); |
318 | $this->saveToCache( $cache, $data ); |
319 | } |
320 | $this->exportExtractedData( $data ); |
321 | } |
322 | |
323 | /** |
324 | * Save data in the cache |
325 | * |
326 | * @param BagOStuff $cache |
327 | * @param array $data |
328 | */ |
329 | protected function saveToCache( BagOStuff $cache, array $data ) { |
330 | global $wgDevelopmentWarnings; |
331 | if ( $data['warnings'] && $wgDevelopmentWarnings ) { |
332 | // If warnings were shown, don't cache it |
333 | return; |
334 | } |
335 | $lazy = []; |
336 | // Cache lazy-loaded attributes separately |
337 | foreach ( self::LAZY_LOADED_ATTRIBUTES as $attrib ) { |
338 | if ( isset( $data['attributes'][$attrib] ) ) { |
339 | $lazy[$attrib] = $data['attributes'][$attrib]; |
340 | unset( $data['attributes'][$attrib] ); |
341 | } |
342 | } |
343 | $mainKey = $this->makeCacheKey( $cache, 'main' ); |
344 | $cache->set( $mainKey, $data, self::CACHE_EXPIRY ); |
345 | foreach ( $lazy as $attrib => $value ) { |
346 | $cache->set( |
347 | $this->makeCacheKey( $cache, 'lazy-attrib', $attrib ), |
348 | $value, |
349 | self::CACHE_EXPIRY |
350 | ); |
351 | } |
352 | } |
353 | |
354 | /** |
355 | * Get the current load queue. Not intended to be used |
356 | * outside of the installer. |
357 | * |
358 | * @return int[] Map of extension.json files' modification timestamps keyed by absolute path |
359 | */ |
360 | public function getQueue() { |
361 | return $this->queued; |
362 | } |
363 | |
364 | /** |
365 | * Clear the current load queue. Not intended to be used |
366 | * outside of the installer. |
367 | */ |
368 | public function clearQueue() { |
369 | $this->queued = []; |
370 | $this->invalidateProcessCache(); |
371 | } |
372 | |
373 | /** |
374 | * After this is called, no more extensions can be loaded |
375 | * |
376 | * @since 1.29 |
377 | */ |
378 | public function finish() { |
379 | $this->finished = true; |
380 | } |
381 | |
382 | /** |
383 | * Get the list of abilities and their values |
384 | * @return bool[] |
385 | */ |
386 | private function getAbilities() { |
387 | return [ |
388 | 'shell' => !Shell::isDisabled(), |
389 | ]; |
390 | } |
391 | |
392 | /** |
393 | * Queries information about the software environment and constructs an appropriate version checker |
394 | * |
395 | * @return VersionChecker |
396 | */ |
397 | private function buildVersionChecker() { |
398 | // array to optionally specify more verbose error messages for |
399 | // missing abilities |
400 | $abilityErrors = [ |
401 | 'shell' => ( new ShellDisabledError() )->getMessage(), |
402 | ]; |
403 | |
404 | return new VersionChecker( |
405 | MW_VERSION, |
406 | PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION, |
407 | get_loaded_extensions(), |
408 | $this->getAbilities(), |
409 | $abilityErrors |
410 | ); |
411 | } |
412 | |
413 | /** |
414 | * Process a queue of extensions and return their extracted data |
415 | * |
416 | * @internal since 1.39. Extensions should use ExtensionProcessor instead. |
417 | * |
418 | * @param int[] $queue keys are filenames, values are ignored |
419 | * @return array extracted info |
420 | * @throws InvalidArgumentException |
421 | * @throws ExtensionDependencyError |
422 | */ |
423 | public function readFromQueue( array $queue ) { |
424 | $processor = new ExtensionProcessor(); |
425 | $versionChecker = $this->buildVersionChecker(); |
426 | $extDependencies = []; |
427 | $warnings = false; |
428 | foreach ( $queue as $path => $mtime ) { |
429 | $json = file_get_contents( $path ); |
430 | if ( $json === false ) { |
431 | throw new InvalidArgumentException( "Unable to read $path, does it exist?" ); |
432 | } |
433 | $info = json_decode( $json, /* $assoc = */ true ); |
434 | if ( !is_array( $info ) ) { |
435 | throw new InvalidArgumentException( "$path is not a valid JSON file." ); |
436 | } |
437 | |
438 | $version = $info['manifest_version']; |
439 | if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) { |
440 | throw new InvalidArgumentException( "$path: unsupported manifest_version: {$version}" ); |
441 | } |
442 | |
443 | // get all requirements/dependencies for this extension |
444 | $requires = $processor->getRequirements( $info, $this->checkDev ); |
445 | |
446 | // validate the information needed and add the requirements |
447 | if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) { |
448 | $extDependencies[$info['name']] = $requires; |
449 | } |
450 | |
451 | // Compatible, read and extract info |
452 | $processor->extractInfo( $path, $info, $version ); |
453 | } |
454 | $data = $processor->getExtractedInfo( $this->loadTestClassesAndNamespaces ); |
455 | $data['warnings'] = $warnings; |
456 | |
457 | // check for incompatible extensions |
458 | $incompatible = $versionChecker |
459 | ->setLoadedExtensionsAndSkins( $data['credits'] ) |
460 | ->checkArray( $extDependencies ); |
461 | |
462 | if ( $incompatible ) { |
463 | throw new ExtensionDependencyError( $incompatible ); |
464 | } |
465 | |
466 | return $data; |
467 | } |
468 | |
469 | protected function exportExtractedData( array $info ) { |
470 | foreach ( $info['globals'] as $key => $val ) { |
471 | // If a merge strategy is set, read it and remove it from the value |
472 | // so it doesn't accidentally end up getting set. |
473 | if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) { |
474 | $mergeStrategy = $val[self::MERGE_STRATEGY]; |
475 | unset( $val[self::MERGE_STRATEGY] ); |
476 | } else { |
477 | $mergeStrategy = 'array_merge'; |
478 | } |
479 | |
480 | if ( $mergeStrategy === 'provide_default' ) { |
481 | if ( !array_key_exists( $key, $GLOBALS ) ) { |
482 | $GLOBALS[$key] = $val; |
483 | } |
484 | continue; |
485 | } |
486 | |
487 | // Optimistic: If the global is not set, or is an empty array, replace it entirely. |
488 | // Will be O(1) performance. |
489 | if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) { |
490 | $GLOBALS[$key] = $val; |
491 | continue; |
492 | } |
493 | |
494 | if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) { |
495 | // config setting that has already been overridden, don't set it |
496 | continue; |
497 | } |
498 | |
499 | switch ( $mergeStrategy ) { |
500 | case 'array_merge_recursive': |
501 | $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val ); |
502 | break; |
503 | case 'array_replace_recursive': |
504 | $GLOBALS[$key] = array_replace_recursive( $val, $GLOBALS[$key] ); |
505 | break; |
506 | case 'array_plus_2d': |
507 | $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val ); |
508 | break; |
509 | case 'array_plus': |
510 | $GLOBALS[$key] += $val; |
511 | break; |
512 | case 'array_merge': |
513 | $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] ); |
514 | break; |
515 | default: |
516 | throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" ); |
517 | } |
518 | } |
519 | |
520 | if ( isset( $info['autoloaderNS'] ) ) { |
521 | AutoLoader::registerNamespaces( $info['autoloaderNS'] ); |
522 | } |
523 | |
524 | if ( isset( $info['autoloaderClasses'] ) ) { |
525 | AutoLoader::registerClasses( $info['autoloaderClasses'] ); |
526 | } |
527 | |
528 | foreach ( $info['defines'] as $name => $val ) { |
529 | if ( !defined( $name ) ) { |
530 | define( $name, $val ); |
531 | } elseif ( constant( $name ) !== $val ) { |
532 | throw new UnexpectedValueException( |
533 | "$name cannot be re-defined with $val it has already been set with " . constant( $name ) |
534 | ); |
535 | } |
536 | } |
537 | |
538 | if ( isset( $info['autoloaderPaths'] ) ) { |
539 | AutoLoader::loadFiles( $info['autoloaderPaths'] ); |
540 | } |
541 | |
542 | $this->loaded += $info['credits']; |
543 | if ( $info['attributes'] ) { |
544 | if ( !$this->attributes ) { |
545 | $this->attributes = $info['attributes']; |
546 | } else { |
547 | $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] ); |
548 | } |
549 | } |
550 | |
551 | // XXX: SettingsBuilder should really be a parameter to this method. |
552 | $settings = $this->getSettingsBuilder(); |
553 | |
554 | foreach ( $info['callbacks'] as $name => $cb ) { |
555 | if ( !is_callable( $cb ) ) { |
556 | if ( is_array( $cb ) ) { |
557 | $cb = '[ ' . implode( ', ', $cb ) . ' ]'; |
558 | } |
559 | throw new UnexpectedValueException( "callback '$cb' is not callable" ); |
560 | } |
561 | $cb( $info['credits'][$name], $settings ); |
562 | } |
563 | } |
564 | |
565 | /** |
566 | * Whether a thing has been loaded |
567 | * @param string $name |
568 | * @param string $constraint The required version constraint for this dependency |
569 | * @throws LogicException if a specific constraint is asked for, |
570 | * but the extension isn't versioned |
571 | * @return bool |
572 | */ |
573 | public function isLoaded( $name, $constraint = '*' ) { |
574 | $isLoaded = isset( $this->loaded[$name] ); |
575 | if ( $constraint === '*' || !$isLoaded ) { |
576 | return $isLoaded; |
577 | } |
578 | // if a specific constraint is requested, but no version is set, throw an exception |
579 | if ( !isset( $this->loaded[$name]['version'] ) ) { |
580 | $msg = "{$name} does not expose its version, but an extension or a skin" |
581 | . " requires: {$constraint}."; |
582 | throw new LogicException( $msg ); |
583 | } |
584 | |
585 | return Semver::satisfies( $this->loaded[$name]['version'], $constraint ); |
586 | } |
587 | |
588 | /** |
589 | * @param string $name |
590 | * @return array |
591 | */ |
592 | public function getAttribute( $name ) { |
593 | if ( isset( $this->testAttributes[$name] ) ) { |
594 | return $this->testAttributes[$name]; |
595 | } |
596 | |
597 | if ( in_array( $name, self::LAZY_LOADED_ATTRIBUTES, true ) ) { |
598 | return $this->getLazyLoadedAttribute( $name ); |
599 | } |
600 | |
601 | return $this->attributes[$name] ?? []; |
602 | } |
603 | |
604 | /** |
605 | * Get an attribute value that isn't cached by reading each |
606 | * extension.json file again |
607 | * @param string $name |
608 | * @return array |
609 | */ |
610 | protected function getLazyLoadedAttribute( $name ) { |
611 | if ( isset( $this->testAttributes[$name] ) ) { |
612 | return $this->testAttributes[$name]; |
613 | } |
614 | if ( isset( $this->lazyAttributes[$name] ) ) { |
615 | return $this->lazyAttributes[$name]; |
616 | } |
617 | |
618 | // See if it's in the cache |
619 | $cache = $this->getCache(); |
620 | $key = $this->makeCacheKey( $cache, 'lazy-attrib', $name ); |
621 | $data = $cache->get( $key ); |
622 | if ( $data !== false ) { |
623 | $this->lazyAttributes[$name] = $data; |
624 | return $data; |
625 | } |
626 | |
627 | $paths = []; |
628 | foreach ( $this->loaded as $info ) { |
629 | // mtime (array value) doesn't matter here since |
630 | // we're skipping cache, so use a dummy time |
631 | $paths[$info['path']] = 1; |
632 | } |
633 | |
634 | $result = $this->readFromQueue( $paths ); |
635 | $data = $result['attributes'][$name] ?? []; |
636 | $this->saveToCache( $cache, $result ); |
637 | $this->lazyAttributes[$name] = $data; |
638 | |
639 | return $data; |
640 | } |
641 | |
642 | /** |
643 | * Force override the value of an attribute during tests |
644 | * |
645 | * @param string $name Name of attribute to override |
646 | * @param array $value Value to set |
647 | * @return ScopedCallback to reset |
648 | * @since 1.33 |
649 | */ |
650 | public function setAttributeForTest( $name, array $value ) { |
651 | // @codeCoverageIgnoreStart |
652 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
653 | throw new LogicException( __METHOD__ . ' can only be used in tests' ); |
654 | } |
655 | // @codeCoverageIgnoreEnd |
656 | if ( isset( $this->testAttributes[$name] ) ) { |
657 | throw new InvalidArgumentException( "The attribute '$name' has already been overridden" ); |
658 | } |
659 | $this->testAttributes[$name] = $value; |
660 | return new ScopedCallback( function () use ( $name ) { |
661 | unset( $this->testAttributes[$name] ); |
662 | } ); |
663 | } |
664 | |
665 | /** |
666 | * Get credits information about all installed extensions and skins. |
667 | * |
668 | * @return array[] Keyed by component name. |
669 | */ |
670 | public function getAllThings() { |
671 | return $this->loaded; |
672 | } |
673 | |
674 | /** |
675 | * Fully expand autoloader paths |
676 | * |
677 | * @param string $dir |
678 | * @param string[] $files |
679 | * @return array |
680 | */ |
681 | protected static function processAutoLoader( $dir, array $files ) { |
682 | // Make paths absolute, relative to the JSON file |
683 | foreach ( $files as &$file ) { |
684 | $file = "$dir/$file"; |
685 | } |
686 | return $files; |
687 | } |
688 | |
689 | /** |
690 | * @internal for use by Setup. Hopefully in the future, we find a better way. |
691 | * @param SettingsBuilder $settingsBuilder |
692 | */ |
693 | public function setSettingsBuilder( SettingsBuilder $settingsBuilder ) { |
694 | $this->settingsBuilder = $settingsBuilder; |
695 | } |
696 | |
697 | private function getSettingsBuilder(): SettingsBuilder { |
698 | if ( $this->settingsBuilder === null ) { |
699 | $this->settingsBuilder = SettingsBuilder::getInstance(); |
700 | } |
701 | return $this->settingsBuilder; |
702 | } |
703 | } |