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