fix: case-insensitive config from env, fix #2935 (#2973)

* refactor

* fix: case-sensitive config from env, fix #2935

* lowercase all config section and keys

* test: add test for case-insensitivity
This commit is contained in:
Dag 2022-08-23 21:19:53 +02:00 committed by GitHub
parent edbafc6144
commit 5165ea265d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 78 additions and 161 deletions

View File

@ -46,7 +46,7 @@ class DisplayAction implements ActionInterface
} }
if (array_key_exists('_cache_timeout', $request)) { if (array_key_exists('_cache_timeout', $request)) {
if (!CUSTOM_CACHE_TIMEOUT) { if (! Configuration::getConfig('cache', 'custom_timeout')) {
unset($request['_cache_timeout']); unset($request['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request); $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request);
header('Location: ' . $uri, true, 301); header('Location: ' . $uri, true, 301);

View File

@ -20,22 +20,22 @@ class MemcachedCache implements CacheInterface
$port = Configuration::getConfig($section, 'port'); $port = Configuration::getConfig($section, 'port');
if (empty($host) && empty($port)) { if (empty($host) && empty($port)) {
throw new \Exception('Configuration for ' . $section . ' missing. Please check your ' . FILE_CONFIG); throw new \Exception('Configuration for ' . $section . ' missing.');
} }
if (empty($host)) { if (empty($host)) {
throw new \Exception('"host" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG); throw new \Exception('"host" param is not set for ' . $section);
} }
if (empty($port)) { if (empty($port)) {
throw new \Exception('"port" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG); throw new \Exception('"port" param is not set for ' . $section);
} }
if (!ctype_digit($port)) { if (!ctype_digit($port)) {
throw new \Exception('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG); throw new \Exception('"port" param is invalid for ' . $section);
} }
$port = intval($port); $port = intval($port);
if ($port < 1 || $port > 65535) { if ($port < 1 || $port > 65535) {
throw new \Exception('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG); throw new \Exception('"port" param is invalid for ' . $section);
} }
$conn = new \Memcached(); $conn = new \Memcached();

View File

@ -22,7 +22,7 @@ class SQLiteCache implements CacheInterface
$section = 'SQLiteCache'; $section = 'SQLiteCache';
$file = Configuration::getConfig($section, 'file'); $file = Configuration::getConfig($section, 'file');
if (empty($file)) { if (!$file) {
throw new \Exception(sprintf('Configuration for %s missing.', $section)); throw new \Exception(sprintf('Configuration for %s missing.', $section));
} }

View File

@ -4,8 +4,6 @@
require __DIR__ . '/../../lib/rssbridge.php'; require __DIR__ . '/../../lib/rssbridge.php';
Configuration::loadConfiguration();
$url = 'https://api.github.com/repos/rss-bridge/rss-bridge/contributors'; $url = 'https://api.github.com/repos/rss-bridge/rss-bridge/contributors';
$contributors = []; $contributors = [];
$next = true; $next = true;

View File

@ -50,7 +50,7 @@ final class BridgeCard
]; ];
} }
if (CUSTOM_CACHE_TIMEOUT) { if (Configuration::getConfig('cache', 'custom_timeout')) {
$parameters['global']['_cache_timeout'] = [ $parameters['global']['_cache_timeout'] = [
'name' => 'Cache timeout in seconds', 'name' => 'Cache timeout in seconds',
'type' => 'number', 'type' => 'number',

View File

@ -19,27 +19,9 @@
*/ */
final class Configuration final class Configuration
{ {
/** private const VERSION = 'dev.2022-06-14';
* Holds the current release version of RSS-Bridge.
*
* Do not access this property directly!
* Use {@see Configuration::getVersion()} instead.
*
* @var string
*
* @todo Replace this property by a constant.
*/
public static $VERSION = 'dev.2022-06-14';
/** private static array $config = [];
* Holds the configuration data.
*
* Do not access this property directly!
* Use {@see Configuration::getConfig()} instead.
*
* @var array|null
*/
private static $config = null;
private function __construct() private function __construct()
{ {
@ -56,7 +38,7 @@ final class Configuration
public static function verifyInstallation() public static function verifyInstallation()
{ {
if (version_compare(\PHP_VERSION, '7.4.0') === -1) { if (version_compare(\PHP_VERSION, '7.4.0') === -1) {
self::reportError('RSS-Bridge requires at least PHP version 7.4.0!'); throw new \Exception('RSS-Bridge requires at least PHP version 7.4.0!');
} }
$errors = []; $errors = [];
@ -97,158 +79,114 @@ final class Configuration
} }
} }
/** public static function loadConfiguration(array $customConfig = [], array $env = [])
* Loads the configuration from disk and checks if the parameters are valid.
*
* Returns an error message and aborts execution if the configuration is invalid.
*
* The RSS-Bridge configuration is split into two files:
* - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships
* with every release of RSS-Bridge (do not modify this file!).
* - {@see FILE_CONFIG} The local configuration file that can be modified
* by server administrators.
*
* RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then
* replace parameters with the contents of {@see FILE_CONFIG}. That way new
* parameters are automatically initialized with default values and custom
* configurations can be reduced to the minimum set of parametes necessary
* (only the ones that changed).
*
* The configuration files must be placed in the root folder of RSS-Bridge
* (next to `index.php`).
*
* _Notice_: The configuration is stored in {@see Configuration::$config}.
*
* @return void
*/
public static function loadConfiguration()
{ {
if (!file_exists(FILE_CONFIG_DEFAULT)) { if (!file_exists(__DIR__ . '/../config.default.ini.php')) {
self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT); throw new \Exception('The default configuration file is missing');
} }
$config = parse_ini_file(__DIR__ . '/../config.default.ini.php', true, INI_SCANNER_TYPED);
$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED);
if (!$config) { if (!$config) {
self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT); throw new \Exception('Error parsing config');
} }
foreach ($config as $header => $section) {
if (file_exists(FILE_CONFIG)) {
// Replace default configuration with custom settings
foreach (parse_ini_file(FILE_CONFIG, true, INI_SCANNER_TYPED) as $header => $section) {
foreach ($section as $key => $value) { foreach ($section as $key => $value) {
$config[$header][$key] = $value; self::setConfig($header, $key, $value);
} }
} }
foreach ($customConfig as $header => $section) {
foreach ($section as $key => $value) {
self::setConfig($header, $key, $value);
} }
}
foreach (getenv() as $envName => $envValue) { foreach ($env as $envName => $envValue) {
// Replace all settings with their respective environment variable if available $nameParts = explode('_', $envName);
$keyArray = explode('_', $envName); if ($nameParts[0] === 'RSSBRIDGE') {
if ($keyArray[0] === 'RSSBRIDGE') { $header = $nameParts[1];
$header = strtolower($keyArray[1]); $key = $nameParts[2];
$key = strtolower($keyArray[2]);
if ($envValue === 'true' || $envValue === 'false') { if ($envValue === 'true' || $envValue === 'false') {
$envValue = filter_var($envValue, FILTER_VALIDATE_BOOLEAN); $envValue = filter_var($envValue, FILTER_VALIDATE_BOOLEAN);
} }
$config[$header][$key] = $envValue; self::setConfig($header, $key, $envValue);
} }
} }
self::$config = $config;
if ( if (
!is_string(self::getConfig('system', 'timezone')) !is_string(self::getConfig('system', 'timezone'))
|| !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)) || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))
) { ) {
self::reportConfigurationError('system', 'timezone'); self::throwConfigError('system', 'timezone');
} }
if (!is_string(self::getConfig('proxy', 'url'))) { if (!is_string(self::getConfig('proxy', 'url'))) {
self::reportConfigurationError('proxy', 'url', 'Is not a valid string'); self::throwConfigError('proxy', 'url', 'Is not a valid string');
} }
if (!is_bool(self::getConfig('proxy', 'by_bridge'))) { if (!is_bool(self::getConfig('proxy', 'by_bridge'))) {
self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean'); self::throwConfigError('proxy', 'by_bridge', 'Is not a valid Boolean');
} }
if (!is_string(self::getConfig('proxy', 'name'))) { if (!is_string(self::getConfig('proxy', 'name'))) {
/** Name of the proxy server */ /** Name of the proxy server */
self::reportConfigurationError('proxy', 'name', 'Is not a valid string'); self::throwConfigError('proxy', 'name', 'Is not a valid string');
} }
if (!is_string(self::getConfig('cache', 'type'))) { if (!is_string(self::getConfig('cache', 'type'))) {
self::reportConfigurationError('cache', 'type', 'Is not a valid string'); self::throwConfigError('cache', 'type', 'Is not a valid string');
} }
if (!is_bool(self::getConfig('cache', 'custom_timeout'))) { if (!is_bool(self::getConfig('cache', 'custom_timeout'))) {
self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean'); self::throwConfigError('cache', 'custom_timeout', 'Is not a valid Boolean');
} }
if (!is_bool(self::getConfig('authentication', 'enable'))) { if (!is_bool(self::getConfig('authentication', 'enable'))) {
self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean'); self::throwConfigError('authentication', 'enable', 'Is not a valid Boolean');
} }
if (!self::getConfig('authentication', 'username')) { if (!self::getConfig('authentication', 'username')) {
self::reportConfigurationError('authentication', 'username', 'Is not a valid string'); self::throwConfigError('authentication', 'username', 'Is not a valid string');
} }
if (! self::getConfig('authentication', 'password')) { if (! self::getConfig('authentication', 'password')) {
self::reportConfigurationError('authentication', 'password', 'Is not a valid string'); self::throwConfigError('authentication', 'password', 'Is not a valid string');
} }
if ( if (
!empty(self::getConfig('admin', 'email')) !empty(self::getConfig('admin', 'email'))
&& !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL) && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL)
) { ) {
self::reportConfigurationError('admin', 'email', 'Is not a valid email address'); self::throwConfigError('admin', 'email', 'Is not a valid email address');
} }
if (!is_bool(self::getConfig('admin', 'donations'))) { if (!is_bool(self::getConfig('admin', 'donations'))) {
self::reportConfigurationError('admin', 'donations', 'Is not a valid Boolean'); self::throwConfigError('admin', 'donations', 'Is not a valid Boolean');
} }
if (!is_string(self::getConfig('error', 'output'))) { if (!is_string(self::getConfig('error', 'output'))) {
self::reportConfigurationError('error', 'output', 'Is not a valid String'); self::throwConfigError('error', 'output', 'Is not a valid String');
} }
if ( if (
!is_numeric(self::getConfig('error', 'report_limit')) !is_numeric(self::getConfig('error', 'report_limit'))
|| self::getConfig('error', 'report_limit') < 1 || self::getConfig('error', 'report_limit') < 1
) { ) {
self::reportConfigurationError('admin', 'report_limit', 'Value is invalid'); self::throwConfigError('admin', 'report_limit', 'Value is invalid');
} }
} }
/** public static function getConfig(string $section, string $key)
* Returns the value of a parameter identified by section and key.
*
* @param string $section The section name.
* @param string $key The property name (key).
* @return mixed|null The parameter value.
*/
public static function getConfig($section, $key)
{ {
if (array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) { return self::$config[strtolower($section)][strtolower($key)] ?? null;
return self::$config[$section][$key];
} }
return null; private static function setConfig(string $section, string $key, $value): void
{
self::$config[strtolower($section)][strtolower($key)] = $value;
} }
/**
* Returns the current version string of RSS-Bridge.
*
* This function returns the contents of {@see Configuration::$VERSION} for
* regular installations and the git branch name and commit id for instances
* running in a git environment.
*
* @return string The version string.
*/
public static function getVersion() public static function getVersion()
{ {
$headFile = __DIR__ . '/../.git/HEAD'; $headFile = __DIR__ . '/../.git/HEAD';
// '@' is used to mute open_basedir warning
if (@is_readable($headFile)) { if (@is_readable($headFile)) {
$revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1); $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
$parts = explode('/', $revisionHashFile); $parts = explode('/', $revisionHashFile);
@ -260,39 +198,11 @@ final class Configuration
} }
} }
} }
return self::VERSION;
return Configuration::$VERSION;
} }
/** private static function throwConfigError($section, $key, $message = '')
* Reports an configuration error for the specified section and key to the
* user and ends execution
*
* @param string $section The section name
* @param string $key The configuration key
* @param string $message An optional message to the user
*
* @return void
*/
private static function reportConfigurationError($section, $key, $message = '')
{ {
$report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL; throw new \Exception("Config [$section] => [$key] is invalid. $message");
if (file_exists(FILE_CONFIG)) {
$report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL;
} elseif (!file_exists(FILE_CONFIG_DEFAULT)) {
$report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL;
} else {
$report .= 'The default configuration file is broken.' . PHP_EOL
. 'Restore the original file from ' . REPOSITORY . PHP_EOL;
}
$report .= $message;
self::reportError($report);
}
private static function reportError($message)
{
throw new \Exception(sprintf('Configuration error: %s', $message));
} }
} }

