summaryrefslogtreecommitdiff
path: root/www/wiki/includes/config
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/config
first commit
Diffstat (limited to 'www/wiki/includes/config')
-rw-r--r--www/wiki/includes/config/Config.php47
-rw-r--r--www/wiki/includes/config/ConfigException.php29
-rw-r--r--www/wiki/includes/config/ConfigFactory.php155
-rw-r--r--www/wiki/includes/config/EtcdConfig.php333
-rw-r--r--www/wiki/includes/config/EtcdConfigParseError.php4
-rw-r--r--www/wiki/includes/config/GlobalVarConfig.php87
-rw-r--r--www/wiki/includes/config/HashConfig.php78
-rw-r--r--www/wiki/includes/config/MultiConfig.php72
-rw-r--r--www/wiki/includes/config/MutableConfig.php38
9 files changed, 843 insertions, 0 deletions
diff --git a/www/wiki/includes/config/Config.php b/www/wiki/includes/config/Config.php
new file mode 100644
index 00000000..38f589dc
--- /dev/null
+++ b/www/wiki/includes/config/Config.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Interface for configuration instances
+ *
+ * @since 1.23
+ */
+interface Config {
+
+ /**
+ * Get a configuration variable such as "Sitename" or "UploadMaintenance."
+ *
+ * @param string $name Name of configuration option
+ * @return mixed Value configured
+ * @throws ConfigException
+ */
+ public function get( $name );
+
+ /**
+ * Check whether a configuration option is set for the given name
+ *
+ * @param string $name Name of configuration option
+ * @return bool
+ * @since 1.24
+ */
+ public function has( $name );
+}
diff --git a/www/wiki/includes/config/ConfigException.php b/www/wiki/includes/config/ConfigException.php
new file mode 100644
index 00000000..3b3ba9de
--- /dev/null
+++ b/www/wiki/includes/config/ConfigException.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Exceptions for config failures
+ *
+ * @since 1.23
+ */
+class ConfigException extends Exception {
+}
diff --git a/www/wiki/includes/config/ConfigFactory.php b/www/wiki/includes/config/ConfigFactory.php
new file mode 100644
index 00000000..2c7afdae
--- /dev/null
+++ b/www/wiki/includes/config/ConfigFactory.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+use MediaWiki\Services\SalvageableService;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Factory class to create Config objects
+ *
+ * @since 1.23
+ */
+class ConfigFactory implements SalvageableService {
+
+ /**
+ * Map of config name => callback
+ * @var array
+ */
+ protected $factoryFunctions = [];
+
+ /**
+ * Config objects that have already been created
+ * name => Config object
+ * @var array
+ */
+ protected $configs = [];
+
+ /**
+ * @deprecated since 1.27, use MediaWikiServices::getConfigFactory() instead.
+ *
+ * @return ConfigFactory
+ */
+ public static function getDefaultInstance() {
+ return \MediaWiki\MediaWikiServices::getInstance()->getConfigFactory();
+ }
+
+ /**
+ * Re-uses existing Cache objects from $other. Cache objects are only re-used if the
+ * registered factory function for both is the same. Cache config is not copied,
+ * and only instances of caches defined on this instance with the same config
+ * are copied.
+ *
+ * @see SalvageableService::salvage()
+ *
+ * @param SalvageableService $other The object to salvage state from. $other must have the
+ * exact same type as $this.
+ */
+ public function salvage( SalvageableService $other ) {
+ Assert::parameterType( self::class, $other, '$other' );
+
+ /** @var ConfigFactory $other */
+ foreach ( $other->factoryFunctions as $name => $otherFunc ) {
+ if ( !isset( $this->factoryFunctions[$name] ) ) {
+ continue;
+ }
+
+ // if the callback function is the same, salvage the Cache object
+ // XXX: Closures are never equal!
+ if ( isset( $other->configs[$name] )
+ && $this->factoryFunctions[$name] == $otherFunc
+ ) {
+ $this->configs[$name] = $other->configs[$name];
+ unset( $other->configs[$name] );
+ }
+ }
+
+ // disable $other
+ $other->factoryFunctions = [];
+ $other->configs = [];
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getConfigNames() {
+ return array_keys( $this->factoryFunctions );
+ }
+
+ /**
+ * Register a new config factory function.
+ * Will override if it's already registered.
+ * Use "*" for $name to provide a fallback config for all unknown names.
+ * @param string $name
+ * @param callable|Config $callback A factory callback that takes this ConfigFactory
+ * as an argument and returns a Config instance, or an existing Config instance.
+ * @throws InvalidArgumentException If an invalid callback is provided
+ */
+ public function register( $name, $callback ) {
+ if ( !is_callable( $callback ) && !( $callback instanceof Config ) ) {
+ if ( is_array( $callback ) ) {
+ $callback = '[ ' . implode( ', ', $callback ) . ' ]';
+ } elseif ( is_object( $callback ) ) {
+ $callback = 'instanceof ' . get_class( $callback );
+ }
+ throw new InvalidArgumentException( 'Invalid callback \'' . $callback . '\' provided' );
+ }
+
+ unset( $this->configs[$name] );
+ $this->factoryFunctions[$name] = $callback;
+ }
+
+ /**
+ * Create a given Config using the registered callback for $name.
+ * If an object was already created, the same Config object is returned.
+ * @param string $name Name of the extension/component you want a Config object for
+ * 'main' is used for core
+ * @throws ConfigException If a factory function isn't registered for $name
+ * @throws UnexpectedValueException If the factory function returns a non-Config object
+ * @return Config
+ */
+ public function makeConfig( $name ) {
+ if ( !isset( $this->configs[$name] ) ) {
+ $key = $name;
+ if ( !isset( $this->factoryFunctions[$key] ) ) {
+ $key = '*';
+ }
+ if ( !isset( $this->factoryFunctions[$key] ) ) {
+ throw new ConfigException( "No registered builder available for $name." );
+ }
+
+ if ( $this->factoryFunctions[$key] instanceof Config ) {
+ $conf = $this->factoryFunctions[$key];
+ } else {
+ $conf = call_user_func( $this->factoryFunctions[$key], $this );
+ }
+
+ if ( $conf instanceof Config ) {
+ $this->configs[$name] = $conf;
+ } else {
+ throw new UnexpectedValueException( "The builder for $name returned a non-Config object." );
+ }
+ }
+
+ return $this->configs[$name];
+ }
+
+}
diff --git a/www/wiki/includes/config/EtcdConfig.php b/www/wiki/includes/config/EtcdConfig.php
new file mode 100644
index 00000000..7020159f
--- /dev/null
+++ b/www/wiki/includes/config/EtcdConfig.php
@@ -0,0 +1,333 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Wikimedia\ObjectFactory;
+use Wikimedia\WaitConditionLoop;
+
+/**
+ * Interface for configuration instances
+ *
+ * @since 1.29
+ */
+class EtcdConfig implements Config, LoggerAwareInterface {
+ /** @var MultiHttpClient */
+ private $http;
+ /** @var BagOStuff */
+ private $srvCache;
+ /** @var array */
+ private $procCache;
+ /** @var LoggerInterface */
+ private $logger;
+
+ /** @var string */
+ private $host;
+ /** @var string */
+ private $protocol;
+ /** @var string */
+ private $directory;
+ /** @var string */
+ private $encoding;
+ /** @var int */
+ private $baseCacheTTL;
+ /** @var int */
+ private $skewCacheTTL;
+ /** @var int */
+ private $timeout;
+
+ /**
+ * @param array $params Parameter map:
+ * - host: the host address and port
+ * - protocol: either http or https
+ * - directory: the etc "directory" were MediaWiki specific variables are located
+ * - encoding: one of ("JSON", "YAML"). Defaults to JSON. [optional]
+ * - cache: BagOStuff instance or ObjectFactory spec thereof for a server cache.
+ * The cache will also be used as a fallback if etcd is down. [optional]
+ * - cacheTTL: logical cache TTL in seconds [optional]
+ * - skewTTL: maximum seconds to randomly lower the assigned TTL on cache save [optional]
+ * - timeout: seconds to wait for etcd before throwing an error [optional]
+ */
+ public function __construct( array $params ) {
+ $params += [
+ 'protocol' => 'http',
+ 'encoding' => 'JSON',
+ 'cacheTTL' => 10,
+ 'skewTTL' => 1,
+ 'timeout' => 2
+ ];
+
+ $this->host = $params['host'];
+ $this->protocol = $params['protocol'];
+ $this->directory = trim( $params['directory'], '/' );
+ $this->encoding = $params['encoding'];
+ $this->skewCacheTTL = $params['skewTTL'];
+ $this->baseCacheTTL = max( $params['cacheTTL'] - $this->skewCacheTTL, 0 );
+ $this->timeout = $params['timeout'];
+
+ if ( !isset( $params['cache'] ) ) {
+ $this->srvCache = new HashBagOStuff();
+ } elseif ( $params['cache'] instanceof BagOStuff ) {
+ $this->srvCache = $params['cache'];
+ } else {
+ $this->srvCache = ObjectFactory::getObjectFromSpec( $params['cache'] );
+ }
+
+ $this->logger = new Psr\Log\NullLogger();
+ $this->http = new MultiHttpClient( [
+ 'connTimeout' => $this->timeout,
+ 'reqTimeout' => $this->timeout,
+ 'logger' => $this->logger
+ ] );
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ $this->http->setLogger( $logger );
+ }
+
+ public function has( $name ) {
+ $this->load();
+
+ return array_key_exists( $name, $this->procCache['config'] );
+ }
+
+ public function get( $name ) {
+ $this->load();
+
+ if ( !array_key_exists( $name, $this->procCache['config'] ) ) {
+ throw new ConfigException( "No entry found for '$name'." );
+ }
+
+ return $this->procCache['config'][$name];
+ }
+
+ public function getModifiedIndex() {
+ $this->load();
+ return $this->procCache['modifiedIndex'];
+ }
+
+ /**
+ * @throws ConfigException
+ */
+ private function load() {
+ if ( $this->procCache !== null ) {
+ return; // already loaded
+ }
+
+ $now = microtime( true );
+ $key = $this->srvCache->makeGlobalKey(
+ __CLASS__,
+ $this->host,
+ $this->directory
+ );
+
+ // Get the cached value or block until it is regenerated (by this or another thread)...
+ $data = null; // latest config info
+ $error = null; // last error message
+ $loop = new WaitConditionLoop(
+ function () use ( $key, $now, &$data, &$error ) {
+ // Check if the values are in cache yet...
+ $data = $this->srvCache->get( $key );
+ if ( is_array( $data ) && $data['expires'] > $now ) {
+ $this->logger->debug( "Found up-to-date etcd configuration cache." );
+
+ return WaitConditionLoop::CONDITION_REACHED;
+ }
+
+ // Cache is either empty or stale;
+ // refresh the cache from etcd, using a mutex to reduce stampedes...
+ if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
+ try {
+ $etcdResponse = $this->fetchAllFromEtcd();
+ $error = $etcdResponse['error'];
+ if ( is_array( $etcdResponse['config'] ) ) {
+ // Avoid having all servers expire cache keys at the same time
+ $expiry = microtime( true ) + $this->baseCacheTTL;
+ $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL;
+ $data = [
+ 'config' => $etcdResponse['config'],
+ 'expires' => $expiry,
+ 'modifiedIndex' => $etcdResponse['modifiedIndex']
+ ];
+ $this->srvCache->set( $key, $data, BagOStuff::TTL_INDEFINITE );
+
+ $this->logger->info( "Refreshed stale etcd configuration cache." );
+
+ return WaitConditionLoop::CONDITION_REACHED;
+ } else {
+ $this->logger->error( "Failed to fetch configuration: $error" );
+ if ( !$etcdResponse['retry'] ) {
+ // Fail fast since the error is likely to keep happening
+ return WaitConditionLoop::CONDITION_FAILED;
+ }
+ }
+ } finally {
+ $this->srvCache->unlock( $key ); // release mutex
+ }
+ }
+
+ if ( is_array( $data ) ) {
+ $this->logger->info( "Using stale etcd configuration cache." );
+
+ return WaitConditionLoop::CONDITION_REACHED;
+ }
+
+ return WaitConditionLoop::CONDITION_CONTINUE;
+ },
+ $this->timeout
+ );
+
+ if ( $loop->invoke() !== WaitConditionLoop::CONDITION_REACHED ) {
+ // No cached value exists and etcd query failed; throw an error
+ throw new ConfigException( "Failed to load configuration from etcd: $error" );
+ }
+
+ $this->procCache = $data;
+ }
+
+ /**
+ * @return array (containing the keys config, error, retry, modifiedIndex)
+ */
+ public function fetchAllFromEtcd() {
+ // TODO: inject DnsSrvDiscoverer in order to be able to test this method
+ $dsd = new DnsSrvDiscoverer( $this->host );
+ $servers = $dsd->getServers();
+ if ( !$servers ) {
+ return $this->fetchAllFromEtcdServer( $this->host );
+ }
+
+ do {
+ // Pick a random etcd server from dns
+ $server = $dsd->pickServer( $servers );
+ $host = IP::combineHostAndPort( $server['target'], $server['port'] );
+ // Try to load the config from this particular server
+ $response = $this->fetchAllFromEtcdServer( $host );
+ if ( is_array( $response['config'] ) || $response['retry'] ) {
+ break;
+ }
+
+ // Avoid the server next time if that failed
+ $servers = $dsd->removeServer( $server, $servers );
+ } while ( $servers );
+
+ return $response;
+ }
+
+ /**
+ * @param string $address Host and port
+ * @return array (containing the keys config, error, retry, modifiedIndex)
+ */
+ protected function fetchAllFromEtcdServer( $address ) {
+ // Retrieve all the values under the MediaWiki config directory
+ list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->http->run( [
+ 'method' => 'GET',
+ 'url' => "{$this->protocol}://{$address}/v2/keys/{$this->directory}/?recursive=true",
+ 'headers' => [ 'content-type' => 'application/json' ]
+ ] );
+
+ $response = [ 'config' => null, 'error' => null, 'retry' => false, 'modifiedIndex' => 0 ];
+
+ static $terminalCodes = [ 404 => true ];
+ if ( $rcode < 200 || $rcode > 399 ) {
+ $response['error'] = strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)";
+ $response['retry'] = empty( $terminalCodes[$rcode] );
+ return $response;
+ }
+
+ try {
+ $parsedResponse = $this->parseResponse( $rbody );
+ } catch ( EtcdConfigParseError $e ) {
+ $parsedResponse = [ 'error' => $e->getMessage() ];
+ }
+ return array_merge( $response, $parsedResponse );
+ }
+
+ /**
+ * Parse a response body, throwing EtcdConfigParseError if there is a validation error
+ *
+ * @param string $rbody
+ * @return array
+ */
+ protected function parseResponse( $rbody ) {
+ $info = json_decode( $rbody, true );
+ if ( $info === null ) {
+ throw new EtcdConfigParseError( "Error unserializing JSON response." );
+ }
+ if ( !isset( $info['node'] ) || !is_array( $info['node'] ) ) {
+ throw new EtcdConfigParseError(
+ "Unexpected JSON response: Missing or invalid node at top level." );
+ }
+ $config = [];
+ $lastModifiedIndex = $this->parseDirectory( '', $info['node'], $config );
+ return [ 'modifiedIndex' => $lastModifiedIndex, 'config' => $config ];
+ }
+
+ /**
+ * Recursively parse a directory node and populate the array passed by
+ * reference, throwing EtcdConfigParseError if there is a validation error
+ *
+ * @param string $dirName The relative directory name
+ * @param array $dirNode The decoded directory node
+ * @param array &$config The output array
+ * @return int lastModifiedIndex The maximum last modified index across all keys in the directory
+ */
+ protected function parseDirectory( $dirName, $dirNode, &$config ) {
+ $lastModifiedIndex = 0;
+ if ( !isset( $dirNode['nodes'] ) ) {
+ throw new EtcdConfigParseError(
+ "Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
+ }
+ if ( !is_array( $dirNode['nodes'] ) ) {
+ throw new EtcdConfigParseError(
+ "Unexpected JSON response in dir '$dirName'; 'nodes' is not an array." );
+ }
+
+ foreach ( $dirNode['nodes'] as $node ) {
+ $baseName = basename( $node['key'] );
+ $fullName = $dirName === '' ? $baseName : "$dirName/$baseName";
+ if ( !empty( $node['dir'] ) ) {
+ $lastModifiedIndex = max(
+ $this->parseDirectory( $fullName, $node, $config ),
+ $lastModifiedIndex );
+ } else {
+ $value = $this->unserialize( $node['value'] );
+ if ( !is_array( $value ) || !array_key_exists( 'val', $value ) ) {
+ throw new EtcdConfigParseError( "Failed to parse value for '$fullName'." );
+ }
+ $lastModifiedIndex = max( $node['modifiedIndex'], $lastModifiedIndex );
+ $config[$fullName] = $value['val'];
+ }
+ }
+ return $lastModifiedIndex;
+ }
+
+ /**
+ * @param string $string
+ * @return mixed
+ */
+ private function unserialize( $string ) {
+ if ( $this->encoding === 'YAML' ) {
+ return yaml_parse( $string );
+ } else { // JSON
+ return json_decode( $string, true );
+ }
+ }
+}
diff --git a/www/wiki/includes/config/EtcdConfigParseError.php b/www/wiki/includes/config/EtcdConfigParseError.php
new file mode 100644
index 00000000..cab90a8e
--- /dev/null
+++ b/www/wiki/includes/config/EtcdConfigParseError.php
@@ -0,0 +1,4 @@
+<?php
+
+class EtcdConfigParseError extends Exception {
+}
diff --git a/www/wiki/includes/config/GlobalVarConfig.php b/www/wiki/includes/config/GlobalVarConfig.php
new file mode 100644
index 00000000..62953719
--- /dev/null
+++ b/www/wiki/includes/config/GlobalVarConfig.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Accesses configuration settings from $GLOBALS
+ *
+ * @since 1.23
+ */
+class GlobalVarConfig implements Config {
+
+ /**
+ * Prefix to use for configuration variables
+ * @var string
+ */
+ private $prefix;
+
+ /**
+ * Default builder function
+ * @return GlobalVarConfig
+ */
+ public static function newInstance() {
+ return new GlobalVarConfig();
+ }
+
+ public function __construct( $prefix = 'wg' ) {
+ $this->prefix = $prefix;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get( $name ) {
+ if ( !$this->has( $name ) ) {
+ throw new ConfigException( __METHOD__ . ": undefined option: '$name'" );
+ }
+ return $this->getWithPrefix( $this->prefix, $name );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function has( $name ) {
+ return $this->hasWithPrefix( $this->prefix, $name );
+ }
+
+ /**
+ * Get a variable with a given prefix, if not the defaults.
+ *
+ * @param string $prefix Prefix to use on the variable, if one.
+ * @param string $name Variable name without prefix
+ * @return mixed
+ */
+ protected function getWithPrefix( $prefix, $name ) {
+ return $GLOBALS[$prefix . $name];
+ }
+
+ /**
+ * Check if a variable with a given prefix is set
+ *
+ * @param string $prefix Prefix to use on the variable
+ * @param string $name Variable name without prefix
+ * @return bool
+ */
+ protected function hasWithPrefix( $prefix, $name ) {
+ $var = $prefix . $name;
+ return array_key_exists( $var, $GLOBALS );
+ }
+}
diff --git a/www/wiki/includes/config/HashConfig.php b/www/wiki/includes/config/HashConfig.php
new file mode 100644
index 00000000..d020d20f
--- /dev/null
+++ b/www/wiki/includes/config/HashConfig.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * A Config instance which stores all settings as a member variable
+ *
+ * @since 1.24
+ */
+class HashConfig implements Config, MutableConfig {
+
+ /**
+ * Array of config settings
+ *
+ * @var array
+ */
+ private $settings;
+
+ /**
+ * @return HashConfig
+ */
+ public static function newInstance() {
+ return new HashConfig;
+ }
+
+ /**
+ * @param array $settings Any current settings to pre-load
+ */
+ public function __construct( array $settings = [] ) {
+ $this->settings = $settings;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get( $name ) {
+ if ( !$this->has( $name ) ) {
+ throw new ConfigException( __METHOD__ . ": undefined option: '$name'" );
+ }
+
+ return $this->settings[$name];
+ }
+
+ /**
+ * @inheritDoc
+ * @since 1.24
+ */
+ public function has( $name ) {
+ return array_key_exists( $name, $this->settings );
+ }
+
+ /**
+ * @see MutableConfig::set
+ * @param string $name
+ * @param mixed $value
+ */
+ public function set( $name, $value ) {
+ $this->settings[$name] = $value;
+ }
+}
diff --git a/www/wiki/includes/config/MultiConfig.php b/www/wiki/includes/config/MultiConfig.php
new file mode 100644
index 00000000..2bbc84c9
--- /dev/null
+++ b/www/wiki/includes/config/MultiConfig.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Provides a fallback sequence for Config objects
+ *
+ * @since 1.24
+ */
+class MultiConfig implements Config {
+
+ /**
+ * Array of Config objects to use
+ * Order matters, the Config objects
+ * will be checked in order to see
+ * whether they have the requested setting
+ *
+ * @var Config[]
+ */
+ private $configs;
+
+ /**
+ * @param Config[] $configs
+ */
+ public function __construct( array $configs ) {
+ $this->configs = $configs;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get( $name ) {
+ foreach ( $this->configs as $config ) {
+ if ( $config->has( $name ) ) {
+ return $config->get( $name );
+ }
+ }
+
+ throw new ConfigException( __METHOD__ . ": undefined option: '$name'" );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function has( $name ) {
+ foreach ( $this->configs as $config ) {
+ if ( $config->has( $name ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/www/wiki/includes/config/MutableConfig.php b/www/wiki/includes/config/MutableConfig.php
new file mode 100644
index 00000000..e765e3bc
--- /dev/null
+++ b/www/wiki/includes/config/MutableConfig.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Copyright 2014
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Interface for mutable configuration instances
+ *
+ * @since 1.24
+ */
+interface MutableConfig {
+
+ /**
+ * Set a configuration variable such a "Sitename" to something like "My Wiki"
+ *
+ * @param string $name Name of configuration option
+ * @param mixed $value Value to set
+ * @throws ConfigException
+ */
+ public function set( $name, $value );
+}