Merge pull request #9373 from nupplaphil/task/server_env
Introduce possibility for mapping $_SERVER variables to config-cache values
This commit is contained in:
commit
ae6b380362
12 changed files with 194 additions and 81 deletions
|
@ -30,11 +30,28 @@ use ParagonIE\HiddenString\HiddenString;
|
|||
*/
|
||||
class Cache
|
||||
{
|
||||
/** @var int Indicates that the cache entry is set by file - Low Priority */
|
||||
const SOURCE_FILE = 0;
|
||||
/** @var int Indicates that the cache entry is set by the DB config table - Middle Priority */
|
||||
const SOURCE_DB = 1;
|
||||
/** @var int Indicates that the cache entry is set by a server environment variable - High Priority */
|
||||
const SOURCE_ENV = 3;
|
||||
/** @var int Indicates that the cache entry is fixed and must not be changed */
|
||||
const SOURCE_FIX = 4;
|
||||
|
||||
/** @var int Default value for a config source */
|
||||
const SOURCE_DEFAULT = self::SOURCE_FILE;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var int[][]
|
||||
*/
|
||||
private $source = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
|
@ -43,11 +60,12 @@ class Cache
|
|||
/**
|
||||
* @param array $config A initial config array
|
||||
* @param bool $hidePasswordOutput True, if cache variables should take extra care of password values
|
||||
* @param int $source Sets a source of the initial config values
|
||||
*/
|
||||
public function __construct(array $config = [], bool $hidePasswordOutput = true)
|
||||
public function __construct(array $config = [], bool $hidePasswordOutput = true, $source = self::SOURCE_DEFAULT)
|
||||
{
|
||||
$this->hidePasswordOutput = $hidePasswordOutput;
|
||||
$this->load($config);
|
||||
$this->load($config, $source);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,9 +73,9 @@ class Cache
|
|||
* Doesn't overwrite previously set values by default to prevent default config files to supersede DB Config.
|
||||
*
|
||||
* @param array $config
|
||||
* @param bool $overwrite Force value overwrite if the config key already exists
|
||||
* @param int $source Indicates the source of the config entry
|
||||
*/
|
||||
public function load(array $config, bool $overwrite = false)
|
||||
public function load(array $config, int $source = self::SOURCE_DEFAULT)
|
||||
{
|
||||
$categories = array_keys($config);
|
||||
|
||||
|
@ -68,11 +86,7 @@ class Cache
|
|||
foreach ($keys as $key) {
|
||||
$value = $config[$category][$key];
|
||||
if (isset($value)) {
|
||||
if ($overwrite) {
|
||||
$this->set($category, $key, $value);
|
||||
} else {
|
||||
$this->setDefault($category, $key, $value);
|
||||
}
|
||||
$this->set($category, $key, $value, $source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,49 +105,45 @@ class Cache
|
|||
{
|
||||
if (isset($this->config[$cat][$key])) {
|
||||
return $this->config[$cat][$key];
|
||||
} elseif (!isset($key) && isset($this->config[$cat])) {
|
||||
} else if (!isset($key) && isset($this->config[$cat])) {
|
||||
return $this->config[$cat];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a default value in the config cache. Ignores already existing keys.
|
||||
*
|
||||
* @param string $cat Config category
|
||||
* @param string $key Config key
|
||||
* @param mixed $value Default value to set
|
||||
*/
|
||||
private function setDefault(string $cat, string $key, $value)
|
||||
{
|
||||
if (!isset($this->config[$cat][$key])) {
|
||||
$this->set($cat, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value in the config cache. Accepts raw output from the config table
|
||||
*
|
||||
* @param string $cat Config category
|
||||
* @param string $key Config key
|
||||
* @param mixed $value Value to set
|
||||
* @param string $cat Config category
|
||||
* @param string $key Config key
|
||||
* @param mixed $value Value to set
|
||||
* @param int $source The source of the current config key
|
||||
*
|
||||
* @return bool True, if the value is set
|
||||
*/
|
||||
public function set(string $cat, string $key, $value)
|
||||
public function set(string $cat, string $key, $value, $source = self::SOURCE_DEFAULT)
|
||||
{
|
||||
if (!isset($this->config[$cat])) {
|
||||
$this->config[$cat] = [];
|
||||
$this->source[$cat] = [];
|
||||
}
|
||||
|
||||
if (isset($this->source[$cat][$key]) &&
|
||||
$source < $this->source[$cat][$key]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hidePasswordOutput &&
|
||||
$key == 'password' &&
|
||||
is_string($value)) {
|
||||
$key == 'password' &&
|
||||
is_string($value)) {
|
||||
$this->config[$cat][$key] = new HiddenString((string)$value);
|
||||
} else {
|
||||
$this->config[$cat][$key] = $value;
|
||||
}
|
||||
|
||||
$this->source[$cat][$key] = $source;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -149,8 +159,10 @@ class Cache
|
|||
{
|
||||
if (isset($this->config[$cat][$key])) {
|
||||
unset($this->config[$cat][$key]);
|
||||
unset($this->source[$cat][$key]);
|
||||
if (count($this->config[$cat]) == 0) {
|
||||
unset($this->config[$cat]);
|
||||
unset($this->source[$cat]);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
|
|
|
@ -70,7 +70,7 @@ class JitConfig extends BaseConfig
|
|||
}
|
||||
|
||||
// load the whole category out of the DB into the cache
|
||||
$this->configCache->load($config, true);
|
||||
$this->configCache->load($config, Cache::SOURCE_DB);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -69,7 +69,7 @@ class PreloadConfig extends BaseConfig
|
|||
$this->config_loaded = true;
|
||||
|
||||
// load the whole category out of the DB into the cache
|
||||
$this->configCache->load($config, true);
|
||||
$this->configCache->load($config, Cache::SOURCE_DB);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,10 +21,8 @@
|
|||
|
||||
namespace Friendica\Database;
|
||||
|
||||
use Exception;
|
||||
use Friendica\Core\Config\Cache;
|
||||
use Friendica\Core\System;
|
||||
use Friendica\DI;
|
||||
use Friendica\Network\HTTPException\InternalServerErrorException;
|
||||
use Friendica\Util\DateTimeFormat;
|
||||
use Friendica\Util\Profiler;
|
||||
|
@ -68,14 +66,13 @@ class Database
|
|||
protected $testmode = false;
|
||||
private $relation = [];
|
||||
|
||||
public function __construct(Cache $configCache, Profiler $profiler, LoggerInterface $logger, array $server = [])
|
||||
public function __construct(Cache $configCache, Profiler $profiler, LoggerInterface $logger)
|
||||
{
|
||||
// We are storing these values for being able to perform a reconnect
|
||||
$this->configCache = $configCache;
|
||||
$this->profiler = $profiler;
|
||||
$this->logger = $logger;
|
||||
|
||||
$this->readServerVariables($server);
|
||||
$this->connect();
|
||||
|
||||
if ($this->isConnected()) {
|
||||
|
@ -84,30 +81,6 @@ class Database
|
|||
}
|
||||
}
|
||||
|
||||
private function readServerVariables(array $server)
|
||||
{
|
||||
// Use environment variables for mysql if they are set beforehand
|
||||
if (!empty($server['MYSQL_HOST'])
|
||||
&& (!empty($server['MYSQL_USERNAME']) || !empty($server['MYSQL_USER']))
|
||||
&& $server['MYSQL_PASSWORD'] !== false
|
||||
&& !empty($server['MYSQL_DATABASE']))
|
||||
{
|
||||
$db_host = $server['MYSQL_HOST'];
|
||||
if (!empty($server['MYSQL_PORT'])) {
|
||||
$db_host .= ':' . $server['MYSQL_PORT'];
|
||||
}
|
||||
$this->configCache->set('database', 'hostname', $db_host);
|
||||
unset($db_host);
|
||||
if (!empty($server['MYSQL_USERNAME'])) {
|
||||
$this->configCache->set('database', 'username', $server['MYSQL_USERNAME']);
|
||||
} else {
|
||||
$this->configCache->set('database', 'username', $server['MYSQL_USER']);
|
||||
}
|
||||
$this->configCache->set('database', 'password', (string) $server['MYSQL_PASSWORD']);
|
||||
$this->configCache->set('database', 'database', $server['MYSQL_DATABASE']);
|
||||
}
|
||||
}
|
||||
|
||||
public function connect()
|
||||
{
|
||||
if (!is_null($this->connection) && $this->connected()) {
|
||||
|
@ -124,6 +97,11 @@ class Database
|
|||
if (count($serverdata) > 1) {
|
||||
$port = trim($serverdata[1]);
|
||||
}
|
||||
|
||||
if (!empty(trim($this->configCache->get('database', 'port')))) {
|
||||
$port = trim($this->configCache->get('database', 'port'));
|
||||
}
|
||||
|
||||
$server = trim($server);
|
||||
$user = trim($this->configCache->get('database', 'username'));
|
||||
$pass = trim($this->configCache->get('database', 'password'));
|
||||
|
@ -658,7 +636,7 @@ class Database
|
|||
$errorno = $this->errorno;
|
||||
|
||||
if ($this->testmode) {
|
||||
throw new Exception(DI::l10n()->t('Database error %d "%s" at "%s"', $errorno, $error, $this->replaceParameters($sql, $args)));
|
||||
throw new DatabaseException($error, $errorno, $this->replaceParameters($sql, $args));
|
||||
}
|
||||
|
||||
$this->logger->error('DB Error', [
|
||||
|
@ -761,7 +739,7 @@ class Database
|
|||
$errorno = $this->errorno;
|
||||
|
||||
if ($this->testmode) {
|
||||
throw new Exception(DI::l10n()->t('Database error %d "%s" at "%s"', $errorno, $error, $this->replaceParameters($sql, $params)));
|
||||
throw new DatabaseException($error, $errorno, $this->replaceParameters($sql, $params));
|
||||
}
|
||||
|
||||
$this->logger->error('DB Error', [
|
||||
|
|
39
src/Database/DatabaseException.php
Normal file
39
src/Database/DatabaseException.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Friendica\Database;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* A database fatal exception, which shouldn't occur
|
||||
*/
|
||||
class DatabaseException extends Exception
|
||||
{
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Construct the exception. Note: The message is NOT binary safe.
|
||||
*
|
||||
* @link https://php.net/manual/en/exception.construct.php
|
||||
*
|
||||
* @param string $message The Database error message.
|
||||
* @param int $code The Database error code.
|
||||
* @param string $query The Database error query.
|
||||
* @param Throwable $previous [optional] The previous throwable used for the exception chaining.
|
||||
*/
|
||||
public function __construct(string $message, int $code, string $query, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('Database error %d "%s" at "%s"', $this->message, $this->code, $this->query);
|
||||
}
|
||||
}
|
|
@ -37,10 +37,10 @@ class ConfigFactory
|
|||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createCache(ConfigFileLoader $loader)
|
||||
public function createCache(ConfigFileLoader $loader, array $server = [])
|
||||
{
|
||||
$configCache = new Cache();
|
||||
$loader->setupCache($configCache);
|
||||
$loader->setupCache($configCache, $server);
|
||||
|
||||
return $configCache;
|
||||
}
|
||||
|
|
|
@ -97,27 +97,30 @@ class ConfigFileLoader
|
|||
* expected local.config.php
|
||||
*
|
||||
* @param Cache $config The config cache to load to
|
||||
* @param array $server The $_SERVER array
|
||||
* @param bool $raw Setup the raw config format
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setupCache(Cache $config, $raw = false)
|
||||
public function setupCache(Cache $config, array $server = [], $raw = false)
|
||||
{
|
||||
// Load static config files first, the order is important
|
||||
$config->load($this->loadStaticConfig('defaults'));
|
||||
$config->load($this->loadStaticConfig('settings'));
|
||||
$config->load($this->loadStaticConfig('defaults'), Cache::SOURCE_FILE);
|
||||
$config->load($this->loadStaticConfig('settings'), Cache::SOURCE_FILE);
|
||||
|
||||
// try to load the legacy config first
|
||||
$config->load($this->loadLegacyConfig('htpreconfig'), true);
|
||||
$config->load($this->loadLegacyConfig('htconfig'), true);
|
||||
$config->load($this->loadLegacyConfig('htpreconfig'), Cache::SOURCE_FILE);
|
||||
$config->load($this->loadLegacyConfig('htconfig'), Cache::SOURCE_FILE);
|
||||
|
||||
// Now load every other config you find inside the 'config/' directory
|
||||
$this->loadCoreConfig($config);
|
||||
|
||||
$config->load($this->loadEnvConfig($server), Cache::SOURCE_ENV);
|
||||
|
||||
// In case of install mode, add the found basepath (because there isn't a basepath set yet
|
||||
if (!$raw && empty($config->get('system', 'basepath'))) {
|
||||
// Setting at least the basepath we know
|
||||
$config->set('system', 'basepath', $this->baseDir);
|
||||
$config->set('system', 'basepath', $this->baseDir, Cache::SOURCE_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,12 +160,12 @@ class ConfigFileLoader
|
|||
{
|
||||
// try to load legacy ini-files first
|
||||
foreach ($this->getConfigFiles(true) as $configFile) {
|
||||
$config->load($this->loadINIConfigFile($configFile), true);
|
||||
$config->load($this->loadINIConfigFile($configFile), Cache::SOURCE_FILE);
|
||||
}
|
||||
|
||||
// try to load supported config at last to overwrite it
|
||||
foreach ($this->getConfigFiles() as $configFile) {
|
||||
$config->load($this->loadConfigFile($configFile), true);
|
||||
$config->load($this->loadConfigFile($configFile), Cache::SOURCE_FILE);
|
||||
}
|
||||
|
||||
return [];
|
||||
|
@ -192,6 +195,38 @@ class ConfigFileLoader
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load environment specific variables, based on the `env.config.php` mapping table
|
||||
*
|
||||
* @param array $server The $_SERVER variable
|
||||
*
|
||||
* @return array The config array (empty if no config was found)
|
||||
*
|
||||
* @throws Exception if the configuration file isn't readable
|
||||
*/
|
||||
public function loadEnvConfig(array $server)
|
||||
{
|
||||
$filepath = $this->baseDir . DIRECTORY_SEPARATOR . // /var/www/html/
|
||||
self::STATIC_DIR . DIRECTORY_SEPARATOR . // static/
|
||||
"env.config.php"; // env.config.php
|
||||
|
||||
if (!file_exists($filepath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$envConfig = $this->loadConfigFile($filepath);
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($envConfig as $envKey => $configStructure) {
|
||||
if (isset($server[$envKey])) {
|
||||
$return[$configStructure[0]][$configStructure[1]] = $server[$envKey];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config files of the config-directory
|
||||
*
|
||||
|
|
|
@ -32,6 +32,11 @@ return [
|
|||
// Can contain the port number with the syntax "hostname:port".
|
||||
'hostname' => '',
|
||||
|
||||
// port (Integer)
|
||||
// Port of the database server.
|
||||
// Can be used instead of adding a port number to the hostname
|
||||
'port' => null,
|
||||
|
||||
// user (String)
|
||||
// Database user name. Please don't use "root".
|
||||
'username' => '',
|
||||
|
|
|
@ -81,7 +81,7 @@ return [
|
|||
Config\Cache::class => [
|
||||
'instanceOf' => Factory\ConfigFactory::class,
|
||||
'call' => [
|
||||
['createCache', [], Dice::CHAIN_CALL],
|
||||
['createCache', [$_SERVER], Dice::CHAIN_CALL],
|
||||
],
|
||||
],
|
||||
App\Mode::class => [
|
||||
|
@ -105,7 +105,6 @@ return [
|
|||
Database::class => [
|
||||
'constructParams' => [
|
||||
[Dice::INSTANCE => \Psr\Log\NullLogger::class],
|
||||
$_SERVER,
|
||||
],
|
||||
],
|
||||
/**
|
||||
|
|
31
static/env.config.php
Normal file
31
static/env.config.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2020, Friendica
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Main mapping table of environment variables to namespaced config values
|
||||
*
|
||||
*/
|
||||
|
||||
return [
|
||||
'MYSQL_HOST' => ['database', 'hostname'],
|
||||
'MYSQL_USERNAME' => ['database', 'username'],
|
||||
'MYSQL_USER' => ['database', 'username'],
|
||||
'MYSQL_PORT' => ['database', 'port'],
|
||||
'MYSQL_PASSWORD' => ['database', 'password'],
|
||||
'MYSQL_DATABASE' => ['database', 'database'],
|
||||
];
|
|
@ -83,16 +83,30 @@ class CacheTest extends MockedTest
|
|||
];
|
||||
|
||||
$configCache = new Cache();
|
||||
$configCache->load($data);
|
||||
$configCache->load($override);
|
||||
$configCache->load($data, Cache::SOURCE_DB);
|
||||
// doesn't override - Low Priority due Config file
|
||||
$configCache->load($override, Cache::SOURCE_FILE);
|
||||
|
||||
$this->assertConfigValues($data, $configCache);
|
||||
|
||||
// override the value
|
||||
$configCache->load($override, true);
|
||||
// override the value - High Prio due Server Env
|
||||
$configCache->load($override, Cache::SOURCE_ENV);
|
||||
|
||||
$this->assertEquals($override['system']['test'], $configCache->get('system', 'test'));
|
||||
$this->assertEquals($override['system']['boolTrue'], $configCache->get('system', 'boolTrue'));
|
||||
|
||||
// Don't overwrite server ENV variables - even in load mode
|
||||
$configCache->load($data, Cache::SOURCE_DB);
|
||||
|
||||
$this->assertEquals($override['system']['test'], $configCache->get('system', 'test'));
|
||||
$this->assertEquals($override['system']['boolTrue'], $configCache->get('system', 'boolTrue'));
|
||||
|
||||
// Overwrite ENV variables with ENV variables
|
||||
$configCache->load($data, Cache::SOURCE_ENV);
|
||||
|
||||
$this->assertConfigValues($data, $configCache);
|
||||
$this->assertNotEquals($override['system']['test'], $configCache->get('system', 'test'));
|
||||
$this->assertNotEquals($override['system']['boolTrue'], $configCache->get('system', 'boolTrue'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -350,7 +350,7 @@ abstract class ConfigTest extends MockedTest
|
|||
*/
|
||||
public function testGetWithRefresh($data)
|
||||
{
|
||||
$this->configCache->load(['test' => ['it' => 'now']]);
|
||||
$this->configCache->load(['test' => ['it' => 'now']], Cache::SOURCE_FILE);
|
||||
|
||||
$this->testedConfig = $this->getInstance();
|
||||
$this->assertInstanceOf(Cache::class, $this->testedConfig->getCache());
|
||||
|
@ -375,7 +375,7 @@ abstract class ConfigTest extends MockedTest
|
|||
*/
|
||||
public function testDeleteWithoutDB($data)
|
||||
{
|
||||
$this->configCache->load(['test' => ['it' => $data]]);
|
||||
$this->configCache->load(['test' => ['it' => $data]], Cache::SOURCE_FILE);
|
||||
|
||||
$this->testedConfig = $this->getInstance();
|
||||
$this->assertInstanceOf(Cache::class, $this->testedConfig->getCache());
|
||||
|
@ -395,7 +395,7 @@ abstract class ConfigTest extends MockedTest
|
|||
*/
|
||||
public function testDeleteWithDB()
|
||||
{
|
||||
$this->configCache->load(['test' => ['it' => 'now', 'quarter' => 'true']]);
|
||||
$this->configCache->load(['test' => ['it' => 'now', 'quarter' => 'true']], Cache::SOURCE_FILE);
|
||||
|
||||
$this->configModel->shouldReceive('delete')
|
||||
->with('test', 'it')
|
||||
|
|
Loading…
Reference in a new issue