diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php index e8912f09..e953b1c1 100644 --- a/actions/DisplayAction.php +++ b/actions/DisplayAction.php @@ -46,7 +46,7 @@ class DisplayAction implements ActionInterface } if (array_key_exists('_cache_timeout', $request)) { - if (!CUSTOM_CACHE_TIMEOUT) { + if (! Configuration::getConfig('cache', 'custom_timeout')) { unset($request['_cache_timeout']); $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request); header('Location: ' . $uri, true, 301); diff --git a/caches/MemcachedCache.php b/caches/MemcachedCache.php index c593ac79..85681e89 100644 --- a/caches/MemcachedCache.php +++ b/caches/MemcachedCache.php @@ -20,22 +20,22 @@ class MemcachedCache implements CacheInterface $port = Configuration::getConfig($section, '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)) { - 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)) { - 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)) { - 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); 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(); diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php index 339e4119..1e44519b 100644 --- a/caches/SQLiteCache.php +++ b/caches/SQLiteCache.php @@ -22,7 +22,7 @@ class SQLiteCache implements CacheInterface $section = 'SQLiteCache'; $file = Configuration::getConfig($section, 'file'); - if (empty($file)) { + if (!$file) { throw new \Exception(sprintf('Configuration for %s missing.', $section)); } diff --git a/contrib/prepare_release/fetch_contributors.php b/contrib/prepare_release/fetch_contributors.php index 479b95c3..e1b56709 100644 --- a/contrib/prepare_release/fetch_contributors.php +++ b/contrib/prepare_release/fetch_contributors.php @@ -4,8 +4,6 @@ require __DIR__ . '/../../lib/rssbridge.php'; -Configuration::loadConfiguration(); - $url = 'https://api.github.com/repos/rss-bridge/rss-bridge/contributors'; $contributors = []; $next = true; diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php index 727680cf..6eef3879 100644 --- a/lib/BridgeCard.php +++ b/lib/BridgeCard.php @@ -50,7 +50,7 @@ final class BridgeCard ]; } - if (CUSTOM_CACHE_TIMEOUT) { + if (Configuration::getConfig('cache', 'custom_timeout')) { $parameters['global']['_cache_timeout'] = [ 'name' => 'Cache timeout in seconds', 'type' => 'number', diff --git a/lib/Configuration.php b/lib/Configuration.php index 9f8b76bc..739c4ff4 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -19,27 +19,9 @@ */ final class Configuration { - /** - * 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 const VERSION = 'dev.2022-06-14'; - /** - * Holds the configuration data. - * - * Do not access this property directly! - * Use {@see Configuration::getConfig()} instead. - * - * @var array|null - */ - private static $config = null; + private static array $config = []; private function __construct() { @@ -56,7 +38,7 @@ final class Configuration public static function verifyInstallation() { 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 = []; @@ -97,158 +79,114 @@ final class Configuration } } - /** - * 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() + public static function loadConfiguration(array $customConfig = [], array $env = []) { - if (!file_exists(FILE_CONFIG_DEFAULT)) { - self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT); + if (!file_exists(__DIR__ . '/../config.default.ini.php')) { + throw new \Exception('The default configuration file is missing'); } - - $config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED); + $config = parse_ini_file(__DIR__ . '/../config.default.ini.php', true, INI_SCANNER_TYPED); if (!$config) { - self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT); + throw new \Exception('Error parsing config'); } - - 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) { - $config[$header][$key] = $value; - } + foreach ($config as $header => $section) { + foreach ($section as $key => $value) { + self::setConfig($header, $key, $value); } } - - foreach (getenv() as $envName => $envValue) { - // Replace all settings with their respective environment variable if available - $keyArray = explode('_', $envName); - if ($keyArray[0] === 'RSSBRIDGE') { - $header = strtolower($keyArray[1]); - $key = strtolower($keyArray[2]); + foreach ($customConfig as $header => $section) { + foreach ($section as $key => $value) { + self::setConfig($header, $key, $value); + } + } + foreach ($env as $envName => $envValue) { + $nameParts = explode('_', $envName); + if ($nameParts[0] === 'RSSBRIDGE') { + $header = $nameParts[1]; + $key = $nameParts[2]; if ($envValue === 'true' || $envValue === 'false') { $envValue = filter_var($envValue, FILTER_VALIDATE_BOOLEAN); } - $config[$header][$key] = $envValue; + self::setConfig($header, $key, $envValue); } } - self::$config = $config; - if ( !is_string(self::getConfig('system', 'timezone')) || !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'))) { - 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'))) { - 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'))) { /** 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'))) { - 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'))) { - 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'))) { - self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean'); + self::throwConfigError('authentication', 'enable', 'Is not a valid Boolean'); } 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')) { - self::reportConfigurationError('authentication', 'password', 'Is not a valid string'); + self::throwConfigError('authentication', 'password', 'Is not a valid string'); } if ( !empty(self::getConfig('admin', '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'))) { - 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'))) { - self::reportConfigurationError('error', 'output', 'Is not a valid String'); + self::throwConfigError('error', 'output', 'Is not a valid String'); } if ( !is_numeric(self::getConfig('error', 'report_limit')) || self::getConfig('error', 'report_limit') < 1 ) { - self::reportConfigurationError('admin', 'report_limit', 'Value is invalid'); + self::throwConfigError('admin', 'report_limit', 'Value is invalid'); } } - /** - * 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) + public static function getConfig(string $section, string $key) { - if (array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) { - return self::$config[$section][$key]; - } - - return null; + return self::$config[strtolower($section)][strtolower($key)] ?? 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() { $headFile = __DIR__ . '/../.git/HEAD'; - // '@' is used to mute open_basedir warning if (@is_readable($headFile)) { $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1); $parts = explode('/', $revisionHashFile); @@ -260,39 +198,11 @@ final class Configuration } } } - - return Configuration::$VERSION; + return self::VERSION; } - /** - * 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 = '') + private static function throwConfigError($section, $key, $message = '') { - $report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL; - - 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)); + throw new \Exception("Config [$section] => [$key] is invalid. $message"); } } diff --git a/lib/FeedItem.php b/lib/FeedItem.php index 4435e0e0..a5aa7035 100644 --- a/lib/FeedItem.php +++ b/lib/FeedItem.php @@ -314,8 +314,7 @@ class FeedItem * * Use {@see FeedItem::getContent()} to get the current item content. * - * @param string|object $content The item content as text or simple_html_dom - * object. + * @param string|object $content The item content as text or simple_html_dom object. * @return self */ public function setContent($content) diff --git a/lib/RssBridge.php b/lib/RssBridge.php index 76873025..a4a434e3 100644 --- a/lib/RssBridge.php +++ b/lib/RssBridge.php @@ -33,12 +33,15 @@ final class RssBridge private function run($request): void { 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')); - define('CUSTOM_CACHE_TIMEOUT', Configuration::getConfig('cache', 'custom_timeout')); - $authenticationMiddleware = new AuthenticationMiddleware(); if (Configuration::getConfig('authentication', 'enable')) { $authenticationMiddleware(); diff --git a/lib/rssbridge.php b/lib/rssbridge.php index b1261ffd..8e5cf69c 100644 --- a/lib/rssbridge.php +++ b/lib/rssbridge.php @@ -36,12 +36,6 @@ const WHITELIST = __DIR__ . '/../whitelist.txt'; /** Path to the default whitelist file */ 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 */ const REPOSITORY = 'https://github.com/RSS-Bridge/rss-bridge/'; diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index 43ae64a3..e913e463 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -9,17 +9,30 @@ use PHPUnit\Framework\TestCase; final class ConfigurationTest extends TestCase { - public function test() + public function testValueFromDefaultConfig() { - putenv('RSSBRIDGE_system_timezone=Europe/Berlin'); Configuration::loadConfiguration(); - - // test nonsense $this->assertSame(null, Configuration::getConfig('foobar', '')); $this->assertSame(null, Configuration::getConfig('foo', 'bar')); $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')); } + + 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')); + } }