Merge pull request #9373 from nupplaphil/task/server_env

Introduce possibility for mapping $_SERVER variables to config-cache values
This commit is contained in:
Hypolite Petovan 2020-10-08 15:00:31 -04:00 committed by GitHub
commit ae6b380362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 194 additions and 81 deletions

View File

@ -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 {

View File

@ -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);
}
/**

View File

@ -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);
}
/**

View File

@ -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', [

View 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);
}
}

View File

@ -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;
}

View File

@ -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
*

View File

@ -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' => '',

View File

@ -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
View 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'],
];

View File

@ -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'));
}
/**

View File

@ -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')