MediaWiki  master
SiteConfiguration.php
Go to the documentation of this file.
1 <?php
22 
127 
131  public $suffixes = [];
132 
136  public $wikis = [];
137 
144  public $settings = [];
145 
150  public $fullLoadCallback = null;
151 
153  public $fullLoadDone = false;
154 
169  public $siteParamsCallback = null;
170 
175  protected $cfgCache = [];
176 
186  public function get( $settingName, $wiki, $site = null, $params = [],
187  $wikiTags = []
188  ) {
189  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
190  $overrides = $this->settings[$settingName] ?? null;
191  $value = $overrides ? $this->processSetting( $overrides, $wiki, $params['tags'] ) : null;
192  if ( !array_key_exists( '@replaceableSettings', $this->settings )
193  || in_array( $settingName, $this->settings['@replaceableSettings'] )
194  ) {
195  $this->doReplacements( $value, $params['replacements'] );
196  }
197  return $value;
198  }
199 
231  private function processSetting( $thisSetting, $wiki, $tags ) {
232  // Optimization: Avoid native type hint on private method called by hot getAll()
233  // <https://gerrit.wikimedia.org/r/c/mediawiki/core/+/820244>
234 
235  $retval = null;
236 
237  if ( array_key_exists( $wiki, $thisSetting ) ) {
238  // Found override by Wiki ID.
239  $retval = $thisSetting[$wiki];
240  } else {
241  if ( array_key_exists( "+$wiki", $thisSetting ) ) {
242  // Found mergable override by Wiki ID.
243  // We continue to look for more merge candidates.
244  $retval = $thisSetting["+$wiki"];
245  }
246 
247  foreach ( $tags as $tag ) {
248  if ( array_key_exists( $tag, $thisSetting ) ) {
249  if ( is_array( $retval ) && is_array( $thisSetting[$tag] ) ) {
250  // Found a mergable override by Tag, without "+" operator.
251  // Merge it with any "+wiki" match from before, and stop the cascade.
252  $retval = self::arrayMerge( $retval, $thisSetting[$tag] );
253  } else {
254  // Found a non-mergable override by Tag.
255  // This could in theory replace a "+wiki" match, but it should never happen
256  // that a setting uses both mergable array values and non-array values.
257  $retval = $thisSetting[$tag];
258  }
259  return $retval;
260  } elseif ( array_key_exists( "+$tag", $thisSetting ) ) {
261  // Found a mergable override by Tag with "+" operator.
262  // Merge it with any "+wiki" or "+tag" matches from before,
263  // and keep looking for more merge candidates.
264  $retval = self::arrayMerge( $retval ?? [], $thisSetting["+$tag"] );
265  }
266  }
267 
268  if ( array_key_exists( 'default', $thisSetting ) ) {
269  if ( is_array( $retval ) && is_array( $thisSetting['default'] ) ) {
270  // Found a mergable default
271  // Merge it with any "+wiki" or "+tag" matches from before.
272  $retval = self::arrayMerge( $retval, $thisSetting['default'] );
273  } else {
274  // Found a default
275  // If any array-based values were built up via "+wiki" or "+tag" matches,
276  // these are thrown away here. We don't support merging array values into
277  // non-array values, and the fallback here is to use the default.
278  $retval = $thisSetting['default'];
279  }
280  }
281  }
282  return $retval;
283  }
284 
291  private function doReplacements( &$value, $replacements ) {
292  // Optimization: Avoid native type hint on private method called by hot getAll()
293  // <https://gerrit.wikimedia.org/r/c/mediawiki/core/+/820244>
294 
295  if ( is_string( $value ) ) {
296  $value = strtr( $value, $replacements );
297  } elseif ( is_array( $value ) ) {
298  foreach ( $value as &$val ) {
299  if ( is_string( $val ) ) {
300  $val = strtr( $val, $replacements );
301  }
302  }
303  }
304  }
305 
314  public function getAll( $wiki, $site = null, $params = [], $wikiTags = [] ) {
315  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
316  $tags = $params['tags'];
317  $localSettings = [];
318  foreach ( $this->settings as $varname => $overrides ) {
319  $value = $this->processSetting( $overrides, $wiki, $tags );
320  if ( $varname[0] === '+' ) {
321  $varname = substr( $varname, 1 );
322  if ( is_array( $value ) && is_array( $GLOBALS[$varname] ) ) {
323  $value = self::arrayMerge( $value, $GLOBALS[$varname] );
324  }
325  }
326  if ( $value !== null ) {
327  $localSettings[$varname] = $value;
328  }
329  }
330 
331  $replacements = $params['replacements'];
332  if ( array_key_exists( '@replaceableSettings', $this->settings ) ) {
333  foreach ( $this->settings['@replaceableSettings'] as $varname ) {
334  if ( array_key_exists( $varname, $localSettings ) ) {
335  $this->doReplacements( $localSettings[$varname], $replacements );
336  }
337  }
338  } else {
339  foreach ( $localSettings as &$value ) {
340  $this->doReplacements( $value, $replacements );
341  }
342  }
343  return $localSettings;
344  }
345 
354  public function getBool( $setting, $wiki, $site = null, $wikiTags = [] ) {
355  return (bool)$this->get( $setting, $wiki, $site, [], $wikiTags );
356  }
357 
363  public function getLocalDatabases() {
364  return $this->wikis;
365  }
366 
376  public function extractVar( $setting, $wiki, $site, &$var,
377  $params = [], $wikiTags = []
378  ) {
379  $value = $this->get( $setting, $wiki, $site, $params, $wikiTags );
380  if ( $value !== null ) {
381  $var = $value;
382  }
383  }
384 
393  public function extractGlobal( $setting, $wiki, $site = null,
394  $params = [], $wikiTags = []
395  ) {
396  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
397  $this->extractGlobalSetting( $setting, $wiki, $params );
398  }
399 
405  public function extractGlobalSetting( $setting, $wiki, $params ) {
406  $overrides = $this->settings[$setting] ?? null;
407  $value = $overrides ? $this->processSetting( $overrides, $wiki, $params['tags'] ) : null;
408  if ( !array_key_exists( '@replaceableSettings', $this->settings )
409  || in_array( $setting, $this->settings['@replaceableSettings'] )
410  ) {
411  $this->doReplacements( $value, $params['replacements'] );
412  }
413  if ( $value !== null ) {
414  if ( substr( $setting, 0, 1 ) == '+' && is_array( $value ) ) {
415  $setting = substr( $setting, 1 );
416  if ( is_array( $GLOBALS[$setting] ) ) {
417  $GLOBALS[$setting] = self::arrayMerge( $GLOBALS[$setting], $value );
418  } else {
419  $GLOBALS[$setting] = $value;
420  }
421  } else {
422  $GLOBALS[$setting] = $value;
423  }
424  }
425  }
426 
434  public function extractAllGlobals( $wiki, $site = null, $params = [],
435  $wikiTags = []
436  ) {
437  $params = $this->mergeParams( $wiki, $site, $params, $wikiTags );
438  foreach ( $this->settings as $varName => $setting ) {
439  $this->extractGlobalSetting( $varName, $wiki, $params );
440  }
441  }
442 
451  protected function getWikiParams( $wiki ) {
452  static $default = [
453  'suffix' => null,
454  'lang' => null,
455  'tags' => [],
456  'params' => [],
457  ];
458 
459  if ( !is_callable( $this->siteParamsCallback ) ) {
460  return $default;
461  }
462 
463  $ret = ( $this->siteParamsCallback )( $this, $wiki );
464  # Validate the returned value
465  if ( !is_array( $ret ) ) {
466  return $default;
467  }
468 
469  foreach ( $default as $name => $def ) {
470  if ( !isset( $ret[$name] ) || ( is_array( $def ) && !is_array( $ret[$name] ) ) ) {
471  $ret[$name] = $def;
472  }
473  }
474 
475  return $ret;
476  }
477 
490  protected function mergeParams( $wiki, $site, array $params, array $wikiTags ) {
491  $ret = $this->getWikiParams( $wiki );
492 
493  if ( $ret['suffix'] === null ) {
494  $ret['suffix'] = $site;
495  }
496 
497  // Make tags based on the db suffix (e.g. wiki family) automatically
498  // available for use in wgConf. The user does not have to maintain
499  // wiki tag lookups (e.g. dblists at WMF) for the wiki family.
500  $wikiTags[] = $ret['suffix'];
501 
502  $ret['tags'] = array_unique( array_merge( $ret['tags'], $wikiTags ) );
503 
504  $ret['params'] += $params;
505 
506  // Make the $lang and $site parameters automatically available if they
507  // were provided by `siteParamsCallback` via getWikiParams()
508  if ( !isset( $ret['params']['lang'] ) && $ret['lang'] !== null ) {
509  $ret['params']['lang'] = $ret['lang'];
510  }
511  if ( !isset( $ret['params']['site'] ) && $ret['suffix'] !== null ) {
512  $ret['params']['site'] = $ret['suffix'];
513  }
514 
515  // Optimization: For hot getAll() code path, precompute replacements to re-use
516  // over hundreds of processSetting() calls.
517  $ret['replacements'] = [];
518  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
519  foreach ( $ret['params'] as $key => $value ) {
520  $ret['replacements'][ '$' . $key ] = $value;
521  }
522 
523  return $ret;
524  }
525 
532  public function siteFromDB( $wiki ) {
533  // Allow override
534  $def = $this->getWikiParams( $wiki );
535  if ( $def['suffix'] !== null && $def['lang'] !== null ) {
536  return [ $def['suffix'], $def['lang'] ];
537  }
538 
539  $languageCode = str_replace( '_', '-', $wiki );
540  foreach ( $this->suffixes as $altSite => $suffix ) {
541  if ( $suffix === '' ) {
542  return [ '', $languageCode ];
543  } elseif ( str_ends_with( $wiki, $suffix ) ) {
544  $site = is_string( $altSite ) ? $altSite : $suffix;
545  $languageCode = substr( $languageCode, 0, -strlen( $suffix ) );
546  return [ $site, $languageCode ];
547  }
548  }
549 
550  return [ null, null ];
551  }
552 
564  public function getConfig( $wiki, $settings ) {
565  $multi = is_array( $settings );
566  $settings = (array)$settings;
567  if ( WikiMap::isCurrentWikiId( $wiki ) ) { // $wiki is this wiki
568  $res = [];
569  foreach ( $settings as $name ) {
570  if ( !preg_match( '/^wg[A-Z]/', $name ) ) {
571  throw new MWException( "Variable '$name' does start with 'wg'." );
572  } elseif ( !isset( $GLOBALS[$name] ) ) {
573  throw new MWException( "Variable '$name' is not set." );
574  }
575  $res[$name] = $GLOBALS[$name];
576  }
577  } else { // $wiki is a foreign wiki
578  if ( isset( $this->cfgCache[$wiki] ) ) {
579  $res = array_intersect_key( $this->cfgCache[$wiki],
580  array_fill_keys( $settings, true ) );
581  if ( count( $res ) == count( $settings ) ) {
582  return $multi ? $res : current( $res ); // cache hit
583  }
584  } elseif ( !in_array( $wiki, $this->wikis ) ) {
585  throw new MWException( "No such wiki '$wiki'." );
586  } else {
587  $this->cfgCache[$wiki] = [];
588  }
589  $result = Shell::makeScriptCommand(
590  MW_INSTALL_PATH . '/maintenance/getConfiguration.php',
591  [
592  '--wiki', $wiki,
593  '--settings', implode( ' ', $settings ),
594  '--format', 'PHP',
595  ]
596  )
597  // limit.sh breaks this call
598  ->limits( [ 'memory' => 0, 'filesize' => 0 ] )
599  ->execute();
600 
601  $data = trim( $result->getStdout() );
602  if ( $result->getExitCode() || $data === '' ) {
603  throw new MWException( "Failed to run getConfiguration.php: {$result->getStdout()}" );
604  }
605  $res = unserialize( $data );
606  if ( !is_array( $res ) ) {
607  throw new MWException( "Failed to unserialize configuration array." );
608  }
609  $this->cfgCache[$wiki] += $res;
610  }
611 
612  return $multi ? $res : current( $res );
613  }
614 
625  private static function arrayMerge( array $array1, array $array2 ) {
626  $out = $array1;
627  foreach ( $array2 as $key => $value ) {
628  if ( isset( $out[$key] ) ) {
629  if ( is_array( $out[$key] ) && is_array( $value ) ) {
630  // Merge the new array into the existing one
631  $out[$key] = self::arrayMerge( $out[$key], $value );
632  } elseif ( is_numeric( $key ) ) {
633  // A numerical key is taken, append the value at the end instead.
634  // It is important that we generally preserve numerical keys and only
635  // fallback to appending values if there are conflicts. This is needed
636  // by configuration variables that hold associative arrays with
637  // meaningful numerical keys, such as $wgNamespacesWithSubpages,
638  // $wgNamespaceProtection, $wgNamespacesToBeSearchedDefault, etc.
639  $out[] = $value;
640  } elseif ( $out[$key] === false ) {
641  // A non-numerical key is taken and holds a false value,
642  // allow it to be overridden always. This exists mainly for the purpose
643  // merging permissions arrays, such as $wgGroupPermissions.
644  $out[$key] = $value;
645  }
646  // Else: The key is already taken and we keep the current value
647 
648  } else {
649  // Add a new key.
650  $out[$key] = $value;
651  }
652  }
653 
654  return $out;
655  }
656 
657  public function loadFullData() {
658  if ( $this->fullLoadCallback && !$this->fullLoadDone ) {
659  ( $this->fullLoadCallback )( $this );
660  $this->fullLoadDone = true;
661  }
662  }
663 }
unserialize( $serialized)
MediaWiki exception.
Definition: MWException.php:29
Executes shell commands.
Definition: Shell.php:46
Configuration holder, particularly for multi-wiki sites.
getConfig( $wiki, $settings)
Get the resolved (post-setup) configuration of a potentially foreign wiki.
getLocalDatabases()
Retrieves an array of local databases.
$wikis
Array of wikis, should be the same as $wgLocalDatabases.
extractAllGlobals( $wiki, $site=null, $params=[], $wikiTags=[])
Retrieves the values of all settings, and places them in their corresponding global variables.
callable null $siteParamsCallback
A callback function that returns an array with the following keys (all optional):
siteFromDB( $wiki)
Work out the site and language name from a database name.
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.
array $cfgCache
Configuration cache for getConfig()
string array $fullLoadCallback
Optional callback to load full configuration data.
extractVar( $setting, $wiki, $site, &$var, $params=[], $wikiTags=[])
Retrieves the value of a given setting, and places it in a variable passed by reference.
extractGlobalSetting( $setting, $wiki, $params)
getWikiParams( $wiki)
Return specific settings for $wiki See the documentation of self::$siteParamsCallback for more in-dep...
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.
$fullLoadDone
Whether or not all data has been loaded.
$suffixes
Array of suffixes, for self::siteFromDB()
static isCurrentWikiId( $wikiId)
Definition: WikiMap.php:321