Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.65% |
146 / 193 |
|
65.79% |
25 / 38 |
CRAP | |
0.00% |
0 / 1 |
SettingsBuilder | |
75.65% |
146 / 193 |
|
65.79% |
25 / 38 |
165.86 | |
0.00% |
0 / 1 |
getInstance | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
disableAccessForUnitTests | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
enableAccessAfterUnitTests | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
__construct | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
load | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadArrayInternal | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
loadFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fileExists | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
wrapSource | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
makeSource | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
validate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
detectDeprecatedConfig | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
detectObsoleteConfig | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getDefaultConfig | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getConfigSchema | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDefinedConfigKeys | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
apply | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
loadRecursive | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
updateSettingsConfig | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
assumeDirtyConfig | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
applySchemas | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
6 | |||
applySettings | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
13 | |||
putConfigValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
putConfigValues | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
overrideConfigValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
overrideConfigValues | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
registerHookHandlers | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
registerHookHandler | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConfig | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
reset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
assertNotReadOnly | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
assertStillLoading | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
enterReadOnlyStage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
enterRegistrationStage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getConfigBuilder | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
warning | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getWarnings | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Settings; |
4 | |
5 | use MediaWiki\Config\Config; |
6 | use MediaWiki\Config\HashConfig; |
7 | use MediaWiki\Config\IterableConfig; |
8 | use MediaWiki\HookContainer\HookContainer; |
9 | use MediaWiki\MainConfigNames; |
10 | use MediaWiki\Registration\ExtensionRegistry; |
11 | use MediaWiki\Settings\Cache\CacheableSource; |
12 | use MediaWiki\Settings\Cache\CachedSource; |
13 | use MediaWiki\Settings\Config\ConfigBuilder; |
14 | use MediaWiki\Settings\Config\ConfigSchema; |
15 | use MediaWiki\Settings\Config\ConfigSchemaAggregator; |
16 | use MediaWiki\Settings\Config\GlobalConfigBuilder; |
17 | use MediaWiki\Settings\Config\PhpIniSink; |
18 | use MediaWiki\Settings\Source\ArraySource; |
19 | use MediaWiki\Settings\Source\FileSource; |
20 | use MediaWiki\Settings\Source\SettingsFileUtils; |
21 | use MediaWiki\Settings\Source\SettingsIncludeLocator; |
22 | use MediaWiki\Settings\Source\SettingsSource; |
23 | use RuntimeException; |
24 | use StatusValue; |
25 | use Wikimedia\ObjectCache\BagOStuff; |
26 | use function array_key_exists; |
27 | |
28 | /** |
29 | * Builder class for constructing a Config object from a set of sources |
30 | * during bootstrap. The SettingsBuilder is used in Setup.php to load |
31 | * and combine settings files and eventually produce the Config object that |
32 | * will be used to configure MediaWiki. |
33 | * |
34 | * The SettingsBuilder object keeps track of "stages" of initialization that |
35 | * correspond to sections of Setup.php: |
36 | * |
37 | * The initial stage is "loading". In this stage, SettingsSources are added |
38 | * to the SettingsBuilder using the load* methods. This sets up the config |
39 | * schema and applies custom configuration values. |
40 | * |
41 | * Once all settings sources have been loaded, the SettingsBuilder is moved to the |
42 | * "registration" stage by calling enterRegistrationStage(). |
43 | * In this stage, config values may still be altered, but no settings sources may |
44 | * be loaded. During the "registration" stage, dynamic defaults are applied, |
45 | * extension registration callbacks are executed, and maintenance scripts have an |
46 | * opportunity to manipulate settings. |
47 | * |
48 | * Finally, the SettingsBuilder is moved to the "operation" stage by calling |
49 | * enterOperationStage(). This renders the SettingsBuilder read only: config values |
50 | * may no longer be changed. At this point, it becomes safe to use the Config object |
51 | * returned by getConfig() to initialize the service container. |
52 | * |
53 | * @since 1.38 |
54 | */ |
55 | class SettingsBuilder { |
56 | |
57 | /** |
58 | * @var int The initial stage in which settings can be loaded, |
59 | * but config values cannot be accessed. |
60 | */ |
61 | private const STAGE_LOADING = 1; |
62 | |
63 | /** |
64 | * @var int The intermediate stage in which settings can no longer be loaded, |
65 | * but config values can be accessed and manipulated programmatically. |
66 | */ |
67 | private const STAGE_REGISTRATION = 10; |
68 | |
69 | /** |
70 | * @var int The final stage in which config values can be accessed, but can |
71 | * no longer be changed. |
72 | */ |
73 | private const STAGE_READ_ONLY = 100; |
74 | |
75 | /** @var string */ |
76 | private $baseDir; |
77 | |
78 | /** @var ExtensionRegistry */ |
79 | private $extensionRegistry; |
80 | |
81 | /** @var BagOStuff */ |
82 | private $cache; |
83 | |
84 | /** @var ConfigBuilder */ |
85 | private $configSink; |
86 | |
87 | /** @var array<string,string> */ |
88 | private $obsoleteConfig; |
89 | |
90 | /** @var Config|null */ |
91 | private $config; |
92 | |
93 | /** @var SettingsSource[] */ |
94 | private $currentBatch; |
95 | |
96 | /** @var ConfigSchemaAggregator */ |
97 | private $configSchema; |
98 | |
99 | /** @var PhpIniSink */ |
100 | private $phpIniSink; |
101 | |
102 | /** |
103 | * Configuration that applies to SettingsBuilder itself. |
104 | * Initialized by the constructor, may be overwritten by regular |
105 | * config values. Merge strategies are currently not implemented |
106 | * but can be added if needed. |
107 | * |
108 | * @var array |
109 | */ |
110 | private $settingsConfig; |
111 | |
112 | /** |
113 | * The stage of the settings builder. This is used to determine |
114 | * which settings are allowed to be changed. |
115 | * |
116 | * @var int see self::STAGE_* |
117 | */ |
118 | private $stage = self::STAGE_LOADING; |
119 | |
120 | /** |
121 | * Whether we have to apply reverse-merging when applying defaults. |
122 | * This will initially be false, and become true once any config settings have been |
123 | * assigned a value. |
124 | * |
125 | * This is used as an optimization, to avoid costly merge logic when loading initial |
126 | * defaults before any config variables have been set. |
127 | * |
128 | * @var bool |
129 | */ |
130 | private $defaultsNeedMerging = false; |
131 | |
132 | /** @var string[] */ |
133 | private $warnings = []; |
134 | |
135 | private static bool $accessDisabledForUnitTests = false; |
136 | |
137 | /** |
138 | * Accessor for the global SettingsBuilder instance. |
139 | * |
140 | * @note It is always preferable to have a SettingsBuilder injected! |
141 | * But as long as we can't to this everywhere, this is the preferred way of |
142 | * getting the global instance of SettingsBuilder. |
143 | * |
144 | * @return SettingsBuilder |
145 | */ |
146 | public static function getInstance(): self { |
147 | static $instance = null; |
148 | |
149 | if ( self::$accessDisabledForUnitTests ) { |
150 | throw new RuntimeException( 'Access is disabled in unit tests' ); |
151 | } |
152 | |
153 | if ( !$instance ) { |
154 | // NOTE: SettingsBuilder is used during bootstrap, before MediaWikiServices |
155 | // is available. It has to be, because it is used to construct the |
156 | // configuration that is used when constructing services. Because of |
157 | // this, we have to instantiate SettingsBuilder directly, we can't |
158 | // use service wiring. |
159 | $instance = new SettingsBuilder( |
160 | MW_INSTALL_PATH, |
161 | ExtensionRegistry::getInstance(), |
162 | new GlobalConfigBuilder( 'wg' ), |
163 | new PhpIniSink() |
164 | ); |
165 | } |
166 | |
167 | return $instance; |
168 | } |
169 | |
170 | /** |
171 | * @internal |
172 | */ |
173 | public static function disableAccessForUnitTests(): void { |
174 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
175 | throw new RuntimeException( 'Can only be called in tests' ); |
176 | } |
177 | self::$accessDisabledForUnitTests = true; |
178 | } |
179 | |
180 | /** |
181 | * @internal |
182 | */ |
183 | public static function enableAccessAfterUnitTests(): void { |
184 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
185 | throw new RuntimeException( 'Can only be called in tests' ); |
186 | } |
187 | self::$accessDisabledForUnitTests = false; |
188 | } |
189 | |
190 | /** |
191 | * @param string $baseDir |
192 | * @param ExtensionRegistry $extensionRegistry |
193 | * @param ConfigBuilder $configSink |
194 | * @param PhpIniSink $phpIniSink |
195 | * @param BagOStuff|null $cache BagOStuff used to cache settings loaded |
196 | * from each source. The caller should beware that secrets contained in |
197 | * any source passed to {@link load} or {@link loadFile} will be cached as |
198 | * well. |
199 | */ |
200 | public function __construct( |
201 | string $baseDir, |
202 | ExtensionRegistry $extensionRegistry, |
203 | ConfigBuilder $configSink, |
204 | PhpIniSink $phpIniSink, |
205 | ?BagOStuff $cache = null |
206 | ) { |
207 | $this->baseDir = $baseDir; |
208 | $this->extensionRegistry = $extensionRegistry; |
209 | $this->cache = $cache; |
210 | $this->configSink = $configSink; |
211 | $this->obsoleteConfig = []; |
212 | $this->configSchema = new ConfigSchemaAggregator(); |
213 | $this->phpIniSink = $phpIniSink; |
214 | $this->settingsConfig = [ |
215 | MainConfigNames::ExtensionDirectory => "$baseDir/extensions", |
216 | MainConfigNames::StyleDirectory => "$baseDir/skins", |
217 | ]; |
218 | $this->reset(); |
219 | } |
220 | |
221 | /** |
222 | * Load settings from a {@link SettingsSource}. |
223 | * Only allowed during the "loading" stage. |
224 | * |
225 | * @param SettingsSource $source |
226 | * @return $this |
227 | */ |
228 | public function load( SettingsSource $source ): self { |
229 | $this->assertStillLoading( __METHOD__ ); |
230 | |
231 | // XXX: We may want to cache the entire batch instead, see T304493. |
232 | $this->currentBatch[] = $this->wrapSource( $source ); |
233 | |
234 | return $this; |
235 | } |
236 | |
237 | /** |
238 | * Load settings from an array. |
239 | * |
240 | * @param array $newSettings |
241 | * |
242 | * @return $this |
243 | */ |
244 | public function loadArray( array $newSettings ): self { |
245 | return $this->load( new ArraySource( $newSettings ) ); |
246 | } |
247 | |
248 | /** |
249 | * Load settings from an array. |
250 | * For internal use. Allowed during "loading" and "registration" stage. |
251 | * |
252 | * @param array $newSettings |
253 | * @param string $func |
254 | * |
255 | * @return $this |
256 | */ |
257 | private function loadArrayInternal( array $newSettings, string $func ): self { |
258 | $this->assertNotReadOnly( $func ); |
259 | |
260 | $source = new ArraySource( $newSettings ); |
261 | $this->currentBatch[] = $this->wrapSource( $source ); |
262 | |
263 | return $this; |
264 | } |
265 | |
266 | /** |
267 | * Load settings from a file. |
268 | * |
269 | * @param string $path |
270 | * @return $this |
271 | */ |
272 | public function loadFile( string $path ): self { |
273 | return $this->load( $this->makeSource( $path ) ); |
274 | } |
275 | |
276 | /** |
277 | * Checks whether the given file exists relative to the settings builder's |
278 | * base directory. |
279 | * |
280 | * @param string $path |
281 | * @return bool |
282 | */ |
283 | public function fileExists( string $path ): bool { |
284 | $path = SettingsFileUtils::resolveRelativeLocation( $path, $this->baseDir ); |
285 | return file_exists( $path ); |
286 | } |
287 | |
288 | /** |
289 | * @param SettingsSource $source |
290 | * |
291 | * @return SettingsSource |
292 | */ |
293 | private function wrapSource( SettingsSource $source ): SettingsSource { |
294 | if ( $this->cache !== null && $source instanceof CacheableSource ) { |
295 | $source = new CachedSource( $this->cache, $source ); |
296 | } |
297 | return $source; |
298 | } |
299 | |
300 | /** |
301 | * @param string $location |
302 | * @return SettingsSource |
303 | */ |
304 | private function makeSource( $location ): SettingsSource { |
305 | // NOTE: Currently, files are the only kind of location, but we could add others. |
306 | // The set of supported source locations will be hard-coded here. |
307 | // Custom SettingsSource would have to be instantiated directly and passed to load(). |
308 | $path = SettingsFileUtils::resolveRelativeLocation( $location, $this->baseDir ); |
309 | |
310 | return $this->wrapSource( new FileSource( $path ) ); |
311 | } |
312 | |
313 | /** |
314 | * Assert that the config loaded so far conforms the schema loaded so far. |
315 | * |
316 | * @note this is slow, so you probably don't want to do this on every request. |
317 | * |
318 | * @return StatusValue |
319 | */ |
320 | public function validate(): StatusValue { |
321 | $config = $this->getConfig(); |
322 | return $this->configSchema->validateConfig( $config ); |
323 | } |
324 | |
325 | /** |
326 | * Detect usage of deprecated settings. A setting is counted as used if |
327 | * it has a value other than the default. Note that deprecated settings are |
328 | * expected to be supported. Settings that have become non-functional should |
329 | * be marked as obsolete instead. |
330 | * |
331 | * @note this is slow, so you probably don't want to do this on every request. |
332 | * @note Code that needs to call detectDeprecatedConfig() should probably also |
333 | * call detectObsoleteConfig() and getWarnings(). |
334 | * |
335 | * @return array<string,string> an associative array mapping config keys |
336 | * to the deprecation messages from the schema. |
337 | */ |
338 | public function detectDeprecatedConfig(): array { |
339 | $config = $this->getConfig(); |
340 | $keys = $this->getDefinedConfigKeys(); |
341 | $deprecated = []; |
342 | |
343 | foreach ( $keys as $key ) { |
344 | $sch = $this->configSchema->getSchemaFor( $key ); |
345 | if ( !isset( $sch['deprecated'] ) ) { |
346 | continue; |
347 | } |
348 | |
349 | $default = $sch['default'] ?? null; |
350 | $value = $config->get( $key ); |
351 | |
352 | if ( $value !== $default ) { |
353 | $deprecated[$key] = $sch['deprecated']; |
354 | } |
355 | } |
356 | |
357 | return $deprecated; |
358 | } |
359 | |
360 | /** |
361 | * Detect usage of obsolete settings. A setting is counted as used if it is |
362 | * defined in any way. Note that obsolete settings are non-functional, while |
363 | * deprecated settings are still supported. |
364 | * |
365 | * @note this is slow, so you probably don't want to do this on every request. |
366 | * @note Code that calls detectObsoleteConfig() may also want to |
367 | * call detectDeprecatedConfig() and getWarnings(). |
368 | * |
369 | * @return array<string,string> an associative array mapping config keys |
370 | * to the deprecation messages from the schema. |
371 | */ |
372 | public function detectObsoleteConfig(): array { |
373 | $config = $this->getConfig(); |
374 | $obsolete = []; |
375 | |
376 | foreach ( $this->obsoleteConfig as $key => $msg ) { |
377 | if ( $config->has( $key ) ) { |
378 | $obsolete[$key] = $msg; |
379 | } |
380 | } |
381 | |
382 | return $obsolete; |
383 | } |
384 | |
385 | /** |
386 | * Return a Config object with default for all settings from all schemas loaded so far. |
387 | * If the schema for a setting doesn't specify a default, null is assumed. |
388 | * |
389 | * @note This will implicitly call apply() |
390 | * |
391 | * @return IterableConfig |
392 | */ |
393 | public function getDefaultConfig(): IterableConfig { |
394 | $this->apply(); |
395 | $defaults = $this->configSchema->getDefaults(); |
396 | $nulls = array_fill_keys( $this->configSchema->getDefinedKeys(), null ); |
397 | |
398 | return new HashConfig( array_merge( $nulls, $defaults ) ); |
399 | } |
400 | |
401 | /** |
402 | * Return the configuration schema. |
403 | * |
404 | * @note This will implicitly call apply() |
405 | * |
406 | * @return ConfigSchema |
407 | */ |
408 | public function getConfigSchema(): ConfigSchema { |
409 | $this->apply(); |
410 | return $this->configSchema; |
411 | } |
412 | |
413 | /** |
414 | * Returns the names of all defined configuration variables |
415 | * |
416 | * @return string[] |
417 | */ |
418 | public function getDefinedConfigKeys(): array { |
419 | $this->apply(); |
420 | return $this->configSchema->getDefinedKeys(); |
421 | } |
422 | |
423 | /** |
424 | * Apply any settings loaded so far to the runtime environment. |
425 | * |
426 | * @note This usually makes all configuration available in global variables. |
427 | * This may however not be the case in the future. |
428 | * |
429 | * @return $this |
430 | * @throws SettingsBuilderException |
431 | */ |
432 | public function apply(): self { |
433 | if ( !$this->currentBatch ) { |
434 | return $this; |
435 | } |
436 | |
437 | $this->assertNotReadOnly( __METHOD__ ); |
438 | $this->config = null; |
439 | |
440 | // XXX: We may want to cache the entire batch after merging together |
441 | // settings from all sources, see T304493. |
442 | $allSettings = $this->loadRecursive( $this->currentBatch ); |
443 | |
444 | foreach ( $allSettings as $settings ) { |
445 | $this->applySettings( $settings ); |
446 | } |
447 | $this->reset(); |
448 | return $this; |
449 | } |
450 | |
451 | /** |
452 | * Loads all sources in the current batch, recursively resolving includes. |
453 | * |
454 | * @param SettingsSource[] $batch The batch of sources to load |
455 | * @param string[] $stack The current stack of includes, for cycle detection |
456 | * |
457 | * @return array[] an array of settings arrays |
458 | */ |
459 | private function loadRecursive( array $batch, array $stack = [] ): array { |
460 | $allSettings = []; |
461 | |
462 | // Depth-first traversal of settings sources. |
463 | foreach ( $batch as $source ) { |
464 | $sourceName = (string)$source; |
465 | |
466 | if ( in_array( $sourceName, $stack ) ) { |
467 | throw new SettingsBuilderException( |
468 | 'Recursive include chain detected: ' . implode( ', ', $stack ) |
469 | ); |
470 | } |
471 | |
472 | $settings = $source->load(); |
473 | $settings['source-name'] = $sourceName; |
474 | |
475 | $allSettings[] = $settings; |
476 | |
477 | $nextBatch = []; |
478 | foreach ( $settings['includes'] ?? [] as $location ) { |
479 | // Try to resolve the include relative to the source, |
480 | // if the source supports that. |
481 | if ( $source instanceof SettingsIncludeLocator ) { |
482 | $location = $source->locateInclude( $location ); |
483 | } |
484 | |
485 | $nextBatch[] = $this->makeSource( $location ); |
486 | } |
487 | |
488 | $nextStack = array_merge( $stack, [ $settings['source-name'] ] ); |
489 | $nextSettings = $this->loadRecursive( $nextBatch, $nextStack ); |
490 | $allSettings = array_merge( $allSettings, $nextSettings ); |
491 | } |
492 | |
493 | return $allSettings; |
494 | } |
495 | |
496 | /** |
497 | * Updates config settings relevant to the behavior if SettingsBuilder itself. |
498 | * |
499 | * @param array $config |
500 | * |
501 | * @return string |
502 | */ |
503 | private function updateSettingsConfig( $config ): string { |
504 | // No merge strategies are applied, defaults are set in the constructor. |
505 | foreach ( $this->settingsConfig as $key => $dummy ) { |
506 | if ( array_key_exists( $key, $config ) ) { |
507 | $this->settingsConfig[ $key ] = $config[ $key ]; |
508 | } |
509 | } |
510 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable,PhanPossiblyUndeclaredVariable Always set |
511 | return $key; |
512 | } |
513 | |
514 | /** |
515 | * Notify SettingsBuilder that it can no longer assume that is has full knowledge of |
516 | * all configuration variables that have been set. This would be the case when other code |
517 | * (such as LocalSettings.php) is manipulating global variables which represent config |
518 | * values. |
519 | * |
520 | * This is used for optimization: up until this method is called, default values can be set |
521 | * directly for any config values that have not been set yet. This avoids the need to |
522 | * run merge logic for all default values during initialization. |
523 | * |
524 | * @note It is useful to call apply() just before this method, so any settings already queued |
525 | * will still benefit from assuming that globals are not dirty. |
526 | * |
527 | * @return self |
528 | */ |
529 | public function assumeDirtyConfig(): SettingsBuilder { |
530 | $this->defaultsNeedMerging = true; |
531 | return $this; |
532 | } |
533 | |
534 | /** |
535 | * Apply schemas from the settings array. |
536 | * |
537 | * This returns the default values to apply, splits into two two categories: |
538 | * "hard" defaults, which can be applied as config overrides without merging. |
539 | * And "soft" defaults, which have to be reverse-merged. |
540 | * Defaults can be considered "hard" if no config value was yet set for them. However, |
541 | * we can only know that as long as we can be sure that nothing has changed config values |
542 | * in a way that bypasses SettingsLoader (e.g. by setting global variables in LocalSettings.php). |
543 | * |
544 | * @param array $settings A settings structure. |
545 | */ |
546 | private function applySchemas( array $settings ) { |
547 | $defaults = []; |
548 | |
549 | if ( isset( $settings['config-schema-inverse'] ) ) { |
550 | $defaults = $settings['config-schema-inverse']['default'] ?? []; |
551 | $this->configSchema->addDefaults( |
552 | $defaults, |
553 | $settings['source-name'] |
554 | ); |
555 | $this->configSchema->addMergeStrategies( |
556 | $settings['config-schema-inverse']['mergeStrategy'] ?? [], |
557 | $settings['source-name'] |
558 | ); |
559 | $this->configSchema->addTypes( |
560 | $settings['config-schema-inverse']['type'] ?? [], |
561 | $settings['source-name'] |
562 | ); |
563 | $this->configSchema->addDynamicDefaults( |
564 | $settings['config-schema-inverse']['dynamicDefault'] ?? [], |
565 | $settings['source-name'] |
566 | ); |
567 | } |
568 | |
569 | if ( isset( $settings['config-schema'] ) ) { |
570 | foreach ( $settings['config-schema'] as $key => $schema ) { |
571 | $this->configSchema->addSchema( $key, $schema ); |
572 | |
573 | if ( $this->configSchema->hasDefaultFor( $key ) ) { |
574 | $defaults[$key] = $this->configSchema->getDefaultFor( $key ); |
575 | } |
576 | } |
577 | } |
578 | |
579 | if ( $this->defaultsNeedMerging ) { |
580 | $mergeStrategies = $this->configSchema->getMergeStrategies(); |
581 | $this->configSink->setMultiDefault( $defaults, $mergeStrategies ); |
582 | } else { |
583 | // Optimization: no merge strategy, just override in one go |
584 | $this->configSink->setMulti( $defaults ); |
585 | } |
586 | } |
587 | |
588 | /** |
589 | * Apply the settings array. |
590 | * |
591 | * @param array $settings |
592 | */ |
593 | private function applySettings( array $settings ) { |
594 | // First extract config variables that change the behavior of SettingsBuilder. |
595 | // No merge strategies are applied, defaults are set in the constructor. |
596 | if ( isset( $settings['config'] ) ) { |
597 | $this->updateSettingsConfig( $settings['config'] ); |
598 | } |
599 | if ( isset( $settings['config-overrides'] ) ) { |
600 | $this->updateSettingsConfig( $settings['config-overrides'] ); |
601 | } |
602 | |
603 | $this->applySchemas( $settings ); |
604 | |
605 | if ( isset( $settings['config'] ) ) { |
606 | $mergeStrategies = $this->configSchema->getMergeStrategies(); |
607 | $this->configSink->setMulti( $settings['config'], $mergeStrategies ); |
608 | } |
609 | |
610 | if ( isset( $settings['config-overrides'] ) ) { |
611 | // no merge strategies, just override in one go |
612 | $this->configSink->setMulti( $settings['config-overrides'] ); |
613 | } |
614 | |
615 | if ( isset( $settings['obsolete-config'] ) ) { |
616 | $this->obsoleteConfig = array_merge( $this->obsoleteConfig, $settings['obsolete-config'] ); |
617 | } |
618 | |
619 | if ( isset( $settings['config'] ) || isset( $settings['config-overrides'] ) ) { |
620 | // We have set some config variables, we can no longer assume we can blindly set defaults |
621 | // without merging with existing config variables. |
622 | // XXX: We could potentially track which config variables have been set, so we can still |
623 | // apply defaults for other config vars without merging. |
624 | $this->defaultsNeedMerging = true; |
625 | } |
626 | |
627 | foreach ( $settings['php-ini'] ?? [] as $option => $value ) { |
628 | $this->phpIniSink->set( |
629 | $option, |
630 | $value |
631 | ); |
632 | } |
633 | |
634 | // TODO: Closely integrate with ExtensionRegistry. Loading extension.json is basically |
635 | // the same as loading settings files. See T297166. |
636 | // That would also mean that extensions would actually be loaded here, |
637 | // not just queued. We can't do this right now, because we need to preserve |
638 | // interoperability with wfLoadExtension() being called from LocalSettings.php. |
639 | |
640 | if ( isset( $settings['extensions'] ) ) { |
641 | $extDir = $this->settingsConfig[MainConfigNames::ExtensionDirectory]; |
642 | foreach ( $settings['extensions'] ?? [] as $ext ) { |
643 | $path = "$extDir/$ext/extension.json"; // see wfLoadExtension |
644 | $this->extensionRegistry->queue( $path ); |
645 | } |
646 | } |
647 | |
648 | if ( isset( $settings['skins'] ) ) { |
649 | $skinDir = $this->settingsConfig[MainConfigNames::StyleDirectory]; |
650 | foreach ( $settings['skins'] ?? [] as $skin ) { |
651 | $path = "$skinDir/$skin/skin.json"; // see wfLoadSkin |
652 | $this->extensionRegistry->queue( $path ); |
653 | } |
654 | } |
655 | } |
656 | |
657 | /** |
658 | * Puts a value into a config variable. |
659 | * Depending on the variable's specification, the new value may |
660 | * be merged with the previous value, or may replace it. |
661 | * This is a shorthand for putConfigValues( [ $key => $value ] ). |
662 | * |
663 | * @see overrideConfigValue |
664 | * |
665 | * @param string $key the name of the config setting |
666 | * @param mixed $value The value to set |
667 | * |
668 | * @return $this |
669 | */ |
670 | public function putConfigValue( string $key, $value ): self { |
671 | return $this->putConfigValues( [ $key => $value ] ); |
672 | } |
673 | |
674 | /** |
675 | * Sets the value of multiple config variables. |
676 | * Depending on the variables' specification, the new values may |
677 | * be merged with the previous values, or they may replace them. |
678 | * This is a shorthand for loadArray( [ 'config' => $values ] ). |
679 | * |
680 | * @see overrideConfigValues |
681 | * |
682 | * @param array $values An associative array mapping names to values. |
683 | * |
684 | * @return $this |
685 | */ |
686 | public function putConfigValues( array $values ): self { |
687 | return $this->loadArrayInternal( [ 'config' => $values ], __METHOD__ ); |
688 | } |
689 | |
690 | /** |
691 | * Override the value of a config variable. |
692 | * This ignores any merge strategies and discards any previous value. |
693 | * This is a shorthand for overrideConfigValues( [ $key => $value ] ). |
694 | * |
695 | * @see putConfigValue |
696 | * |
697 | * @param string $key the name of the config setting |
698 | * @param mixed $value The value to set |
699 | * |
700 | * @return $this |
701 | */ |
702 | public function overrideConfigValue( string $key, $value ): self { |
703 | return $this->overrideConfigValues( [ $key => $value ] ); |
704 | } |
705 | |
706 | /** |
707 | * Override the value of multiple config variables. |
708 | * This ignores any merge strategies and discards any previous value. |
709 | * This is a shorthand for loadArray( [ 'config-overrides' => $values ] ). |
710 | * |
711 | * @see putConfigValues |
712 | * |
713 | * @param array $values An associative array mapping names to values. |
714 | * |
715 | * @return $this |
716 | */ |
717 | public function overrideConfigValues( array $values ): self { |
718 | return $this->loadArrayInternal( [ 'config-overrides' => $values ], __METHOD__ ); |
719 | } |
720 | |
721 | /** |
722 | * Register hook handlers. |
723 | * |
724 | * @param array<string,mixed> $handlers An associative array using the same structure |
725 | * as the Hooks config setting: |
726 | * Each value is a list of handler callbacks for the hook. |
727 | * |
728 | * @return $this |
729 | * @see HookContainer::register() |
730 | */ |
731 | public function registerHookHandlers( array $handlers ): self { |
732 | // NOTE: Rely on the merge strategy for the Hooks setting. |
733 | // TODO: Make hook handlers a separate structure in settings files, |
734 | // like they are in extension.json. |
735 | return $this->loadArrayInternal( [ 'config' => [ 'Hooks' => $handlers ] ], __METHOD__ ); |
736 | } |
737 | |
738 | /** |
739 | * Register a hook handler. |
740 | * |
741 | * @param string $hook |
742 | * @param mixed $handler |
743 | * |
744 | * @return $this |
745 | * @see HookContainer::register() |
746 | */ |
747 | public function registerHookHandler( string $hook, $handler ): self { |
748 | // NOTE: Rely on the merge strategy for the Hooks setting. |
749 | // TODO: Make hook handlers a separate structure in settings files, |
750 | // like they are in extension.json. |
751 | return $this->loadArray( [ 'config' => [ 'Hooks' => [ $hook => [ $handler ] ] ] ] ); |
752 | } |
753 | |
754 | /** |
755 | * Returns the config loaded so far. Implicitly triggers apply() when needed. |
756 | * |
757 | * @note This will implicitly call apply() |
758 | * |
759 | * @return Config |
760 | */ |
761 | public function getConfig(): Config { |
762 | // XXX: Would be nice if we could forbid using this method |
763 | // before enterRegistrationStage() is called. But we need |
764 | // access to some configuration earlier, e.g. WikiFarmSettingsDirectory. |
765 | |
766 | if ( $this->config && !$this->currentBatch ) { |
767 | return $this->config; |
768 | } |
769 | |
770 | $this->apply(); |
771 | $this->config = $this->configSink->build(); |
772 | |
773 | return $this->config; |
774 | } |
775 | |
776 | private function reset() { |
777 | $this->currentBatch = []; |
778 | } |
779 | |
780 | private function assertNotReadOnly( string $func ): void { |
781 | if ( $this->stage === self::STAGE_READ_ONLY ) { |
782 | throw new SettingsBuilderException( |
783 | "$func not supported in operation stage." |
784 | ); |
785 | } |
786 | } |
787 | |
788 | private function assertStillLoading( string $func ): void { |
789 | if ( $this->stage !== self::STAGE_LOADING ) { |
790 | throw new SettingsBuilderException( |
791 | "$func only supported while still in the loading stage." |
792 | ); |
793 | } |
794 | } |
795 | |
796 | /** |
797 | * Sets the SettingsBuilder read-only. |
798 | * |
799 | * Call this before using the configuration returned by getConfig() to construct services objects |
800 | * or initialize the service container. |
801 | * |
802 | * @internal For use in Setup.php. |
803 | */ |
804 | public function enterReadOnlyStage(): void { |
805 | $this->apply(); |
806 | $this->stage = self::STAGE_READ_ONLY; |
807 | } |
808 | |
809 | /** |
810 | * Prevents additional settings from being loaded, but still allows manipulation of config values. |
811 | * |
812 | * Call this before applying dynamic defaults and executing extension registration callbacks. |
813 | * |
814 | * @internal For use in Setup.php. |
815 | */ |
816 | public function enterRegistrationStage(): void { |
817 | $this->apply(); |
818 | $this->stage = self::STAGE_REGISTRATION; |
819 | } |
820 | |
821 | /** |
822 | * @internal For use in Setup.php, pending a better solution. |
823 | * @return ConfigBuilder |
824 | */ |
825 | public function getConfigBuilder(): ConfigBuilder { |
826 | $this->apply(); |
827 | return $this->configSink; |
828 | } |
829 | |
830 | /** |
831 | * Log a settings related warning, such as a deprecated config variable. |
832 | * |
833 | * This can be used during bootstrapping, when the regular logger is not yet available. |
834 | * The warnings will be passed to a regular logger after bootstrapping is complete. |
835 | * In addition, the updater will fail if it finds any warnings. |
836 | * This allows us to warn about deprecated settings, and make sure they are |
837 | * replaced before the update proceeds. |
838 | * |
839 | * @param string $msg |
840 | */ |
841 | public function warning( string $msg ) { |
842 | $this->assertNotReadOnly( __METHOD__ ); |
843 | $this->warnings[] = trim( $msg ); |
844 | } |
845 | |
846 | /** |
847 | * Returns any warnings logged by calling warning(). |
848 | * |
849 | * @internal |
850 | * @return string[] |
851 | */ |
852 | public function getWarnings(): array { |
853 | return $this->warnings; |
854 | } |
855 | |
856 | } |