Merge pull request #7313 from nupplaphil/task/dyn_config
Dynamic config loading
This commit is contained in:
commit
0c56e75b89
|
@ -12,7 +12,7 @@
|
||||||
*
|
*
|
||||||
* Then set the following for your MySQL installation
|
* Then set the following for your MySQL installation
|
||||||
*
|
*
|
||||||
* If you're unsure about what any of the config keys below do, please check the config/defaults.config.php file for
|
* If you're unsure about what any of the config keys below do, please check the static/defaults.config.php file for
|
||||||
* detailed documentation of their data type and behavior.
|
* detailed documentation of their data type and behavior.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -33,15 +33,24 @@ return [
|
||||||
|
|
||||||
### Configuration location
|
### Configuration location
|
||||||
|
|
||||||
The `config` directory holds key configuration files:
|
The `config` directory holds key configuration files and can have different config files.
|
||||||
|
All of them have to end with `.config.php` and must not include `-sample` in their name.
|
||||||
|
|
||||||
- `defaults.config.php` holds the default values for all the configuration keys that can only be set in `local.config.php`.
|
Some examples of common known configuration files:
|
||||||
- `settings.config.php` holds the default values for some configuration keys that are set through the admin settings page.
|
|
||||||
- `local.config.php` holds the current node custom configuration.
|
- `local.config.php` holds the current node custom configuration.
|
||||||
- `addon.config.php` is optional and holds the custom configuration for specific addons.
|
- `addon.config.php` is optional and holds the custom configuration for specific addons.
|
||||||
|
|
||||||
Addons can define their own default configuration values in `addon/[addon]/config/[addon].config.php` which is loaded when the addon is activated.
|
Addons can define their own default configuration values in `addon/[addon]/config/[addon].config.php` which is loaded when the addon is activated.
|
||||||
|
|
||||||
|
### Static Configuration location
|
||||||
|
|
||||||
|
The `static` directory holds the codebase default configurations files.
|
||||||
|
They must not be changed by users, because they can get changed from release to release.
|
||||||
|
|
||||||
|
Currently, the following configurations are included:
|
||||||
|
- `defaults.config.php` holds the default values for all the configuration keys that can only be set in `local.config.php`.
|
||||||
|
- `settings.config.php` holds the default values for some configuration keys that are set through the admin settings page.
|
||||||
|
|
||||||
#### Migrating from .htconfig.php to config/local.config.php
|
#### Migrating from .htconfig.php to config/local.config.php
|
||||||
|
|
||||||
The legacy `.htconfig.php` configuration file is still supported, but is deprecated and will be removed in a subsequent Friendica release.
|
The legacy `.htconfig.php` configuration file is still supported, but is deprecated and will be removed in a subsequent Friendica release.
|
||||||
|
@ -292,7 +301,7 @@ Or it is for testing purposes only.
|
||||||
**Attention:** Please be warned that you shouldn't use one of these values without the knowledge what it could trigger.
|
**Attention:** Please be warned that you shouldn't use one of these values without the knowledge what it could trigger.
|
||||||
Especially don't do that with undocumented values.
|
Especially don't do that with undocumented values.
|
||||||
|
|
||||||
These configurations keys and their default value are listed in `config/defaults.config.php` and should be overwritten in `config/local.config.php`.
|
These configurations keys and their default value are listed in `static/defaults.config.php` and should be overwritten in `config/local.config.php`.
|
||||||
|
|
||||||
## Administrator Options
|
## Administrator Options
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ use Friendica\Database\DBA;
|
||||||
use Friendica\Model\Profile;
|
use Friendica\Model\Profile;
|
||||||
use Friendica\Network\HTTPException;
|
use Friendica\Network\HTTPException;
|
||||||
use Friendica\Util\BaseURL;
|
use Friendica\Util\BaseURL;
|
||||||
use Friendica\Util\Config\ConfigFileLoader;
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use Friendica\Util\HTTPSignature;
|
use Friendica\Util\HTTPSignature;
|
||||||
use Friendica\Util\Profiler;
|
use Friendica\Util\Profiler;
|
||||||
use Friendica\Util\Strings;
|
use Friendica\Util\Strings;
|
||||||
|
@ -360,9 +360,6 @@ class App
|
||||||
$this->getMode()->determine($this->getBasePath());
|
$this->getMode()->determine($this->getBasePath());
|
||||||
|
|
||||||
if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
|
if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
|
||||||
$loader = new ConfigFileLoader($this->getBasePath(), $this->getMode());
|
|
||||||
$this->config->getCache()->load($loader->loadCoreConfig('addon'), true);
|
|
||||||
|
|
||||||
$this->profiler->update(
|
$this->profiler->update(
|
||||||
$this->config->get('system', 'profiler', false),
|
$this->config->get('system', 'profiler', false),
|
||||||
$this->config->get('rendertime', 'callstack', false));
|
$this->config->get('rendertime', 'callstack', false));
|
||||||
|
|
|
@ -9,7 +9,7 @@ use Friendica\Core\Installer;
|
||||||
use Friendica\Core\Theme;
|
use Friendica\Core\Theme;
|
||||||
use Friendica\Util\BasePath;
|
use Friendica\Util\BasePath;
|
||||||
use Friendica\Util\BaseURL;
|
use Friendica\Util\BaseURL;
|
||||||
use Friendica\Util\Config\ConfigFileLoader;
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class AutomaticInstallation extends Console
|
class AutomaticInstallation extends Console
|
||||||
|
|
|
@ -45,7 +45,7 @@ abstract class Configuration
|
||||||
/**
|
/**
|
||||||
* @brief Loads all configuration values of family into a cached storage.
|
* @brief Loads all configuration values of family into a cached storage.
|
||||||
*
|
*
|
||||||
* All configuration values of the system are stored in the cache ( @see IConfigCache )
|
* All configuration values of the system are stored in the cache ( @see ConfigCache )
|
||||||
*
|
*
|
||||||
* @param string $cat The category of the configuration value
|
* @param string $cat The category of the configuration value
|
||||||
*
|
*
|
||||||
|
@ -59,7 +59,7 @@ abstract class Configuration
|
||||||
*
|
*
|
||||||
* Get a particular config value from the given category ($cat)
|
* Get a particular config value from the given category ($cat)
|
||||||
* and the $key from a cached storage either from the $this->configAdapter
|
* and the $key from a cached storage either from the $this->configAdapter
|
||||||
* (@see IConfigAdapter ) or from the $this->configCache (@see IConfigCache ).
|
* (@see IConfigAdapter ) or from the $this->configCache (@see ConfigCache ).
|
||||||
*
|
*
|
||||||
* @param string $cat The category of the configuration value
|
* @param string $cat The category of the configuration value
|
||||||
* @param string $key The configuration key to query
|
* @param string $key The configuration key to query
|
||||||
|
|
|
@ -96,7 +96,7 @@ class DBStructure
|
||||||
* Loads the database structure definition from the config/dbstructure.config.php file.
|
* Loads the database structure definition from the config/dbstructure.config.php file.
|
||||||
* On first pass, defines DB_UPDATE_VERSION constant.
|
* On first pass, defines DB_UPDATE_VERSION constant.
|
||||||
*
|
*
|
||||||
* @see config/dbstructure.config.php
|
* @see static/dbstructure.config.php
|
||||||
* @param boolean $with_addons_structure Whether to tack on addons additional tables
|
* @param boolean $with_addons_structure Whether to tack on addons additional tables
|
||||||
* @param string $basePath The base path of this application
|
* @param string $basePath The base path of this application
|
||||||
* @return array
|
* @return array
|
||||||
|
@ -106,16 +106,16 @@ class DBStructure
|
||||||
{
|
{
|
||||||
if (!self::$definition) {
|
if (!self::$definition) {
|
||||||
|
|
||||||
$filename = $basePath . '/config/dbstructure.config.php';
|
$filename = $basePath . '/static/dbstructure.config.php';
|
||||||
|
|
||||||
if (!is_readable($filename)) {
|
if (!is_readable($filename)) {
|
||||||
throw new Exception('Missing database structure config file config/dbstructure.config.php');
|
throw new Exception('Missing database structure config file static/dbstructure.config.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
$definition = require $filename;
|
$definition = require $filename;
|
||||||
|
|
||||||
if (!$definition) {
|
if (!$definition) {
|
||||||
throw new Exception('Corrupted database structure config file config/dbstructure.config.php');
|
throw new Exception('Corrupted database structure config file static/dbstructure.config.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$definition = $definition;
|
self::$definition = $definition;
|
||||||
|
|
|
@ -5,9 +5,9 @@ namespace Friendica\Factory;
|
||||||
use Friendica\Core;
|
use Friendica\Core;
|
||||||
use Friendica\Core\Config;
|
use Friendica\Core\Config;
|
||||||
use Friendica\Core\Config\Cache;
|
use Friendica\Core\Config\Cache;
|
||||||
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use Friendica\Model\Config\Config as ConfigModel;
|
use Friendica\Model\Config\Config as ConfigModel;
|
||||||
use Friendica\Model\Config\PConfig as PConfigModel;
|
use Friendica\Model\Config\PConfig as PConfigModel;
|
||||||
use Friendica\Util\Config\ConfigFileLoader;
|
|
||||||
|
|
||||||
class ConfigFactory
|
class ConfigFactory
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,7 +7,7 @@ use Friendica\Core\Config\Cache\PConfigCache;
|
||||||
use Friendica\Factory;
|
use Friendica\Factory;
|
||||||
use Friendica\Util\BasePath;
|
use Friendica\Util\BasePath;
|
||||||
use Friendica\Util\BaseURL;
|
use Friendica\Util\BaseURL;
|
||||||
use Friendica\Util\Config;
|
use Friendica\Util\ConfigFileLoader;
|
||||||
|
|
||||||
class DependencyFactory
|
class DependencyFactory
|
||||||
{
|
{
|
||||||
|
@ -27,7 +27,7 @@ class DependencyFactory
|
||||||
$basePath = BasePath::create($directory, $_SERVER);
|
$basePath = BasePath::create($directory, $_SERVER);
|
||||||
$mode = new App\Mode($basePath);
|
$mode = new App\Mode($basePath);
|
||||||
$router = new App\Router();
|
$router = new App\Router();
|
||||||
$configLoader = new Config\ConfigFileLoader($basePath, $mode);
|
$configLoader = new ConfigFileLoader($basePath, $mode);
|
||||||
$configCache = Factory\ConfigFactory::createCache($configLoader);
|
$configCache = Factory\ConfigFactory::createCache($configLoader);
|
||||||
$profiler = Factory\ProfilerFactory::create($configCache);
|
$profiler = Factory\ProfilerFactory::create($configCache);
|
||||||
$database = Factory\DBFactory::init($configCache, $profiler, $_SERVER);
|
$database = Factory\DBFactory::init($configCache, $profiler, $_SERVER);
|
||||||
|
|
|
@ -12,7 +12,7 @@ use Friendica\Database\DBA;
|
||||||
use Friendica\Database\DBStructure;
|
use Friendica\Database\DBStructure;
|
||||||
use Friendica\Model\Register;
|
use Friendica\Model\Register;
|
||||||
use Friendica\Module\BaseAdminModule;
|
use Friendica\Module\BaseAdminModule;
|
||||||
use Friendica\Util\Config\ConfigFileLoader;
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use Friendica\Util\DateTimeFormat;
|
use Friendica\Util\DateTimeFormat;
|
||||||
use Friendica\Util\Network;
|
use Friendica\Util\Network;
|
||||||
|
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Friendica\Util\Config;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstract class in case of handling with config files
|
|
||||||
*/
|
|
||||||
abstract class ConfigFileManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The Sub directory of the config-files
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
const SUBDIRECTORY = 'config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default name of the user defined config file
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
const CONFIG_LOCAL = 'local';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default name of the user defined ini file
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
const CONFIG_INI = 'local';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default name of the user defined legacy config file
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
const CONFIG_HTCONFIG = 'htconfig';
|
|
||||||
|
|
||||||
protected $baseDir;
|
|
||||||
protected $configDir;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $baseDir The base directory of Friendica
|
|
||||||
*/
|
|
||||||
public function __construct($baseDir)
|
|
||||||
{
|
|
||||||
$this->baseDir = $baseDir;
|
|
||||||
$this->configDir = $baseDir . DIRECTORY_SEPARATOR . self::SUBDIRECTORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the full name (including the path) for a *.config.php (default is local.config.php)
|
|
||||||
*
|
|
||||||
* @param string $name The config name (default is empty, which means local.config.php)
|
|
||||||
*
|
|
||||||
* @return string The full name or empty if not found
|
|
||||||
*/
|
|
||||||
protected function getConfigFullName($name = '')
|
|
||||||
{
|
|
||||||
$name = !empty($name) ? $name : self::CONFIG_LOCAL;
|
|
||||||
|
|
||||||
$fullName = $this->configDir . DIRECTORY_SEPARATOR . $name . '.config.php';
|
|
||||||
return file_exists($fullName) ? $fullName : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the full name (including the path) for a *.ini.php (default is local.ini.php)
|
|
||||||
*
|
|
||||||
* @param string $name The config name (default is empty, which means local.ini.php)
|
|
||||||
*
|
|
||||||
* @return string The full name or empty if not found
|
|
||||||
*/
|
|
||||||
protected function getIniFullName($name = '')
|
|
||||||
{
|
|
||||||
$name = !empty($name) ? $name : self::CONFIG_INI;
|
|
||||||
|
|
||||||
$fullName = $this->configDir . DIRECTORY_SEPARATOR . $name . '.ini.php';
|
|
||||||
return file_exists($fullName) ? $fullName : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the full name (including the path) for a .*.php (default is .htconfig.php)
|
|
||||||
*
|
|
||||||
* @param string $name The config name (default is empty, which means .htconfig.php)
|
|
||||||
*
|
|
||||||
* @return string The full name or empty if not found
|
|
||||||
*/
|
|
||||||
protected function getHtConfigFullName($name = '')
|
|
||||||
{
|
|
||||||
$name = !empty($name) ? $name : self::CONFIG_HTCONFIG;
|
|
||||||
|
|
||||||
$fullName = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php';
|
|
||||||
return file_exists($fullName) ? $fullName : '';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Friendica\Util\Config;
|
namespace Friendica\Util;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Friendica\App;
|
use Friendica\App;
|
||||||
use Friendica\Core\Addon;
|
use Friendica\Core\Addon;
|
||||||
use Friendica\Core\Config\Cache\ConfigCache;
|
use Friendica\Core\Config\Cache\ConfigCache;
|
||||||
|
@ -14,16 +15,65 @@ use Friendica\Core\Config\Cache\ConfigCache;
|
||||||
* - *.ini.php (deprecated)
|
* - *.ini.php (deprecated)
|
||||||
* - *.htconfig.php (deprecated)
|
* - *.htconfig.php (deprecated)
|
||||||
*/
|
*/
|
||||||
class ConfigFileLoader extends ConfigFileManager
|
class ConfigFileLoader
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The Sub directory of the config-files
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CONFIG_DIR = 'config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Sub directory of the static config-files
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const STATIC_DIR = 'static';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default name of the user defined ini file
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CONFIG_INI = 'local';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default name of the user defined legacy config file
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CONFIG_HTCONFIG = 'htconfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sample string inside the configs, which shouldn't get loaded
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const SAMPLE_END = '-sample';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var App\Mode
|
* @var App\Mode
|
||||||
*/
|
*/
|
||||||
private $appMode;
|
private $appMode;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $baseDir;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $configDir;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $staticDir;
|
||||||
|
|
||||||
public function __construct($baseDir, App\Mode $mode)
|
public function __construct($baseDir, App\Mode $mode)
|
||||||
{
|
{
|
||||||
parent::__construct($baseDir);
|
$this->baseDir = $baseDir;
|
||||||
|
$this->configDir = $baseDir . DIRECTORY_SEPARATOR . self::CONFIG_DIR;
|
||||||
|
$this->staticDir = $baseDir . DIRECTORY_SEPARATOR . self::STATIC_DIR;
|
||||||
$this->appMode = $mode;
|
$this->appMode = $mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,17 +86,20 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
* @param ConfigCache $config The config cache to load to
|
* @param ConfigCache $config The config cache to load to
|
||||||
* @param bool $raw Setup the raw config format
|
* @param bool $raw Setup the raw config format
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function setupCache(ConfigCache $config, $raw = false)
|
public function setupCache(ConfigCache $config, $raw = false)
|
||||||
{
|
{
|
||||||
$config->load($this->loadCoreConfig('defaults'));
|
// Load static config files first, the order is important
|
||||||
$config->load($this->loadCoreConfig('settings'));
|
$config->load($this->loadStaticConfig('defaults'));
|
||||||
|
$config->load($this->loadStaticConfig('settings'));
|
||||||
|
|
||||||
|
// try to load the legacy config first
|
||||||
$config->load($this->loadLegacyConfig('htpreconfig'), true);
|
$config->load($this->loadLegacyConfig('htpreconfig'), true);
|
||||||
$config->load($this->loadLegacyConfig('htconfig'), true);
|
$config->load($this->loadLegacyConfig('htconfig'), true);
|
||||||
|
|
||||||
$config->load($this->loadCoreConfig('local'), true);
|
// Now load every other config you find inside the 'config/' directory
|
||||||
|
$this->loadCoreConfig($config);
|
||||||
|
|
||||||
// In case of install mode, add the found basepath (because there isn't a basepath set yet
|
// In case of install mode, add the found basepath (because there isn't a basepath set yet
|
||||||
if (!$raw && ($this->appMode->isInstall() || empty($config->get('system', 'basepath')))) {
|
if (!$raw && ($this->appMode->isInstall() || empty($config->get('system', 'basepath')))) {
|
||||||
|
@ -56,25 +109,52 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to load the specified core-configuration and returns the config array.
|
* Tries to load the static core-configuration and returns the config array.
|
||||||
*
|
*
|
||||||
* @param string $name The name of the configuration (default is empty, which means 'local')
|
* @param string $name The name of the configuration
|
||||||
*
|
*
|
||||||
* @return array The config array (empty if no config found)
|
* @return array The config array (empty if no config found)
|
||||||
*
|
*
|
||||||
* @throws \Exception if the configuration file isn't readable
|
* @throws Exception if the configuration file isn't readable
|
||||||
*/
|
*/
|
||||||
public function loadCoreConfig($name = '')
|
private function loadStaticConfig($name)
|
||||||
{
|
{
|
||||||
if (!empty($this->getConfigFullName($name))) {
|
$configName = $this->staticDir . DIRECTORY_SEPARATOR . $name . '.config.php';
|
||||||
return $this->loadConfigFile($this->getConfigFullName($name));
|
$iniName = $this->staticDir . DIRECTORY_SEPARATOR . $name . '.ini.php';
|
||||||
} elseif (!empty($this->getIniFullName($name))) {
|
|
||||||
return $this->loadINIConfigFile($this->getIniFullName($name));
|
if (file_exists($configName)) {
|
||||||
|
return $this->loadConfigFile($configName);
|
||||||
|
} elseif (file_exists($iniName)) {
|
||||||
|
return $this->loadINIConfigFile($iniName);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to load the specified core-configuration into the config cache.
|
||||||
|
*
|
||||||
|
* @param ConfigCache $config The Config cache
|
||||||
|
*
|
||||||
|
* @return array The config array (empty if no config found)
|
||||||
|
*
|
||||||
|
* @throws Exception if the configuration file isn't readable
|
||||||
|
*/
|
||||||
|
private function loadCoreConfig(ConfigCache $config)
|
||||||
|
{
|
||||||
|
// try to load legacy ini-files first
|
||||||
|
foreach ($this->getConfigFiles(true) as $configFile) {
|
||||||
|
$config->load($this->loadINIConfigFile($configFile), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to load supported config at last to overwrite it
|
||||||
|
foreach ($this->getConfigFiles() as $configFile) {
|
||||||
|
$config->load($this->loadConfigFile($configFile), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to load the specified addon-configuration and returns the config array.
|
* Tries to load the specified addon-configuration and returns the config array.
|
||||||
*
|
*
|
||||||
|
@ -82,14 +162,14 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
*
|
*
|
||||||
* @return array The config array (empty if no config found)
|
* @return array The config array (empty if no config found)
|
||||||
*
|
*
|
||||||
* @throws \Exception if the configuration file isn't readable
|
* @throws Exception if the configuration file isn't readable
|
||||||
*/
|
*/
|
||||||
public function loadAddonConfig($name)
|
public function loadAddonConfig($name)
|
||||||
{
|
{
|
||||||
$filepath = $this->baseDir . DIRECTORY_SEPARATOR . // /var/www/html/
|
$filepath = $this->baseDir . DIRECTORY_SEPARATOR . // /var/www/html/
|
||||||
Addon::DIRECTORY . DIRECTORY_SEPARATOR . // addon/
|
Addon::DIRECTORY . DIRECTORY_SEPARATOR . // addon/
|
||||||
$name . DIRECTORY_SEPARATOR . // openstreetmap/
|
$name . DIRECTORY_SEPARATOR . // openstreetmap/
|
||||||
self::SUBDIRECTORY . DIRECTORY_SEPARATOR . // config/
|
self::CONFIG_DIR . DIRECTORY_SEPARATOR . // config/
|
||||||
$name . ".config.php"; // openstreetmap.config.php
|
$name . ".config.php"; // openstreetmap.config.php
|
||||||
|
|
||||||
if (file_exists($filepath)) {
|
if (file_exists($filepath)) {
|
||||||
|
@ -99,6 +179,32 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the config files of the config-directory
|
||||||
|
*
|
||||||
|
* @param bool $ini True, if scan for ini-files instead of config files
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getConfigFiles(bool $ini = false)
|
||||||
|
{
|
||||||
|
$files = scandir($this->configDir);
|
||||||
|
$found = array();
|
||||||
|
|
||||||
|
$filePattern = ($ini ? '*.ini.php' : '*.config.php');
|
||||||
|
|
||||||
|
// Don't load sample files
|
||||||
|
$sampleEnd = self::SAMPLE_END . ($ini ? '.ini.php' : '.config.php');
|
||||||
|
|
||||||
|
foreach ($files as $filename) {
|
||||||
|
if (fnmatch($filePattern, $filename) && substr_compare($filename, $sampleEnd, -strlen($sampleEnd))) {
|
||||||
|
$found[] = $this->configDir . '/' . $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to load the legacy config files (.htconfig.php, .htpreconfig.php) and returns the config array.
|
* Tries to load the legacy config files (.htconfig.php, .htpreconfig.php) and returns the config array.
|
||||||
*
|
*
|
||||||
|
@ -110,11 +216,14 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
*/
|
*/
|
||||||
private function loadLegacyConfig($name = '')
|
private function loadLegacyConfig($name = '')
|
||||||
{
|
{
|
||||||
|
$name = !empty($name) ? $name : self::CONFIG_HTCONFIG;
|
||||||
|
$fullName = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php';
|
||||||
|
|
||||||
$config = [];
|
$config = [];
|
||||||
if (!empty($this->getHtConfigFullName($name))) {
|
if (file_exists($fullName)) {
|
||||||
$a = new \stdClass();
|
$a = new \stdClass();
|
||||||
$a->config = [];
|
$a->config = [];
|
||||||
include $this->getHtConfigFullName($name);
|
include $fullName;
|
||||||
|
|
||||||
$htConfigCategories = array_keys($a->config);
|
$htConfigCategories = array_keys($a->config);
|
||||||
|
|
||||||
|
@ -172,11 +281,11 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
/**
|
/**
|
||||||
* Tries to load the specified legacy configuration file and returns the config array.
|
* Tries to load the specified legacy configuration file and returns the config array.
|
||||||
*
|
*
|
||||||
* @deprecated since version 2018.12
|
|
||||||
* @param string $filepath
|
* @param string $filepath
|
||||||
*
|
*
|
||||||
* @return array The configuration array
|
* @return array The configuration array
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
|
* @deprecated since version 2018.12
|
||||||
*/
|
*/
|
||||||
private function loadINIConfigFile($filepath)
|
private function loadINIConfigFile($filepath)
|
||||||
{
|
{
|
||||||
|
@ -185,7 +294,7 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
$config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
|
$config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
|
||||||
|
|
||||||
if ($config === false) {
|
if ($config === false) {
|
||||||
throw new \Exception('Error parsing INI config file ' . $filepath);
|
throw new Exception('Error parsing INI config file ' . $filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
|
@ -203,16 +312,17 @@ class ConfigFileLoader extends ConfigFileManager
|
||||||
* ];
|
* ];
|
||||||
*
|
*
|
||||||
* @param string $filepath The filepath of the
|
* @param string $filepath The filepath of the
|
||||||
|
*
|
||||||
* @return array The config array0
|
* @return array The config array0
|
||||||
*
|
*
|
||||||
* @throws \Exception if the config cannot get loaded.
|
* @throws Exception if the config cannot get loaded.
|
||||||
*/
|
*/
|
||||||
private function loadConfigFile($filepath)
|
private function loadConfigFile($filepath)
|
||||||
{
|
{
|
||||||
$config = include($filepath);
|
$config = include($filepath);
|
||||||
|
|
||||||
if (!is_array($config)) {
|
if (!is_array($config)) {
|
||||||
throw new \Exception('Error loading config file ' . $filepath);
|
throw new Exception('Error loading config file ' . $filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $config;
|
return $config;
|
|
@ -6,14 +6,13 @@
|
||||||
namespace Friendica\Test;
|
namespace Friendica\Test;
|
||||||
|
|
||||||
use Friendica\App\Mode;
|
use Friendica\App\Mode;
|
||||||
use Friendica\App\Router;
|
|
||||||
use Friendica\Core\Config\Cache\ConfigCache;
|
use Friendica\Core\Config\Cache\ConfigCache;
|
||||||
use Friendica\Database\Database;
|
use Friendica\Database\Database;
|
||||||
use Friendica\Factory\ConfigFactory;
|
use Friendica\Factory\ConfigFactory;
|
||||||
use Friendica\Factory\DBFactory;
|
use Friendica\Factory\DBFactory;
|
||||||
use Friendica\Factory\ProfilerFactory;
|
use Friendica\Factory\ProfilerFactory;
|
||||||
use Friendica\Util\BasePath;
|
use Friendica\Util\BasePath;
|
||||||
use Friendica\Util\Config\ConfigFileLoader;
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use Friendica\Util\Profiler;
|
use Friendica\Util\Profiler;
|
||||||
use PHPUnit\DbUnit\DataSet\YamlDataSet;
|
use PHPUnit\DbUnit\DataSet\YamlDataSet;
|
||||||
use PHPUnit\DbUnit\TestCaseTrait;
|
use PHPUnit\DbUnit\TestCaseTrait;
|
||||||
|
|
|
@ -21,33 +21,34 @@ trait VFSTrait
|
||||||
$structure = [
|
$structure = [
|
||||||
'config' => [],
|
'config' => [],
|
||||||
'bin' => [],
|
'bin' => [],
|
||||||
'test' => []
|
'static' => [],
|
||||||
|
'test' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
// create a virtual directory and copy all needed files and folders to it
|
// create a virtual directory and copy all needed files and folders to it
|
||||||
$this->root = vfsStream::setup('friendica', 0777, $structure);
|
$this->root = vfsStream::setup('friendica', 0777, $structure);
|
||||||
|
|
||||||
$this->setConfigFile('defaults.config.php');
|
$this->setConfigFile('defaults.config.php', true);
|
||||||
$this->setConfigFile('settings.config.php');
|
$this->setConfigFile('settings.config.php', true);
|
||||||
$this->setConfigFile('local.config.php');
|
$this->setConfigFile('local.config.php');
|
||||||
$this->setConfigFile('dbstructure.config.php');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copying a config file from the file system to the Virtual File System
|
* Copying a config file from the file system to the Virtual File System
|
||||||
*
|
*
|
||||||
* @param string $filename The filename of the config file
|
* @param string $filename The filename of the config file
|
||||||
|
* @param bool $static True, if the folder `static` instead of `config` should be used
|
||||||
*/
|
*/
|
||||||
protected function setConfigFile($filename)
|
protected function setConfigFile($filename, bool $static = false)
|
||||||
{
|
{
|
||||||
$file = dirname(__DIR__) . DIRECTORY_SEPARATOR .
|
$file = dirname(__DIR__) . DIRECTORY_SEPARATOR .
|
||||||
'..' . DIRECTORY_SEPARATOR .
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
'config' . DIRECTORY_SEPARATOR .
|
($static ? 'static' : 'config') . DIRECTORY_SEPARATOR .
|
||||||
$filename;
|
$filename;
|
||||||
|
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
vfsStream::newFile($filename)
|
vfsStream::newFile($filename)
|
||||||
->at($this->root->getChild('config'))
|
->at($this->root->getChild(($static ? 'static' : 'config')))
|
||||||
->setContent(file_get_contents($file));
|
->setContent(file_get_contents($file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,11 +57,12 @@ trait VFSTrait
|
||||||
* Delets a config file from the Virtual File System
|
* Delets a config file from the Virtual File System
|
||||||
*
|
*
|
||||||
* @param string $filename The filename of the config file
|
* @param string $filename The filename of the config file
|
||||||
|
* @param bool $static True, if the folder `static` instead of `config` should be used
|
||||||
*/
|
*/
|
||||||
protected function delConfigFile($filename)
|
protected function delConfigFile($filename, bool $static = false)
|
||||||
{
|
{
|
||||||
if ($this->root->hasChild('config/' . $filename)) {
|
if ($this->root->hasChild(($static ? 'static' : 'config') . '/' . $filename)) {
|
||||||
$this->root->getChild('config')->removeChild($filename);
|
$this->root->getChild(($static ? 'static' : 'config'))->removeChild($filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
tests/datasets/config/B.config.php
Normal file
29
tests/datasets/config/B.config.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test file for local configuration
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'database' => [
|
||||||
|
'hostname' => 'testhost',
|
||||||
|
'username' => 'testuser',
|
||||||
|
'password' => 'testpw',
|
||||||
|
'database' => 'testdb',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
|
||||||
|
'config' => [
|
||||||
|
'admin_email' => 'admin@overwritten.local',
|
||||||
|
'sitename' => 'Friendica Social Network',
|
||||||
|
'register_policy' => \Friendica\Module\Register::OPEN,
|
||||||
|
'register_text' => '',
|
||||||
|
],
|
||||||
|
'system' => [
|
||||||
|
'default_timezone' => 'UTC',
|
||||||
|
'language' => 'en',
|
||||||
|
'theme' => 'frio',
|
||||||
|
'newKey' => 'newValue',
|
||||||
|
],
|
||||||
|
];
|
20
tests/datasets/config/B.ini.php
Normal file
20
tests/datasets/config/B.ini.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* A test local ini file
|
||||||
|
*/
|
||||||
|
|
||||||
|
return <<<INI
|
||||||
|
|
||||||
|
[database]
|
||||||
|
hostname = testhost
|
||||||
|
username = testuser
|
||||||
|
password = testpw
|
||||||
|
database = testdb
|
||||||
|
|
||||||
|
[system]
|
||||||
|
theme = changed
|
||||||
|
newKey = newValue
|
||||||
|
|
||||||
|
[config]
|
||||||
|
admin_email = admin@overwritten.local
|
||||||
|
INI;
|
|
@ -14,6 +14,7 @@ use Friendica\Core\System;
|
||||||
use Friendica\Factory;
|
use Friendica\Factory;
|
||||||
use Friendica\Network\HTTPException;
|
use Friendica\Network\HTTPException;
|
||||||
use Friendica\Util\BaseURL;
|
use Friendica\Util\BaseURL;
|
||||||
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use Monolog\Handler\TestHandler;
|
use Monolog\Handler\TestHandler;
|
||||||
|
|
||||||
require_once __DIR__ . '/../../include/api.php';
|
require_once __DIR__ . '/../../include/api.php';
|
||||||
|
|
|
@ -392,7 +392,7 @@ FIN;
|
||||||
|
|
||||||
// Local configuration
|
// Local configuration
|
||||||
|
|
||||||
// If you're unsure about what any of the config keys below do, please check the config/defaults.config.php for detailed
|
// If you're unsure about what any of the config keys below do, please check the static/defaults.config.php for detailed
|
||||||
// documentation of their data type and behavior.
|
// documentation of their data type and behavior.
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -6,7 +6,7 @@ use Friendica\App;
|
||||||
use Friendica\Core\Config\Cache\ConfigCache;
|
use Friendica\Core\Config\Cache\ConfigCache;
|
||||||
use Friendica\Test\MockedTest;
|
use Friendica\Test\MockedTest;
|
||||||
use Friendica\Test\Util\VFSTrait;
|
use Friendica\Test\Util\VFSTrait;
|
||||||
use Friendica\Util\Config\ConfigFileLoader;
|
use Friendica\Util\ConfigFileLoader;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
use org\bovigo\vfs\vfsStream;
|
use org\bovigo\vfs\vfsStream;
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class ConfigFileLoaderTest extends MockedTest
|
||||||
'..' . DIRECTORY_SEPARATOR .
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
'datasets' . DIRECTORY_SEPARATOR .
|
'datasets' . DIRECTORY_SEPARATOR .
|
||||||
'config' . DIRECTORY_SEPARATOR .
|
'config' . DIRECTORY_SEPARATOR .
|
||||||
'local.config.php';
|
'A.config.php';
|
||||||
|
|
||||||
vfsStream::newFile('local.config.php')
|
vfsStream::newFile('local.config.php')
|
||||||
->at($this->root->getChild('config'))
|
->at($this->root->getChild('config'))
|
||||||
|
@ -105,7 +105,7 @@ class ConfigFileLoaderTest extends MockedTest
|
||||||
'..' . DIRECTORY_SEPARATOR .
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
'datasets' . DIRECTORY_SEPARATOR .
|
'datasets' . DIRECTORY_SEPARATOR .
|
||||||
'config' . DIRECTORY_SEPARATOR .
|
'config' . DIRECTORY_SEPARATOR .
|
||||||
'local.ini.php';
|
'A.ini.php';
|
||||||
|
|
||||||
vfsStream::newFile('local.ini.php')
|
vfsStream::newFile('local.ini.php')
|
||||||
->at($this->root->getChild('config'))
|
->at($this->root->getChild('config'))
|
||||||
|
@ -185,7 +185,7 @@ class ConfigFileLoaderTest extends MockedTest
|
||||||
'..' . DIRECTORY_SEPARATOR .
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
'datasets' . DIRECTORY_SEPARATOR .
|
'datasets' . DIRECTORY_SEPARATOR .
|
||||||
'config' . DIRECTORY_SEPARATOR .
|
'config' . DIRECTORY_SEPARATOR .
|
||||||
'local.config.php';
|
'A.config.php';
|
||||||
|
|
||||||
vfsStream::newFile('test.config.php')
|
vfsStream::newFile('test.config.php')
|
||||||
->at($this->root->getChild('addon')->getChild('test')->getChild('config'))
|
->at($this->root->getChild('addon')->getChild('test')->getChild('config'))
|
||||||
|
@ -202,4 +202,91 @@ class ConfigFileLoaderTest extends MockedTest
|
||||||
|
|
||||||
$this->assertEquals('admin@test.it', $conf['config']['admin_email']);
|
$this->assertEquals('admin@test.it', $conf['config']['admin_email']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* test loading multiple config files - the last config should work
|
||||||
|
*/
|
||||||
|
public function testLoadMultipleConfigs()
|
||||||
|
{
|
||||||
|
$this->delConfigFile('local.config.php');
|
||||||
|
|
||||||
|
$fileDir = dirname(__DIR__) . DIRECTORY_SEPARATOR .
|
||||||
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
|
'datasets' . DIRECTORY_SEPARATOR .
|
||||||
|
'config' . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
vfsStream::newFile('A.config.php')
|
||||||
|
->at($this->root->getChild('config'))
|
||||||
|
->setContent(file_get_contents($fileDir . 'A.config.php'));
|
||||||
|
vfsStream::newFile('B.config.php')
|
||||||
|
->at($this->root->getChild('config'))
|
||||||
|
->setContent(file_get_contents($fileDir . 'B.config.php'));
|
||||||
|
|
||||||
|
$configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode);
|
||||||
|
$configCache = new ConfigCache();
|
||||||
|
|
||||||
|
$configFileLoader->setupCache($configCache);
|
||||||
|
|
||||||
|
$this->assertEquals('admin@overwritten.local', $configCache->get('config', 'admin_email'));
|
||||||
|
$this->assertEquals('newValue', $configCache->get('system', 'newKey'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* test loading multiple config files - the last config should work (INI-version)
|
||||||
|
*/
|
||||||
|
public function testLoadMultipleInis()
|
||||||
|
{
|
||||||
|
$this->delConfigFile('local.config.php');
|
||||||
|
|
||||||
|
$fileDir = dirname(__DIR__) . DIRECTORY_SEPARATOR .
|
||||||
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
|
'datasets' . DIRECTORY_SEPARATOR .
|
||||||
|
'config' . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
vfsStream::newFile('A.ini.php')
|
||||||
|
->at($this->root->getChild('config'))
|
||||||
|
->setContent(file_get_contents($fileDir . 'A.ini.php'));
|
||||||
|
vfsStream::newFile('B.ini.php')
|
||||||
|
->at($this->root->getChild('config'))
|
||||||
|
->setContent(file_get_contents($fileDir . 'B.ini.php'));
|
||||||
|
|
||||||
|
$configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode);
|
||||||
|
$configCache = new ConfigCache();
|
||||||
|
|
||||||
|
$configFileLoader->setupCache($configCache);
|
||||||
|
|
||||||
|
$this->assertEquals('admin@overwritten.local', $configCache->get('config', 'admin_email'));
|
||||||
|
$this->assertEquals('newValue', $configCache->get('system', 'newKey'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that sample-files (e.g. local-sample.config.php) is never loaded
|
||||||
|
*/
|
||||||
|
public function testNotLoadingSamples()
|
||||||
|
{
|
||||||
|
$this->delConfigFile('local.config.php');
|
||||||
|
|
||||||
|
$fileDir = dirname(__DIR__) . DIRECTORY_SEPARATOR .
|
||||||
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
|
'..' . DIRECTORY_SEPARATOR .
|
||||||
|
'datasets' . DIRECTORY_SEPARATOR .
|
||||||
|
'config' . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
vfsStream::newFile('A.ini.php')
|
||||||
|
->at($this->root->getChild('config'))
|
||||||
|
->setContent(file_get_contents($fileDir . 'A.ini.php'));
|
||||||
|
vfsStream::newFile('B-sample.ini.php')
|
||||||
|
->at($this->root->getChild('config'))
|
||||||
|
->setContent(file_get_contents($fileDir . 'B.ini.php'));
|
||||||
|
|
||||||
|
$configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode);
|
||||||
|
$configCache = new ConfigCache();
|
||||||
|
|
||||||
|
$configFileLoader->setupCache($configCache);
|
||||||
|
|
||||||
|
$this->assertEquals('admin@test.it', $configCache->get('config', 'admin_email'));
|
||||||
|
$this->assertEmpty($configCache->get('system', 'NewKey'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ use Friendica\Worker\Delivery;
|
||||||
* This function is responsible for doing post update changes to the data
|
* This function is responsible for doing post update changes to the data
|
||||||
* (not the structure) in the database.
|
* (not the structure) in the database.
|
||||||
*
|
*
|
||||||
* Database structure changes are done in config/dbstructure.config.php
|
* Database structure changes are done in static/dbstructure.config.php
|
||||||
*
|
*
|
||||||
* If there is a need for a post process to a structure change, update this file
|
* If there is a need for a post process to a structure change, update this file
|
||||||
* by adding a new function at the end with the number of the new DB_UPDATE_VERSION.
|
* by adding a new function at the end with the number of the new DB_UPDATE_VERSION.
|
||||||
|
@ -33,8 +33,8 @@ use Friendica\Worker\Delivery;
|
||||||
* You are currently on version 4711 and you are preparing changes that demand an update script.
|
* You are currently on version 4711 and you are preparing changes that demand an update script.
|
||||||
*
|
*
|
||||||
* 1. Create a function "update_4712()" here in the update.php
|
* 1. Create a function "update_4712()" here in the update.php
|
||||||
* 2. Apply the needed structural changes in config/dbStructure.php
|
* 2. Apply the needed structural changes in static/dbStructure.php
|
||||||
* 3. Set DB_UPDATE_VERSION in config/dbstructure.config.php to 4712.
|
* 3. Set DB_UPDATE_VERSION in static/dbstructure.config.php to 4712.
|
||||||
*
|
*
|
||||||
* If you need to run a script before the database update, name the function "pre_update_4712()"
|
* If you need to run a script before the database update, name the function "pre_update_4712()"
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
// Local configuration
|
// Local configuration
|
||||||
|
|
||||||
// If you're unsure about what any of the config keys below do, please check the config/defaults.config.php for detailed
|
// If you're unsure about what any of the config keys below do, please check the static/defaults.config.php for detailed
|
||||||
// documentation of their data type and behavior.
|
// documentation of their data type and behavior.
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
Loading…
Reference in a new issue