MediaWiki  master
SiteConfiguration.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Config;
22 
25 use RuntimeException;
26 
131 
135  public $suffixes = [];
136 
140  public $wikis = [];
141 
148  public $settings = [];
149 
154  public $fullLoadCallback = null;
155 
157  public $fullLoadDone = false;
158 
173  public $siteParamsCallback = null;
174 
179  protected $cfgCache = [];
180 
190  public function get(
191  $settingName,
192  $wiki,
193  $site = null,
194  $params = [],
195  $wikiTags = []
196  ) {
197  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
198  $overrides = $this->settings[$settingName] ?? null;
199  $value = $overrides ? $this->processSetting( $overrides, $wiki, $params['tags'] ) : null;
200  if ( !array_key_exists( '@replaceableSettings', $this->settings )
201  || in_array( $settingName, $this->settings['@replaceableSettings'] )
202  ) {
203  $this->doReplacements( $value, $params['replacements'] );
204  }
205  return $value;
206  }
207 
239  private function processSetting( $thisSetting, $wiki, $tags ) {
240  // Optimization: Avoid native type hint on private method called by hot getAll()
241  // <https://gerrit.wikimedia.org/r/c/mediawiki/core/+/820244>
242 
243  $retval = null;
244 
245  if ( array_key_exists( $wiki, $thisSetting ) ) {
246  // Found override by Wiki ID.
247  $retval = $thisSetting[$wiki];
248  } else {
249  if ( array_key_exists( "+$wiki", $thisSetting ) ) {
250  // Found mergable override by Wiki ID.
251  // We continue to look for more merge candidates.
252  $retval = $thisSetting["+$wiki"];
253  }
254 
255  foreach ( $tags as $tag ) {
256  if ( array_key_exists( $tag, $thisSetting ) ) {
257  if ( is_array( $retval ) && is_array( $thisSetting[$tag] ) ) {
258  // Found a mergable override by Tag, without "+" operator.
259  // Merge it with any "+wiki" match from before, and stop the cascade.
260  $retval = self::arrayMerge( $retval, $thisSetting[$tag] );
261  } else {
262  // Found a non-mergable override by Tag.
263  // This could in theory replace a "+wiki" match, but it should never happen
264  // that a setting uses both mergable array values and non-array values.
265  $retval = $thisSetting[$tag];
266  }
267  return $retval;
268  } elseif ( array_key_exists( "+$tag", $thisSetting ) ) {
269  // Found a mergable override by Tag with "+" operator.
270  // Merge it with any "+wiki" or "+tag" matches from before,
271  // and keep looking for more merge candidates.
272  $retval = self::arrayMerge( $retval ?? [], $thisSetting["+$tag"] );
273  }
274  }
275 
276  if ( array_key_exists( 'default', $thisSetting ) ) {
277  if ( is_array( $retval ) && is_array( $thisSetting['default'] ) ) {
278  // Found a mergable default
279  // Merge it with any "+wiki" or "+tag" matches from before.
280  $retval = self::arrayMerge( $retval, $thisSetting['default'] );
281  } else {
282  // Found a default
283  // If any array-based values were built up via "+wiki" or "+tag" matches,
284  // these are thrown away here. We don't support merging array values into
285  // non-array values, and the fallback here is to use the default.
286  $retval = $thisSetting['default'];
287  }
288  }
289  }
290  return $retval;
291  }
292 
299  private function doReplacements( &$value, $replacements ) {
300  // Optimization: Avoid native type hint on private method called by hot getAll()
301  // <https://gerrit.wikimedia.org/r/c/mediawiki/core/+/820244>
302 
303  if ( is_string( $value ) ) {
304  $value = strtr( $value, $replacements );
305  } elseif ( is_array( $value ) ) {
306  foreach ( $value as &$val ) {
307  if ( is_string( $val ) ) {
308  $val = strtr( $val, $replacements );
309  }
310  }
311  }
312  }
313 
322  public function getAll( $wiki, $site = null, $params = [], $wikiTags = [] ) {
323  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
324  $tags = $params['tags'];
325  $localSettings = [];
326  foreach ( $this->settings as $varname => $overrides ) {
327  $value = $this->processSetting( $overrides, $wiki, $tags );
328  if ( $varname[0] === '+' ) {
329  $varname = substr( $varname, 1 );
330  if ( is_array( $value ) && is_array( $GLOBALS[$varname] ) ) {
331  $value = self::arrayMerge( $value, $GLOBALS[$varname] );
332  }
333  }
334  if ( $value !== null ) {
335  $localSettings[$varname] = $value;
336  }
337  }
338 
339  $replacements = $params['replacements'];
340  if ( array_key_exists( '@replaceableSettings', $this->settings ) ) {
341  foreach ( $this->settings['@replaceableSettings'] as $varname ) {
342  if ( array_key_exists( $varname, $localSettings ) ) {
343  $this->doReplacements( $localSettings[$varname], $replacements );
344  }
345  }
346  } else {
347  foreach ( $localSettings as &$value ) {
348  $this->doReplacements( $value, $replacements );
349  }
350  }
351  return $localSettings;
352  }
353 
363  public function getBool( $setting, $wiki, $site = null, $wikiTags = [] ) {
364  return (bool)$this->get( $setting, $wiki, $site, [], $wikiTags );
365  }
366 
372  public function getLocalDatabases() {
373  return $this->wikis;
374  }
375 
387  public function extractVar(
388  $setting,
389  $wiki,
390  $site,
391  &$var,
392  $params = [],
393  $wikiTags = []
394  ) {
395  wfDeprecated( __METHOD__, '1.41' );
396  $value = $this->get( $setting, $wiki, $site, $params, $wikiTags );
397  if ( $value !== null ) {
398  $var = $value;
399  }
400  }
401 
412  public function extractGlobal(
413  $setting,
414  $wiki,
415  $site = null,
416  $params = [],
417  $wikiTags = []
418  ) {
419  wfDeprecated( __METHOD__, '1.41' );
420  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
421  $this->extractGlobalSetting( $setting, $wiki, $params );
422  }
423 
429  public function extractGlobalSetting( $setting, $wiki, $params ) {
430  $overrides = $this->settings[$setting] ?? null;
431  $value = $overrides ? $this->processSetting( $overrides, $wiki, $params['tags'] ) : null;
432  if ( !array_key_exists( '@replaceableSettings', $this->settings )
433  || in_array( $setting, $this->settings['@replaceableSettings'] )
434  ) {
435  $this->doReplacements( $value, $params['replacements'] );
436  }
437  if ( $value !== null ) {
438  if ( substr( $setting, 0, 1 ) == '+' && is_array( $value ) ) {
439  $setting = substr( $setting, 1 );
440  if ( is_array( $GLOBALS[$setting] ) ) {
441  $GLOBALS[$setting] = self::arrayMerge( $GLOBALS[$setting], $value );
442  } else {
443  $GLOBALS[$setting] = $value;
444  }
445  } else {
446  $GLOBALS[$setting] = $value;
447  }
448  }
449  }
450 
458  public function extractAllGlobals(
459  $wiki,
460  $site = null,
461  $params = [],
462  $wikiTags = []
463  ) {
464  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
465  foreach ( $this->settings as $varName => $setting ) {
466  $this->extractGlobalSetting( $varName, $wiki, $params );
467  }
468  }
469 
478  protected function getWikiParams( $wiki ) {
479  static $default = [
480  'suffix' => null,
481  'lang' => null,
482  'tags' => [],
483  'params' => [],
484  ];
485 
486  if ( !is_callable( $this->siteParamsCallback ) ) {
487  return $default;
488  }
489 
490  $ret = ( $this->siteParamsCallback )( $this, $wiki );
491  # Validate the returned value
492  if ( !is_array( $ret ) ) {
493  return $default;
494  }
495 
496  foreach ( $default as $name => $def ) {
497  if ( !isset( $ret[$name] ) || ( is_array( $def ) && !is_array( $ret[$name] ) ) ) {
498  $ret[$name] = $def;
499  }
500  }
501 
502  return $ret;
503  }
504 
517  protected function mergeParams( $wiki, $site, array $params, array $wikiTags ) {
518  $ret = $this->getWikiParams( $wiki );
519 
520  $ret['suffix'] ??= $site;
521 
522  // Make tags based on the db suffix (e.g. wiki family) automatically
523  // available for use in wgConf. The user does not have to maintain
524  // wiki tag lookups (e.g. dblists at WMF) for the wiki family.
525  $wikiTags[] = $ret['suffix'];
526 
527  $ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) );
528 
529  $ret['params'] += $params;
530 
531  // Make the $lang and $site parameters automatically available if they
532  // were provided by `siteParamsCallback` via getWikiParams()
533  if ( !isset( $ret['params']['lang'] ) && $ret['lang'] !== null ) {
534  $ret['params']['lang'] = $ret['lang'];
535  }
536  if ( !isset( $ret['params']['site'] ) && $ret['suffix'] !== null ) {
537  $ret['params']['site'] = $ret['suffix'];
538  }
539 
540  // Optimization: For hot getAll() code path, precompute replacements to re-use
541  // over hundreds of processSetting() calls.
542  $ret['replacements'] = [];
543  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
544  foreach ( $ret['params'] as $key => $value ) {
545  $ret['replacements']['$' . $key] = $value;
546  }
547 
548  return $ret;
549  }
550 
557  public function siteFromDB( $wiki ) {
558  // Allow override
559  $def = $this->getWikiParams( $wiki );
560  if ( $def['suffix'] !== null && $def['lang'] !== null ) {
561  return [ $def['suffix'], $def['lang'] ];
562  }
563 
564  $languageCode = str_replace( '_', '-', $wiki );
565  foreach ( $this->suffixes as $altSite => $suffix ) {
566  if ( $suffix === '' ) {
567  return [ '', $languageCode ];
568  } elseif ( str_ends_with( $wiki, $suffix ) ) {
569  $site = is_string( $altSite ) ? $altSite : $suffix;
570  $languageCode = substr( $languageCode, 0, -strlen( $suffix ) );
571  return [ $site, $languageCode ];
572  }
573  }
574 
575  return [ null, null ];
576  }
577 
590  public function getConfig( $wiki, $settings ) {
591  wfDeprecated( __METHOD__, '1.41' );
592  $multi = is_array( $settings );
593  $settings = (array)$settings;
594  if ( WikiMap::isCurrentWikiId( $wiki ) ) { // $wiki is this wiki
595  $res = [];
596  foreach ( $settings as $name ) {
597  if ( !preg_match( '/^wg[A-Z]/', $name ) ) {
598  throw new ConfigException( "Variable '$name' does start with 'wg'." );
599  } elseif ( !isset( $GLOBALS[$name] ) ) {
600  throw new ConfigException( "Variable '$name' is not set." );
601  }
602  $res[$name] = $GLOBALS[$name];
603  }
604  } else { // $wiki is a foreign wiki
605  if ( isset( $this->cfgCache[$wiki] ) ) {
606  $res = array_intersect_key(
607  $this->cfgCache[$wiki],
608  array_fill_keys( $settings, true )
609  );
610  if ( count( $res ) == count( $settings ) ) {
611  return $multi ? $res : current( $res ); // cache hit
612  }
613  } elseif ( !in_array( $wiki, $this->wikis ) ) {
614  throw new ConfigException( "No such wiki '$wiki'." );
615  } else {
616  $this->cfgCache[$wiki] = [];
617  }
618  $result = Shell::makeScriptCommand(
619  'getConfiguration',
620  [
621  '--wiki', $wiki,
622  '--settings', implode( ' ', $settings ),
623  '--format', 'PHP',
624  ]
625  )
626  // limit.sh breaks this call
627  ->limits( [ 'memory' => 0, 'filesize' => 0 ] )
628  ->execute();
629 
630  $data = trim( $result->getStdout() );
631  if ( $result->getExitCode() || $data === '' ) {
632  throw new RuntimeException( "Failed to run getConfiguration.php: {$result->getStderr()}" );
633  }
634  $res = unserialize( $data );
635  if ( !is_array( $res ) ) {
636  throw new RuntimeException( "Failed to unserialize configuration array." );
637  }
638  $this->cfgCache[$wiki] += $res;
639  }
640 
641  return $multi ? $res : current( $res );
642  }
643 
654  private static function arrayMerge( array $array1, array $array2 ) {
655  $out = $array1;
656  foreach ( $array2 as $key => $value ) {
657  if ( isset( $out[$key] ) ) {
658  if ( is_array( $out[$key] ) && is_array( $value ) ) {
659  // Merge the new array into the existing one
660  $out[$key] = self::arrayMerge( $out[$key], $value );
661  } elseif ( is_numeric( $key ) ) {
662  // A numerical key is taken, append the value at the end instead.
663  // It is important that we generally preserve numerical keys and only
664  // fallback to appending values if there are conflicts. This is needed
665  // by configuration variables that hold associative arrays with
666  // meaningful numerical keys, such as $wgNamespacesWithSubpages,
667  // $wgNamespaceProtection, $wgNamespacesToBeSearchedDefault, etc.
668  $out[] = $value;
669  } elseif ( $out[$key] === false ) {
670  // A non-numerical key is taken and holds a false value,
671  // allow it to be overridden always. This exists mainly for the purpose
672  // merging permissions arrays, such as $wgGroupPermissions.
673  $out[$key] = $value;
674  }
675  // Else: The key is already taken and we keep the current value
676 
677  } else {
678  // Add a new key.
679  $out[$key] = $value;
680  }
681  }
682 
683  return $out;
684  }
685 
686  public function loadFullData() {
687  if ( $this->fullLoadCallback && !$this->fullLoadDone ) {
688  ( $this->fullLoadCallback )( $this );
689  $this->fullLoadDone = true;
690  }
691  }
692 }
693 
697 class_alias( SiteConfiguration::class, 'SiteConfiguration' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Exceptions for config failures.
Configuration holder, particularly for multi-wiki sites.
extractGlobalSetting( $setting, $wiki, $params)
$suffixes
Array of suffixes, for self::siteFromDB()
extractVar( $setting, $wiki, $site, &$var, $params=[], $wikiTags=[])
Retrieves the value of a given setting, and places it in a variable passed by reference.
$fullLoadDone
Whether or not all data has been loaded.
getLocalDatabases()
Retrieves an array of local databases.
getWikiParams( $wiki)
Return specific settings for $wiki See the documentation of self::$siteParamsCallback for more in-dep...
siteFromDB( $wiki)
Work out the site and language name from a database name.
extractAllGlobals( $wiki, $site=null, $params=[], $wikiTags=[])
Retrieves the values of all settings, and places them in their corresponding global variables.
array $cfgCache
Configuration cache for getConfig()
getConfig( $wiki, $settings)
Get the resolved (post-setup) configuration of a potentially foreign wiki.
string array $fullLoadCallback
Optional callback to load full configuration data.
$wikis
Array of wikis, should be the same as $wgLocalDatabases.
mergeParams( $wiki, $site, array $params, array $wikiTags)
Merge params between the ones passed to the function and the ones given by self::$siteParamsCallback ...
$settings
The whole array of settings.
getAll( $wiki, $site=null, $params=[], $wikiTags=[])
Gets all settings for a wiki.
callable null $siteParamsCallback
A callback function that returns an array with the following keys (all optional):
getBool( $setting, $wiki, $site=null, $wikiTags=[])
Retrieves a configuration setting for a given wiki, forced to a boolean.
extractGlobal( $setting, $wiki, $site=null, $params=[], $wikiTags=[])
Retrieves the value of a given setting, and places it in its corresponding global variable.
Executes shell commands.
Definition: Shell.php:46
static makeScriptCommand(string $script, array $parameters, $options=[])
Generate a Command object to run a MediaWiki maintenance script.
Definition: Shell.php:186
Tools for dealing with other locally-hosted wikis.
Definition: WikiMap.php:31
static isCurrentWikiId( $wikiId)
Definition: WikiMap.php:323