diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 978fc9da..e67b2902 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -132,11 +132,11 @@ public static function checkPHPVersion($minVersion, $curVersion) /** * Checks Shaarli has the proper access permissions to its resources * - * @param array $globalConfig The $GLOBALS['config'] array + * @param ConfigManager $conf Configuration Manager instance. * * @return array A list of the detected configuration issues */ - public static function checkResourcePermissions($globalConfig) + public static function checkResourcePermissions($conf) { $errors = array(); @@ -145,7 +145,7 @@ public static function checkResourcePermissions($globalConfig) 'application', 'inc', 'plugins', - $globalConfig['RAINTPL_TPL'] + $conf->get('resource.raintpl_tpl'), ) as $path) { if (! is_readable(realpath($path))) { $errors[] = '"'.$path.'" directory is not readable'; @@ -154,10 +154,10 @@ public static function checkResourcePermissions($globalConfig) // Check cache and data directories are readable and writeable foreach (array( - $globalConfig['CACHEDIR'], - $globalConfig['DATADIR'], - $globalConfig['PAGECACHE'], - $globalConfig['RAINTPL_TMP'] + $conf->get('resource.thumbnails_cache'), + $conf->get('resource.data_dir'), + $conf->get('resource.page_cache'), + $conf->get('resource.raintpl_tmp'), ) as $path) { if (! is_readable(realpath($path))) { $errors[] = '"'.$path.'" directory is not readable'; @@ -169,11 +169,11 @@ public static function checkResourcePermissions($globalConfig) // Check configuration files are readable and writeable foreach (array( - $globalConfig['CONFIG_FILE'], - $globalConfig['DATASTORE'], - $globalConfig['IPBANS_FILENAME'], - $globalConfig['LOG_FILE'], - $globalConfig['UPDATECHECK_FILENAME'] + $conf->getConfigFileExt(), + $conf->get('resource.datastore'), + $conf->get('resource.ban_file'), + $conf->get('resource.log'), + $conf->get('resource.update_check'), ) as $path) { if (! is_file(realpath($path))) { # the file may not exist yet diff --git a/application/Config.php b/application/Config.php deleted file mode 100644 index 05a59452..00000000 --- a/application/Config.php +++ /dev/null @@ -1,221 +0,0 @@ - $value) { - $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($config['config'][$key], true).';'. PHP_EOL; - } - - if (isset($config['plugins'])) { - foreach ($config['plugins'] as $key => $value) { - $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($config['plugins'][$key], true).';'. PHP_EOL; - } - } - - if (!file_put_contents($config['config']['CONFIG_FILE'], $configStr) - || strcmp(file_get_contents($config['config']['CONFIG_FILE']), $configStr) != 0 - ) { - throw new Exception( - 'Shaarli could not create the config file. - Please make sure Shaarli has the right to write in the folder is it installed in.' - ); - } -} - -/** - * Process plugin administration form data and save it in an array. - * - * @param array $formData Data sent by the plugin admin form. - * - * @return array New list of enabled plugin, ordered. - * - * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid. - */ -function save_plugin_config($formData) -{ - // Make sure there are no duplicates in orders. - if (!validate_plugin_order($formData)) { - throw new PluginConfigOrderException(); - } - - $plugins = array(); - $newEnabledPlugins = array(); - foreach ($formData as $key => $data) { - if (startsWith($key, 'order')) { - continue; - } - - // If there is no order, it means a disabled plugin has been enabled. - if (isset($formData['order_' . $key])) { - $plugins[(int) $formData['order_' . $key]] = $key; - } - else { - $newEnabledPlugins[] = $key; - } - } - - // New enabled plugins will be added at the end of order. - $plugins = array_merge($plugins, $newEnabledPlugins); - - // Sort plugins by order. - if (!ksort($plugins)) { - throw new PluginConfigOrderException(); - } - - $finalPlugins = array(); - // Make plugins order continuous. - foreach ($plugins as $plugin) { - $finalPlugins[] = $plugin; - } - - return $finalPlugins; -} - -/** - * Validate plugin array submitted. - * Will fail if there is duplicate orders value. - * - * @param array $formData Data from submitted form. - * - * @return bool true if ok, false otherwise. - */ -function validate_plugin_order($formData) -{ - $orders = array(); - foreach ($formData as $key => $value) { - // No duplicate order allowed. - if (in_array($value, $orders)) { - return false; - } - - if (startsWith($key, 'order')) { - $orders[] = $value; - } - } - - return true; -} - -/** - * Affect plugin parameters values into plugins array. - * - * @param mixed $plugins Plugins array ($plugins[]['parameters']['param_name'] = . - * @param mixed $config Plugins configuration. - * - * @return mixed Updated $plugins array. - */ -function load_plugin_parameter_values($plugins, $config) -{ - $out = $plugins; - foreach ($plugins as $name => $plugin) { - if (empty($plugin['parameters'])) { - continue; - } - - foreach ($plugin['parameters'] as $key => $param) { - if (!empty($config[$key])) { - $out[$name]['parameters'][$key] = $config[$key]; - } - } - } - - return $out; -} - -/** - * Exception used if a mandatory field is missing in given configuration. - */ -class MissingFieldConfigException extends Exception -{ - public $field; - - /** - * Construct exception. - * - * @param string $field field name missing. - */ - public function __construct($field) - { - $this->field = $field; - $this->message = 'Configuration value is required for '. $this->field; - } -} - -/** - * Exception used if an unauthorized attempt to edit configuration has been made. - */ -class UnauthorizedConfigException extends Exception -{ - /** - * Construct exception. - */ - public function __construct() - { - $this->message = 'You are not authorized to alter config.'; - } -} - -/** - * Exception used if an error occur while saving plugin configuration. - */ -class PluginConfigOrderException extends Exception -{ - /** - * Construct exception. - */ - public function __construct() - { - $this->message = 'An error occurred while trying to save plugins loading order.'; - } -} diff --git a/application/FileUtils.php b/application/FileUtils.php index 6a12ef0e..6cac9825 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -9,11 +9,13 @@ class IOException extends Exception /** * Construct a new IOException * - * @param string $path path to the ressource that cannot be accessed + * @param string $path path to the resource that cannot be accessed + * @param string $message Custom exception message. */ - public function __construct($path) + public function __construct($path, $message = '') { $this->path = $path; - $this->message = 'Error accessing '.$this->path; + $this->message = empty($message) ? 'Error accessing' : $message; + $this->message .= PHP_EOL . $this->path; } } diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 82580787..7cd88370 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -14,13 +14,21 @@ class PageBuilder */ private $tpl; + /** + * @var ConfigManager $conf Configuration Manager instance. + */ + protected $conf; + /** * PageBuilder constructor. * $tpl is initialized at false for lazy loading. + * + * @param ConfigManager $conf Configuration Manager instance (reference). */ - function __construct() + function __construct(&$conf) { $this->tpl = false; + $this->conf = $conf; } /** @@ -33,17 +41,17 @@ private function initialize() try { $version = ApplicationUtils::checkUpdate( shaarli_version, - $GLOBALS['config']['UPDATECHECK_FILENAME'], - $GLOBALS['config']['UPDATECHECK_INTERVAL'], - $GLOBALS['config']['ENABLE_UPDATECHECK'], + $this->conf->get('resource.update_check'), + $this->conf->get('updates.check_updates_interval'), + $this->conf->get('updates.check_updates'), isLoggedIn(), - $GLOBALS['config']['UPDATECHECK_BRANCH'] + $this->conf->get('updates.check_updates_branch') ); $this->tpl->assign('newVersion', escape($version)); $this->tpl->assign('versionError', ''); } catch (Exception $exc) { - logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], $exc->getMessage()); + logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); $this->tpl->assign('newVersion', ''); $this->tpl->assign('versionError', escape($exc->getMessage())); } @@ -62,19 +70,24 @@ private function initialize() $this->tpl->assign('scripturl', index_url($_SERVER)); $this->tpl->assign('pagetitle', 'Shaarli'); $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? - if (!empty($GLOBALS['title'])) { - $this->tpl->assign('pagetitle', $GLOBALS['title']); + if ($this->conf->exists('general.title')) { + $this->tpl->assign('pagetitle', $this->conf->get('general.title')); } - if (!empty($GLOBALS['titleLink'])) { - $this->tpl->assign('titleLink', $GLOBALS['titleLink']); + if ($this->conf->exists('general.header_link')) { + $this->tpl->assign('titleLink', $this->conf->get('general.header_link')); } - if (!empty($GLOBALS['pagetitle'])) { - $this->tpl->assign('pagetitle', $GLOBALS['pagetitle']); + if ($this->conf->exists('pagetitle')) { + $this->tpl->assign('pagetitle', $this->conf->get('pagetitle')); } - $this->tpl->assign('shaarlititle', empty($GLOBALS['title']) ? 'Shaarli': $GLOBALS['title']); + $this->tpl->assign('shaarlititle', $this->conf->get('title', 'Shaarli')); + $this->tpl->assign('openshaarli', $this->conf->get('security.open_shaarli', false)); + $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', false)); + $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); if (!empty($GLOBALS['plugin_errors'])) { $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); } + // To be removed with a proper theme configuration. + $this->tpl->assign('conf', $this->conf); } /** @@ -85,7 +98,6 @@ private function initialize() */ public function assign($placeholder, $value) { - // Lazy initialization if ($this->tpl === false) { $this->initialize(); } @@ -101,7 +113,6 @@ public function assign($placeholder, $value) */ public function assignAll($data) { - // Lazy initialization if ($this->tpl === false) { $this->initialize(); } @@ -113,6 +124,7 @@ public function assignAll($data) foreach ($data as $key => $value) { $this->assign($key, $value); } + return true; } /** @@ -123,10 +135,10 @@ public function assignAll($data) */ public function renderPage($page) { - // Lazy initialization - if ($this->tpl===false) { + if ($this->tpl === false) { $this->initialize(); } + $this->tpl->draw($page); } diff --git a/application/PluginManager.php b/application/PluginManager.php index 787ac6a9..dca7e63e 100644 --- a/application/PluginManager.php +++ b/application/PluginManager.php @@ -4,17 +4,9 @@ * Class PluginManager * * Use to manage, load and execute plugins. - * - * Using Singleton design pattern. */ class PluginManager { - /** - * PluginManager singleton instance. - * @var PluginManager $instance - */ - private static $instance; - /** * List of authorized plugins from configuration file. * @var array $authorizedPlugins @@ -27,6 +19,11 @@ class PluginManager */ private $loadedPlugins = array(); + /** + * @var ConfigManager Configuration Manager instance. + */ + protected $conf; + /** * Plugins subdirectory. * @var string $PLUGINS_PATH @@ -40,33 +37,13 @@ class PluginManager public static $META_EXT = 'meta'; /** - * Private constructor: new instances not allowed. - */ - private function __construct() - { - } - - /** - * Cloning isn't allowed either. + * Constructor. * - * @return void + * @param ConfigManager $conf Configuration Manager instance. */ - private function __clone() + public function __construct(&$conf) { - } - - /** - * Return existing instance of PluginManager, or create it. - * - * @return PluginManager instance. - */ - public static function getInstance() - { - if (!(self::$instance instanceof self)) { - self::$instance = new self(); - } - - return self::$instance; + $this->conf = $conf; } /** @@ -102,9 +79,9 @@ public function load($authorizedPlugins) /** * Execute all plugins registered hook. * - * @param string $hook name of the hook to trigger. - * @param array $data list of data to manipulate passed by reference. - * @param array $params additional parameters such as page target. + * @param string $hook name of the hook to trigger. + * @param array $data list of data to manipulate passed by reference. + * @param array $params additional parameters such as page target. * * @return void */ @@ -122,7 +99,7 @@ public function executeHooks($hook, &$data, $params = array()) $hookFunction = $this->buildHookName($hook, $plugin); if (function_exists($hookFunction)) { - $data = call_user_func($hookFunction, $data); + $data = call_user_func($hookFunction, $data, $this->conf); } } } @@ -148,6 +125,7 @@ private function loadPlugin($dir, $pluginName) throw new PluginFileNotFoundException($pluginName); } + $conf = $this->conf; include_once $pluginFilePath; $this->loadedPlugins[] = $pluginName; diff --git a/application/Updater.php b/application/Updater.php index 58c13c07..fd45d17f 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -12,16 +12,16 @@ class Updater */ protected $doneUpdates; - /** - * @var array Shaarli's configuration array. - */ - protected $config; - /** * @var LinkDB instance. */ protected $linkDB; + /** + * @var ConfigManager $conf Configuration Manager instance. + */ + protected $conf; + /** * @var bool True if the user is logged in, false otherwise. */ @@ -35,16 +35,16 @@ class Updater /** * Object constructor. * - * @param array $doneUpdates Updates which are already done. - * @param array $config Shaarli's configuration array. - * @param LinkDB $linkDB LinkDB instance. - * @param boolean $isLoggedIn True if the user is logged in. + * @param array $doneUpdates Updates which are already done. + * @param LinkDB $linkDB LinkDB instance. + * @oaram ConfigManager $conf Configuration Manager instance. + * @param boolean $isLoggedIn True if the user is logged in. */ - public function __construct($doneUpdates, $config, $linkDB, $isLoggedIn) + public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) { $this->doneUpdates = $doneUpdates; - $this->config = $config; $this->linkDB = $linkDB; + $this->conf = $conf; $this->isLoggedIn = $isLoggedIn; // Retrieve all update methods. @@ -114,19 +114,19 @@ public function getDoneUpdates() */ public function updateMethodMergeDeprecatedConfigFile() { - $config_file = $this->config['config']['CONFIG_FILE']; - - if (is_file($this->config['config']['DATADIR'].'/options.php')) { - include $this->config['config']['DATADIR'].'/options.php'; + if (is_file($this->conf->get('resource.data_dir') . '/options.php')) { + include $this->conf->get('resource.data_dir') . '/options.php'; // Load GLOBALS into config + $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS); + $allowedKeys[] = 'config'; foreach ($GLOBALS as $key => $value) { - $this->config[$key] = $value; + if (in_array($key, $allowedKeys)) { + $this->conf->set($key, $value); + } } - $this->config['config']['CONFIG_FILE'] = $config_file; - writeConfig($this->config, $this->isLoggedIn); - - unlink($this->config['config']['DATADIR'].'/options.php'); + $this->conf->write($this->isLoggedIn); + unlink($this->conf->get('resource.data_dir').'/options.php'); } return true; @@ -143,7 +143,76 @@ public function updateMethodRenameDashTags() $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); $this->linkDB[$link['linkdate']] = $link; } - $this->linkDB->savedb($this->config['config']['PAGECACHE']); + $this->linkDB->savedb($this->conf->get('resource.page_cache')); + return true; + } + + /** + * Move old configuration in PHP to the new config system in JSON format. + * + * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'. + * It will also convert legacy setting keys to the new ones. + */ + public function updateMethodConfigToJson() + { + // JSON config already exists, nothing to do. + if ($this->conf->getConfigIO() instanceof ConfigJson) { + return true; + } + + $configPhp = new ConfigPhp(); + $configJson = new ConfigJson(); + $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php'); + rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php'); + $this->conf->setConfigIO($configJson); + $this->conf->reload(); + + $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING); + foreach (ConfigPhp::$ROOT_KEYS as $key) { + $this->conf->set($legacyMap[$key], $oldConfig[$key]); + } + + // Set sub config keys (config and plugins) + $subConfig = array('config', 'plugins'); + foreach ($subConfig as $sub) { + foreach ($oldConfig[$sub] as $key => $value) { + if (isset($legacyMap[$sub .'.'. $key])) { + $configKey = $legacyMap[$sub .'.'. $key]; + } else { + $configKey = $sub .'.'. $key; + } + $this->conf->set($configKey, $value); + } + } + + try{ + $this->conf->write($this->isLoggedIn); + return true; + } catch (IOException $e) { + error_log($e->getMessage()); + return false; + } + } + + /** + * Escape settings which have been manually escaped in every request in previous versions: + * - general.title + * - general.header_link + * - extras.redirector + * + * @return bool true if the update is successful, false otherwise. + */ + public function escapeUnescapedConfig() + { + try { + $this->conf->set('general.title', escape($this->conf->get('general.title'))); + $this->conf->set('general.header_link', escape($this->conf->get('general.header_link'))); + $this->conf->set('redirector.url', escape($this->conf->get('redirector.url'))); + $this->conf->write($this->isLoggedIn); + } catch (Exception $e) { + error_log($e->getMessage()); + return false; + } return true; } } @@ -203,7 +272,6 @@ private function buildMessage($message) } } - /** * Read the updates file, and return already done updates. * diff --git a/application/Utils.php b/application/Utils.php index da521cce..9a8ca6d1 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -273,4 +273,4 @@ function autoLocale($headerLocale) } } setlocale(LC_ALL, $attempts); -} \ No newline at end of file +} diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php new file mode 100644 index 00000000..2b68fe6a --- /dev/null +++ b/application/config/ConfigIO.php @@ -0,0 +1,33 @@ +'; + } +} diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php new file mode 100644 index 00000000..ff41772a --- /dev/null +++ b/application/config/ConfigManager.php @@ -0,0 +1,392 @@ +configFile = $configFile; + $this->initialize(); + } + + /** + * Reset the ConfigManager instance. + */ + public function reset() + { + $this->initialize(); + } + + /** + * Rebuild the loaded config array from config files. + */ + public function reload() + { + $this->load(); + } + + /** + * Initialize the ConfigIO and loaded the conf. + */ + protected function initialize() + { + if (file_exists($this->configFile . '.php')) { + $this->configIO = new ConfigPhp(); + } else { + $this->configIO = new ConfigJson(); + } + $this->load(); + } + + /** + * Load configuration in the ConfigurationManager. + */ + protected function load() + { + $this->loadedConfig = $this->configIO->read($this->getConfigFileExt()); + $this->setDefaultValues(); + } + + /** + * Get a setting. + * + * Supports nested settings with dot separated keys. + * Eg. 'config.stuff.option' will find $conf[config][stuff][option], + * or in JSON: + * { "config": { "stuff": {"option": "mysetting" } } } } + * + * @param string $setting Asked setting, keys separated with dots. + * @param string $default Default value if not found. + * + * @return mixed Found setting, or the default value. + */ + public function get($setting, $default = '') + { + // During the ConfigIO transition, map legacy settings to the new ones. + if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) { + $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting]; + } + + $settings = explode('.', $setting); + $value = self::getConfig($settings, $this->loadedConfig); + if ($value === self::$NOT_FOUND) { + return $default; + } + return $value; + } + + /** + * Set a setting, and eventually write it. + * + * Supports nested settings with dot separated keys. + * + * @param string $setting Asked setting, keys separated with dots. + * @param string $value Value to set. + * @param bool $write Write the new setting in the config file, default false. + * @param bool $isLoggedIn User login state, default false. + * + * @throws Exception Invalid + */ + public function set($setting, $value, $write = false, $isLoggedIn = false) + { + if (empty($setting) || ! is_string($setting)) { + throw new Exception('Invalid setting key parameter. String expected, got: '. gettype($setting)); + } + + // During the ConfigIO transition, map legacy settings to the new ones. + if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) { + $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting]; + } + + $settings = explode('.', $setting); + self::setConfig($settings, $value, $this->loadedConfig); + if ($write) { + $this->write($isLoggedIn); + } + } + + /** + * Check if a settings exists. + * + * Supports nested settings with dot separated keys. + * + * @param string $setting Asked setting, keys separated with dots. + * + * @return bool true if the setting exists, false otherwise. + */ + public function exists($setting) + { + // During the ConfigIO transition, map legacy settings to the new ones. + if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) { + $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting]; + } + + $settings = explode('.', $setting); + $value = self::getConfig($settings, $this->loadedConfig); + if ($value === self::$NOT_FOUND) { + return false; + } + return true; + } + + /** + * Call the config writer. + * + * @param bool $isLoggedIn User login state. + * + * @return bool True if the configuration has been successfully written, false otherwise. + * + * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf. + * @throws UnauthorizedConfigException: user is not authorize to change configuration. + * @throws IOException: an error occurred while writing the new config file. + */ + public function write($isLoggedIn) + { + // These fields are required in configuration. + $mandatoryFields = array( + 'credentials.login', + 'credentials.hash', + 'credentials.salt', + 'security.session_protection_disabled', + 'general.timezone', + 'general.title', + 'general.header_link', + 'privacy.default_private_links', + 'redirector.url', + ); + + // Only logged in user can alter config. + if (is_file($this->getConfigFileExt()) && !$isLoggedIn) { + throw new UnauthorizedConfigException(); + } + + // Check that all mandatory fields are provided in $conf. + foreach ($mandatoryFields as $field) { + if (! $this->exists($field)) { + throw new MissingFieldConfigException($field); + } + } + + return $this->configIO->write($this->getConfigFileExt(), $this->loadedConfig); + } + + /** + * Set the config file path (without extension). + * + * @param string $configFile File path. + */ + public function setConfigFile($configFile) + { + $this->configFile = $configFile; + } + + /** + * Return the configuration file path (without extension). + * + * @return string Config path. + */ + public function getConfigFile() + { + return $this->configFile; + } + + /** + * Get the configuration file path with its extension. + * + * @return string Config file path. + */ + public function getConfigFileExt() + { + return $this->configFile . $this->configIO->getExtension(); + } + + /** + * Recursive function which find asked setting in the loaded config. + * + * @param array $settings Ordered array which contains keys to find. + * @param array $conf Loaded settings, then sub-array. + * + * @return mixed Found setting or NOT_FOUND flag. + */ + protected static function getConfig($settings, $conf) + { + if (!is_array($settings) || count($settings) == 0) { + return self::$NOT_FOUND; + } + + $setting = array_shift($settings); + if (!isset($conf[$setting])) { + return self::$NOT_FOUND; + } + + if (count($settings) > 0) { + return self::getConfig($settings, $conf[$setting]); + } + return $conf[$setting]; + } + + /** + * Recursive function which find asked setting in the loaded config. + * + * @param array $settings Ordered array which contains keys to find. + * @param mixed $value + * @param array $conf Loaded settings, then sub-array. + * + * @return mixed Found setting or NOT_FOUND flag. + */ + protected static function setConfig($settings, $value, &$conf) + { + if (!is_array($settings) || count($settings) == 0) { + return self::$NOT_FOUND; + } + + $setting = array_shift($settings); + if (count($settings) > 0) { + return self::setConfig($settings, $value, $conf[$setting]); + } + $conf[$setting] = $value; + } + + /** + * Set a bunch of default values allowing Shaarli to start without a config file. + */ + protected function setDefaultValues() + { + $this->setEmpty('resource.data_dir', 'data'); + $this->setEmpty('resource.config', 'data/config.php'); + $this->setEmpty('resource.datastore', 'data/datastore.php'); + $this->setEmpty('resource.ban_file', 'data/ipbans.php'); + $this->setEmpty('resource.updates', 'data/updates.txt'); + $this->setEmpty('resource.log', 'data/log.txt'); + $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); + $this->setEmpty('resource.raintpl_tpl', 'tpl/'); + $this->setEmpty('resource.raintpl_tmp', 'tmp/'); + $this->setEmpty('resource.thumbnails_cache', 'cache'); + $this->setEmpty('resource.page_cache', 'pagecache'); + + $this->setEmpty('security.ban_after', 4); + $this->setEmpty('security.ban_duration', 1800); + $this->setEmpty('security.session_protection_disabled', false); + $this->setEmpty('security.open_shaarli', false); + + $this->setEmpty('general.header_link', '?'); + $this->setEmpty('general.links_per_page', 20); + $this->setEmpty('general.enabled_plugins', array('qrcode')); + + $this->setEmpty('updates.check_updates', false); + $this->setEmpty('updates.check_updates_branch', 'stable'); + $this->setEmpty('updates.check_updates_interval', 86400); + + $this->setEmpty('feed.rss_permalinks', true); + $this->setEmpty('feed.show_atom', false); + + $this->setEmpty('privacy.default_private_links', false); + $this->setEmpty('privacy.hide_public_links', false); + $this->setEmpty('privacy.hide_timestamps', false); + + $this->setEmpty('thumbnail.enable_thumbnails', true); + $this->setEmpty('thumbnail.enable_localcache', true); + + $this->setEmpty('redirector.url', ''); + $this->setEmpty('redirector.encode_url', true); + + $this->setEmpty('plugins', array()); + } + + /** + * Set only if the setting does not exists. + * + * @param string $key Setting key. + * @param mixed $value Setting value. + */ + public function setEmpty($key, $value) + { + if (! $this->exists($key)) { + $this->set($key, $value); + } + } + + /** + * @return ConfigIO + */ + public function getConfigIO() + { + return $this->configIO; + } + + /** + * @param ConfigIO $configIO + */ + public function setConfigIO($configIO) + { + $this->configIO = $configIO; + } +} + +/** + * Exception used if a mandatory field is missing in given configuration. + */ +class MissingFieldConfigException extends Exception +{ + public $field; + + /** + * Construct exception. + * + * @param string $field field name missing. + */ + public function __construct($field) + { + $this->field = $field; + $this->message = 'Configuration value is required for '. $this->field; + } +} + +/** + * Exception used if an unauthorized attempt to edit configuration has been made. + */ +class UnauthorizedConfigException extends Exception +{ + /** + * Construct exception. + */ + public function __construct() + { + $this->message = 'You are not authorized to alter config.'; + } +} diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php new file mode 100644 index 00000000..27187b66 --- /dev/null +++ b/application/config/ConfigPhp.php @@ -0,0 +1,132 @@ + will actually look for . + * The Updater will use this array to transform keys when switching to JSON. + * + * @var array current key => legacy key. + */ + public static $LEGACY_KEYS_MAPPING = array( + 'credentials.login' => 'login', + 'credentials.hash' => 'hash', + 'credentials.salt' => 'salt', + 'resource.data_dir' => 'config.DATADIR', + 'resource.config' => 'config.CONFIG_FILE', + 'resource.datastore' => 'config.DATASTORE', + 'resource.updates' => 'config.UPDATES_FILE', + 'resource.log' => 'config.LOG_FILE', + 'resource.update_check' => 'config.UPDATECHECK_FILENAME', + 'resource.raintpl_tpl' => 'config.RAINTPL_TPL', + 'resource.raintpl_tmp' => 'config.RAINTPL_TMP', + 'resource.thumbnails_cache' => 'config.CACHEDIR', + 'resource.page_cache' => 'config.PAGECACHE', + 'resource.ban_file' => 'config.IPBANS_FILENAME', + 'security.session_protection_disabled' => 'disablesessionprotection', + 'security.ban_after' => 'config.BAN_AFTER', + 'security.ban_duration' => 'config.BAN_DURATION', + 'general.title' => 'title', + 'general.timezone' => 'timezone', + 'general.header_link' => 'titleLink', + 'updates.check_updates' => 'config.ENABLE_UPDATECHECK', + 'updates.check_updates_branch' => 'config.UPDATECHECK_BRANCH', + 'updates.check_updates_interval' => 'config.UPDATECHECK_INTERVAL', + 'privacy.default_private_links' => 'privateLinkByDefault', + 'feed.rss_permalinks' => 'config.ENABLE_RSS_PERMALINKS', + 'general.links_per_page' => 'config.LINKS_PER_PAGE', + 'thumbnail.enable_thumbnails' => 'config.ENABLE_THUMBNAILS', + 'thumbnail.enable_localcache' => 'config.ENABLE_LOCALCACHE', + 'general.enabled_plugins' => 'config.ENABLED_PLUGINS', + 'redirector.url' => 'redirector', + 'redirector.encode_url' => 'config.REDIRECTOR_URLENCODE', + 'feed.show_atom' => 'config.SHOW_ATOM', + 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS', + 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS', + 'security.open_shaarli' => 'config.OPEN_SHAARLI', + ); + + /** + * @inheritdoc + */ + function read($filepath) + { + if (! file_exists($filepath) || ! is_readable($filepath)) { + return array(); + } + + include $filepath; + + $out = array(); + foreach (self::$ROOT_KEYS as $key) { + $out[$key] = $GLOBALS[$key]; + } + $out['config'] = $GLOBALS['config']; + $out['plugins'] = !empty($GLOBALS['plugins']) ? $GLOBALS['plugins'] : array(); + return $out; + } + + /** + * @inheritdoc + */ + function write($filepath, $conf) + { + $configStr = ' $value) { + $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL; + } + + if (isset($conf['plugins'])) { + foreach ($conf['plugins'] as $key => $value) { + $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($conf['plugins'][$key], true).';'. PHP_EOL; + } + } + + if (!file_put_contents($filepath, $configStr) + || strcmp(file_get_contents($filepath), $configStr) != 0 + ) { + throw new IOException( + $filepath, + 'Shaarli could not create the config file. + Please make sure Shaarli has the right to write in the folder is it installed in.' + ); + } + } + + /** + * @inheritdoc + */ + function getExtension() + { + return '.php'; + } +} diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php new file mode 100644 index 00000000..047d2b03 --- /dev/null +++ b/application/config/ConfigPlugin.php @@ -0,0 +1,120 @@ + $data) { + if (startsWith($key, 'order')) { + continue; + } + + // If there is no order, it means a disabled plugin has been enabled. + if (isset($formData['order_' . $key])) { + $plugins[(int) $formData['order_' . $key]] = $key; + } + else { + $newEnabledPlugins[] = $key; + } + } + + // New enabled plugins will be added at the end of order. + $plugins = array_merge($plugins, $newEnabledPlugins); + + // Sort plugins by order. + if (!ksort($plugins)) { + throw new PluginConfigOrderException(); + } + + $finalPlugins = array(); + // Make plugins order continuous. + foreach ($plugins as $plugin) { + $finalPlugins[] = $plugin; + } + + return $finalPlugins; +} + +/** + * Validate plugin array submitted. + * Will fail if there is duplicate orders value. + * + * @param array $formData Data from submitted form. + * + * @return bool true if ok, false otherwise. + */ +function validate_plugin_order($formData) +{ + $orders = array(); + foreach ($formData as $key => $value) { + // No duplicate order allowed. + if (in_array($value, $orders)) { + return false; + } + + if (startsWith($key, 'order')) { + $orders[] = $value; + } + } + + return true; +} + +/** + * Affect plugin parameters values into plugins array. + * + * @param mixed $plugins Plugins array ($plugins[]['parameters']['param_name'] = . + * @param mixed $conf Plugins configuration. + * + * @return mixed Updated $plugins array. + */ +function load_plugin_parameter_values($plugins, $conf) +{ + $out = $plugins; + foreach ($plugins as $name => $plugin) { + if (empty($plugin['parameters'])) { + continue; + } + + foreach ($plugin['parameters'] as $key => $param) { + if (!empty($conf[$key])) { + $out[$name]['parameters'][$key] = $conf[$key]; + } + } + } + + return $out; +} + +/** + * Exception used if an error occur while saving plugin configuration. + */ +class PluginConfigOrderException extends Exception +{ + /** + * Construct exception. + */ + public function __construct() + { + $this->message = 'An error occurred while trying to save plugins loading order.'; + } +} diff --git a/index.php b/index.php index 7465c41f..b9576de8 100644 --- a/index.php +++ b/index.php @@ -22,114 +22,13 @@ date_default_timezone_set('UTC'); } -/* ----------------------------------------------------------------------------- - * Hardcoded parameters - * You should not touch any code below (or at your own risks!) - * (These parameters can be overwritten by editing the file /data/config.php) - * ----------------------------------------------------------------------------- - */ - -/* - * Shaarli directories & configuration files - */ -// Data subdirectory -$GLOBALS['config']['DATADIR'] = 'data'; - -// Main configuration file -$GLOBALS['config']['CONFIG_FILE'] = $GLOBALS['config']['DATADIR'].'/config.php'; - -// Link datastore -$GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php'; - -// Banned IPs -$GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php'; - -// Processed updates file. -$GLOBALS['config']['UPDATES_FILE'] = $GLOBALS['config']['DATADIR'].'/updates.txt'; - -// Access log -$GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt'; - -// For updates check of Shaarli -$GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt'; - -// Set ENABLE_UPDATECHECK to disabled by default. -$GLOBALS['config']['ENABLE_UPDATECHECK'] = false; - -// RainTPL cache directory (keep the trailing slash!) -$GLOBALS['config']['RAINTPL_TMP'] = 'tmp/'; -// Raintpl template directory (keep the trailing slash!) -$GLOBALS['config']['RAINTPL_TPL'] = 'tpl/'; - -// Thumbnail cache directory -$GLOBALS['config']['CACHEDIR'] = 'cache'; - -// Atom & RSS feed cache directory -$GLOBALS['config']['PAGECACHE'] = 'pagecache'; - -/* - * Global configuration - */ -// Ban IP after this many failures -$GLOBALS['config']['BAN_AFTER'] = 4; -// Ban duration for IP address after login failures (in seconds) -$GLOBALS['config']['BAN_DURATION'] = 1800; - -// Feed options -// Enable RSS permalinks by default. -// This corresponds to the default behavior of shaarli before this was added as an option. -$GLOBALS['config']['ENABLE_RSS_PERMALINKS'] = true; -// If true, an extra "ATOM feed" button will be displayed in the toolbar -$GLOBALS['config']['SHOW_ATOM'] = false; - -// Link display options -$GLOBALS['config']['HIDE_PUBLIC_LINKS'] = false; -$GLOBALS['config']['HIDE_TIMESTAMPS'] = false; -$GLOBALS['config']['LINKS_PER_PAGE'] = 20; - -// Open Shaarli (true): anyone can add/edit/delete links without having to login -$GLOBALS['config']['OPEN_SHAARLI'] = false; - -// Thumbnails -// Display thumbnails in links -$GLOBALS['config']['ENABLE_THUMBNAILS'] = true; -// Store thumbnails in a local cache -$GLOBALS['config']['ENABLE_LOCALCACHE'] = true; - -// Update check frequency for Shaarli. 86400 seconds=24 hours -$GLOBALS['config']['UPDATECHECK_BRANCH'] = 'stable'; -$GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400; - -$GLOBALS['config']['REDIRECTOR_URLENCODE'] = true; - -/* - * Plugin configuration - * - * Warning: order matters! - * - * These settings may be be overriden in: - * - data/config.php - * - each plugin's configuration file - */ -//$GLOBALS['config']['ENABLED_PLUGINS'] = array( -// 'qrcode', 'archiveorg', 'readityourself', 'demo_plugin', 'playvideos', -// 'wallabag', 'markdown', 'addlink_toolbar', -//); -$GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode'); - -// Initialize plugin parameters array. -$GLOBALS['plugins'] = array(); - -// PubSubHubbub support. Put an empty string to disable, or put your hub url here to enable. -$GLOBALS['config']['PUBSUBHUB_URL'] = ''; - /* * PHP configuration */ define('shaarli_version', '0.7.0'); // http://server.com/x/shaarli --> /shaarli/ -define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); +define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0))); // High execution time in case of problematic imports/exports. ini_set('max_input_time','60'); @@ -144,17 +43,13 @@ // See all errors (for debugging only) //error_reporting(-1); -/* - * User configuration - */ -if (is_file($GLOBALS['config']['CONFIG_FILE'])) { - require_once $GLOBALS['config']['CONFIG_FILE']; -} // Shaarli library require_once 'application/ApplicationUtils.php'; require_once 'application/Cache.php'; require_once 'application/CachedPage.php'; +require_once 'application/config/ConfigManager.php'; +require_once 'application/config/ConfigPlugin.php'; require_once 'application/FeedBuilder.php'; require_once 'application/FileUtils.php'; require_once 'application/HttpUtils.php'; @@ -166,10 +61,10 @@ require_once 'application/TimeZone.php'; require_once 'application/Url.php'; require_once 'application/Utils.php'; -require_once 'application/Config.php'; require_once 'application/PluginManager.php'; require_once 'application/Router.php'; require_once 'application/Updater.php'; +require_once 'inc/rain.tpl.class.php'; // Ensure the PHP version is supported try { @@ -210,16 +105,19 @@ $_COOKIE['shaarli'] = session_id(); } -include "inc/rain.tpl.class.php"; //include Rain TPL -raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory -raintpl::$cache_dir = $GLOBALS['config']['RAINTPL_TMP']; // cache directory +$conf = new ConfigManager(); +$conf->setEmpty('general.timezone', date_default_timezone_get()); +$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); +RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl'); // template directory +RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory -$pluginManager = PluginManager::getInstance(); -$pluginManager->load($GLOBALS['config']['ENABLED_PLUGINS']); +$pluginManager = new PluginManager($conf); +$pluginManager->load($conf->get('general.enabled_plugins')); + +date_default_timezone_set($conf->get('general.timezone', 'UTC')); ob_start(); // Output buffering for the page cache. - // In case stupid admin has left magic_quotes enabled in php.ini: if (get_magic_quotes_gpc()) { @@ -235,18 +133,9 @@ function stripslashes_deep($value) { $value = is_array($value) ? array_map('stri header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); -// Handling of old config file which do not have the new parameters. -if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(index_url($_SERVER)); -if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); -if (empty($GLOBALS['redirector'])) $GLOBALS['redirector']=''; -if (empty($GLOBALS['disablesessionprotection'])) $GLOBALS['disablesessionprotection']=false; -if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=false; -if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; -// I really need to rewrite Shaarli with a proper configuation manager. - -if (! is_file($GLOBALS['config']['CONFIG_FILE'])) { +if (! is_file($conf->getConfigFileExt())) { // Ensure Shaarli has proper access to its resources - $errors = ApplicationUtils::checkResourcePermissions($GLOBALS['config']); + $errors = ApplicationUtils::checkResourcePermissions($conf); if ($errors != array()) { $message = '

Insufficient permissions: