<?php
/**
 * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
 * Atom feeds for websites that don't have one.
 *
 * For the full license information, please view the UNLICENSE file distributed
 * with this source code.
 *
 * @package	Core
 * @license	http://unlicense.org/ UNLICENSE
 * @link	https://github.com/rss-bridge/rss-bridge
 */

/**
 * Configuration module for RSS-Bridge.
 *
 * This class implements a configuration module for RSS-Bridge.
 */
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.2020-02-26';

	/**
	 * Holds the configuration data.
	 *
	 * Do not access this property directly!
	 * Use {@see Configuration::getConfig()} instead.
	 *
	 * @var array|null
	 */
	private static $config = null;

	/**
	 * Throw an exception when trying to create a new instance of this class.
	 *
	 * @throws \LogicException if called.
	 */
	public function __construct(){
		throw new \LogicException('Can\'t create object of this class!');
	}

	/**
	 * Verifies the current installation of RSS-Bridge and PHP.
	 *
	 * Returns an error message and aborts execution if the installation does
	 * not satisfy the requirements of RSS-Bridge.
	 *
	 * **Requirements**
	 * - PHP 5.6.0 or higher
	 * - `openssl` extension
	 * - `libxml` extension
	 * - `mbstring` extension
	 * - `simplexml` extension
	 * - `curl` extension
	 * - `json` extension
	 * - The cache folder specified by {@see PATH_CACHE} requires write permission
	 * - The whitelist file specified by {@see WHITELIST} requires write permission
	 *
	 * @link http://php.net/supported-versions.php PHP Supported Versions
	 * @link http://php.net/manual/en/book.openssl.php OpenSSL
	 * @link http://php.net/manual/en/book.libxml.php libxml
	 * @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring)
	 * @link http://php.net/manual/en/book.simplexml.php SimpleXML
	 * @link http://php.net/manual/en/book.curl.php Client URL Library (curl)
	 * @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json)
	 *
	 * @return void
	 */
	public static function verifyInstallation() {

		// Check PHP version
		if(version_compare(PHP_VERSION, '5.6.0') === -1)
			self::reportError('RSS-Bridge requires at least PHP version 5.6.0!');

		// extensions check
		if(!extension_loaded('openssl'))
			self::reportError('"openssl" extension not loaded. Please check "php.ini"');

		if(!extension_loaded('libxml'))
			self::reportError('"libxml" extension not loaded. Please check "php.ini"');

		if(!extension_loaded('mbstring'))
			self::reportError('"mbstring" extension not loaded. Please check "php.ini"');

		if(!extension_loaded('simplexml'))
			self::reportError('"simplexml" extension not loaded. Please check "php.ini"');

		// Allow RSS-Bridge to run without curl module in CLI mode without root certificates
		if(!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))))
			self::reportError('"curl" extension not loaded. Please check "php.ini"');

		if(!extension_loaded('json'))
			self::reportError('"json" extension not loaded. Please check "php.ini"');

		// Check cache folder permissions (write permissions required)
		if(!is_writable(PATH_CACHE))
			self::reportError('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');

	}

	/**
	 * 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))
			self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT);

		Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED);
		if(!Configuration::$config)
			self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT);

		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) {
					// Skip unknown sections and keys
					if(array_key_exists($header, Configuration::$config) && array_key_exists($key, Configuration::$config[$header])) {
						Configuration::$config[$header][$key] = $value;
					}
				}
			}
		}

		if(!is_string(self::getConfig('system', 'timezone'))
		|| !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
			self::reportConfigurationError('system', 'timezone');

		date_default_timezone_set(self::getConfig('system', 'timezone'));

		if(!is_string(self::getConfig('proxy', 'url')))
			self::reportConfigurationError('proxy', 'url', 'Is not a valid string');

		if(!empty(self::getConfig('proxy', 'url'))) {
			/** URL of the proxy server */
			define('PROXY_URL', self::getConfig('proxy', 'url'));
		}

		if(!is_bool(self::getConfig('proxy', 'by_bridge')))
			self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean');

		/** True if proxy usage can be enabled selectively for each bridge */
		define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));

		if(!is_string(self::getConfig('proxy', 'name')))
			self::reportConfigurationError('proxy', 'name', 'Is not a valid string');

		/** Name of the proxy server */
		define('PROXY_NAME', self::getConfig('proxy', 'name'));

		if(!is_string(self::getConfig('cache', 'type')))
			self::reportConfigurationError('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');

		/** True if the cache timeout can be specified by the user */
		define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));

		if(!is_bool(self::getConfig('authentication', 'enable')))
			self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean');

		if(!is_string(self::getConfig('authentication', 'username')))
			self::reportConfigurationError('authentication', 'username', 'Is not a valid string');

		if(!is_string(self::getConfig('authentication', 'password')))
			self::reportConfigurationError('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');

		if(!is_string(self::getConfig('error', 'output')))
			self::reportConfigurationError('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');

	}

	/**
	 * 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[$section][$key];
		}

		return null;

	}

	/**
	 * 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 = PATH_ROOT . '.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);

			if(isset($parts[3])) {
				$branchName = $parts[3];
				if(file_exists($revisionHashFile)) {
					return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7);
				}
			}
		}

		return Configuration::$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 = '') {

		$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);

	}

	/**
	 * Reports an error message to the user and ends execution
	 *
	 * @param string $message The error message
	 *
	 * @return void
	 */
	private static function reportError($message) {

		header('Content-Type: text/plain', true, 500);
		die('Configuration error' . PHP_EOL . $message);

	}
}