View File

@ -314,8 +314,7 @@ class FeedItem
* *
* Use {@see FeedItem::getContent()} to get the current item content. * Use {@see FeedItem::getContent()} to get the current item content.
* *
* @param string|object $content The item content as text or simple_html_dom * @param string|object $content The item content as text or simple_html_dom object.
* object.
* @return self * @return self
*/ */
public function setContent($content) public function setContent($content)

View File

@ -33,12 +33,15 @@ final class RssBridge
private function run($request): void private function run($request): void
{ {
Configuration::verifyInstallation(); Configuration::verifyInstallation();
Configuration::loadConfiguration();
$customConfig = [];
if (file_exists(__DIR__ . '/../config.ini.php')) {
$customConfig = parse_ini_file(__DIR__ . '/../config.ini.php', true, INI_SCANNER_TYPED);
}
Configuration::loadConfiguration($customConfig, getenv());
date_default_timezone_set(Configuration::getConfig('system', 'timezone')); date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
define('CUSTOM_CACHE_TIMEOUT', Configuration::getConfig('cache', 'custom_timeout'));
$authenticationMiddleware = new AuthenticationMiddleware(); $authenticationMiddleware = new AuthenticationMiddleware();
if (Configuration::getConfig('authentication', 'enable')) { if (Configuration::getConfig('authentication', 'enable')) {
$authenticationMiddleware(); $authenticationMiddleware();

View File

@ -36,12 +36,6 @@ const WHITELIST = __DIR__ . '/../whitelist.txt';
/** Path to the default whitelist file */ /** Path to the default whitelist file */
const WHITELIST_DEFAULT = __DIR__ . '/../whitelist.default.txt'; const WHITELIST_DEFAULT = __DIR__ . '/../whitelist.default.txt';
/** Path to the configuration file */
const FILE_CONFIG = __DIR__ . '/../config.ini.php';
/** Path to the default configuration file */
const FILE_CONFIG_DEFAULT = __DIR__ . '/../config.default.ini.php';
/** URL to the RSS-Bridge repository */ /** URL to the RSS-Bridge repository */
const REPOSITORY = 'https://github.com/RSS-Bridge/rss-bridge/'; const REPOSITORY = 'https://github.com/RSS-Bridge/rss-bridge/';

View File

@ -9,17 +9,30 @@ use PHPUnit\Framework\TestCase;
final class ConfigurationTest extends TestCase final class ConfigurationTest extends TestCase
{ {
public function test() public function testValueFromDefaultConfig()
{ {
putenv('RSSBRIDGE_system_timezone=Europe/Berlin');
Configuration::loadConfiguration(); Configuration::loadConfiguration();
// test nonsense
$this->assertSame(null, Configuration::getConfig('foobar', '')); $this->assertSame(null, Configuration::getConfig('foobar', ''));
$this->assertSame(null, Configuration::getConfig('foo', 'bar')); $this->assertSame(null, Configuration::getConfig('foo', 'bar'));
$this->assertSame(null, Configuration::getConfig('cache', '')); $this->assertSame(null, Configuration::getConfig('cache', ''));
$this->assertSame('UTC', Configuration::getConfig('system', 'timezone'));
}
// test value from env public function testValueFromCustomConfig()
{
Configuration::loadConfiguration(['system' => ['timezone' => 'Europe/Berlin']]);
$this->assertSame('Europe/Berlin', Configuration::getConfig('system', 'timezone')); $this->assertSame('Europe/Berlin', Configuration::getConfig('system', 'timezone'));
} }
public function testValueFromEnv()
{
putenv('RSSBRIDGE_system_timezone=Europe/Berlin');
putenv('RSSBRIDGE_TwitterV2Bridge_twitterv2apitoken=aaa');
putenv('RSSBRIDGE_SQLiteCache_file=bbb');
Configuration::loadConfiguration([], getenv());
$this->assertSame('Europe/Berlin', Configuration::getConfig('system', 'timezone'));
$this->assertSame('aaa', Configuration::getConfig('TwitterV2Bridge', 'twitterv2apitoken'));
$this->assertSame('bbb', Configuration::getConfig('SQLiteCache', 'file'));
$this->assertSame('bbb', Configuration::getConfig('sqlitecache', 'file'));
}
} }