From 823a363c3b2e10008a607c8b69c1a3d4e9b44ea1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 16 May 2016 08:54:03 +0200 Subject: [PATCH 01/13] Configuration template indenting --- tpl/configure.html | 114 +++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 36 deletions(-) diff --git a/tpl/configure.html b/tpl/configure.html index 77c8b7d9..b7f87c68 100644 --- a/tpl/configure.html +++ b/tpl/configure.html @@ -3,48 +3,90 @@ {include="includes"} {include="page.footer"} From 59404d7909b21682ec0782778452a8a70e38b25e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 18 May 2016 21:43:59 +0200 Subject: [PATCH 02/13] Introduce a configuration manager (not plugged yet) --- application/config/ConfigIO.php | 33 +++ application/config/ConfigManager.php | 363 +++++++++++++++++++++++++++ application/config/ConfigPhp.php | 93 +++++++ application/config/ConfigPlugin.php | 118 +++++++++ tests/config/ConfigManagerTest.php | 48 ++++ tests/config/ConfigPhpTest.php | 82 ++++++ tests/config/ConfigPluginTest.php | 121 +++++++++ tests/config/php/configOK.php | 14 ++ 8 files changed, 872 insertions(+) create mode 100644 application/config/ConfigIO.php create mode 100644 application/config/ConfigManager.php create mode 100644 application/config/ConfigPhp.php create mode 100644 application/config/ConfigPlugin.php create mode 100644 tests/config/ConfigManagerTest.php create mode 100644 tests/config/ConfigPhpTest.php create mode 100644 tests/config/ConfigPluginTest.php create mode 100644 tests/config/php/configOK.php 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 @@ +initialize(); + } + + return self::$instance; + } + + /** + * Rebuild the loaded config array from config files. + */ + public function reload() + { + $this->initialize(); + } + + /** + * Initialize loaded conf in ConfigManager. + */ + protected function initialize() + { + /*if (! file_exists(self::$CONFIG_FILE .'.php')) { + $this->configIO = new ConfigJson(); + } else { + $this->configIO = new ConfigPhp(); + }*/ + $this->configIO = new ConfigPhp(); + $this->loadedConfig = $this->configIO->read(self::$CONFIG_FILE); + $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 = '') + { + $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. + */ + public function set($setting, $value, $write = false, $isLoggedIn = false) + { + $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) + { + $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. + * + * @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( + 'login', 'hash', 'salt', 'timezone', 'title', 'titleLink', + 'redirector', 'disablesessionprotection', 'privateLinkByDefault' + ); + + // Only logged in user can alter config. + if (is_file(self::$CONFIG_FILE) && !$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); + } + } + + $this->configIO->write(self::$CONFIG_FILE, $this->loadedConfig); + } + + /** + * Get the configuration file path. + * + * @return string Config file path. + */ + public function getConfigFile() + { + return self::$CONFIG_FILE . $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() + { + // Data subdirectory + $this->setEmpty('config.DATADIR', 'data'); + + // Main configuration file + $this->setEmpty('config.CONFIG_FILE', 'data/config.php'); + + // Link datastore + $this->setEmpty('config.DATASTORE', 'data/datastore.php'); + + // Banned IPs + $this->setEmpty('config.IPBANS_FILENAME', 'data/ipbans.php'); + + // Processed updates file. + $this->setEmpty('config.UPDATES_FILE', 'data/updates.txt'); + + // Access log + $this->setEmpty('config.LOG_FILE', 'data/log.txt'); + + // For updates check of Shaarli + $this->setEmpty('config.UPDATECHECK_FILENAME', 'data/lastupdatecheck.txt'); + + // Set ENABLE_UPDATECHECK to disabled by default. + $this->setEmpty('config.ENABLE_UPDATECHECK', false); + + // RainTPL cache directory (keep the trailing slash!) + $this->setEmpty('config.RAINTPL_TMP', 'tmp/'); + // Raintpl template directory (keep the trailing slash!) + $this->setEmpty('config.RAINTPL_TPL', 'tpl/'); + + // Thumbnail cache directory + $this->setEmpty('config.CACHEDIR', 'cache'); + + // Atom & RSS feed cache directory + $this->setEmpty('config.PAGECACHE', 'pagecache'); + + // Ban IP after this many failures + $this->setEmpty('config.BAN_AFTER', 4); + // Ban duration for IP address after login failures (in seconds) + $this->setEmpty('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. + $this->setEmpty('config.ENABLE_RSS_PERMALINKS', true); + // If true, an extra "ATOM feed" button will be displayed in the toolbar + $this->setEmpty('config.SHOW_ATOM', false); + + // Link display options + $this->setEmpty('config.HIDE_PUBLIC_LINKS', false); + $this->setEmpty('config.HIDE_TIMESTAMPS', false); + $this->setEmpty('config.LINKS_PER_PAGE', 20); + + // Open Shaarli (true): anyone can add/edit/delete links without having to login + $this->setEmpty('config.OPEN_SHAARLI', false); + + // Thumbnails + // Display thumbnails in links + $this->setEmpty('config.ENABLE_THUMBNAILS', true); + // Store thumbnails in a local cache + $this->setEmpty('config.ENABLE_LOCALCACHE', true); + + // Update check frequency for Shaarli. 86400 seconds=24 hours + $this->setEmpty('config.UPDATECHECK_BRANCH', 'stable'); + $this->setEmpty('config.UPDATECHECK_INTERVAL', 86400); + + $this->setEmpty('redirector', ''); + $this->setEmpty('config.REDIRECTOR_URLENCODE', true); + + // Enabled plugins. + $this->setEmpty('config.ENABLED_PLUGINS', array('qrcode')); + + // Initialize plugin parameters array. + $this->setEmpty('plugins', array()); + } + + /** + * Set only if the setting does not exists. + * + * @param string $key Setting key. + * @param mixed $value Setting value. + */ + protected function setEmpty($key, $value) + { + if (! $this->exists($key)) { + $this->set($key, $value); + } + } +} + +/** + * 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..311aeb81 --- /dev/null +++ b/application/config/ConfigPhp.php @@ -0,0 +1,93 @@ +getExtension(); + 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) + { + $filepath .= $this->getExtension(); + + $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; + } + } + + // FIXME! + //$configStr .= 'date_default_timezone_set('.var_export($conf['timezone'], 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..8af89d04 --- /dev/null +++ b/application/config/ConfigPlugin.php @@ -0,0 +1,118 @@ + $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/tests/config/ConfigManagerTest.php b/tests/config/ConfigManagerTest.php new file mode 100644 index 00000000..1b6358f3 --- /dev/null +++ b/tests/config/ConfigManagerTest.php @@ -0,0 +1,48 @@ +conf = ConfigManager::getInstance(); + } + + public function tearDown() + { + @unlink($this->conf->getConfigFile()); + } + + public function testSetWriteGet() + { + // This won't work with ConfigPhp. + $this->markTestIncomplete(); + + $this->conf->set('paramInt', 42); + $this->conf->set('paramString', 'value1'); + $this->conf->set('paramBool', false); + $this->conf->set('paramArray', array('foo' => 'bar')); + $this->conf->set('paramNull', null); + + $this->conf->write(true); + $this->conf->reload(); + + $this->assertEquals(42, $this->conf->get('paramInt')); + $this->assertEquals('value1', $this->conf->get('paramString')); + $this->assertFalse($this->conf->get('paramBool')); + $this->assertEquals(array('foo' => 'bar'), $this->conf->get('paramArray')); + $this->assertEquals(null, $this->conf->get('paramNull')); + } + +} \ No newline at end of file diff --git a/tests/config/ConfigPhpTest.php b/tests/config/ConfigPhpTest.php new file mode 100644 index 00000000..0f849bd5 --- /dev/null +++ b/tests/config/ConfigPhpTest.php @@ -0,0 +1,82 @@ +configIO = new ConfigPhp(); + } + + /** + * Read a simple existing config file. + */ + public function testRead() + { + $conf = $this->configIO->read('tests/config/php/configOK'); + $this->assertEquals('root', $conf['login']); + $this->assertEquals('lala', $conf['redirector']); + $this->assertEquals('data/datastore.php', $conf['config']['DATASTORE']); + $this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']); + } + + /** + * Read a non existent config file -> empty array. + */ + public function testReadNonExistent() + { + $this->assertEquals(array(), $this->configIO->read('nope')); + } + + /** + * Write a new config file. + */ + public function testWriteNew() + { + $dataFile = 'tests/config/php/configWrite'; + $data = array( + 'login' => 'root', + 'redirector' => 'lala', + 'config' => array( + 'DATASTORE' => 'data/datastore.php', + ), + 'plugins' => array( + 'WALLABAG_VERSION' => '1', + ) + ); + $this->configIO->write($dataFile, $data); + $expected = 'assertEquals($expected, file_get_contents($dataFile .'.php')); + unlink($dataFile .'.php'); + } + + /** + * Overwrite an existing setting. + */ + public function testOverwrite() + { + $source = 'tests/config/php/configOK.php'; + $dest = 'tests/config/php/configOverwrite'; + copy($source, $dest . '.php'); + $conf = $this->configIO->read($dest); + $conf['redirector'] = 'blabla'; + $this->configIO->write($dest, $conf); + $conf = $this->configIO->read($dest); + $this->assertEquals('blabla', $conf['redirector']); + unlink($dest .'.php'); + } +} diff --git a/tests/config/ConfigPluginTest.php b/tests/config/ConfigPluginTest.php new file mode 100644 index 00000000..716631b0 --- /dev/null +++ b/tests/config/ConfigPluginTest.php @@ -0,0 +1,121 @@ + 2, // no plugin related + 'plugin2' => 0, // new - at the end + 'plugin3' => 0, // 2nd + 'order_plugin3' => 8, + 'plugin4' => 0, // 1st + 'order_plugin4' => 5, + ); + + $expected = array( + 'plugin3', + 'plugin4', + 'plugin2', + ); + + $out = save_plugin_config($data); + $this->assertEquals($expected, $out); + } + + /** + * Test save_plugin_config with invalid data. + * + * @expectedException PluginConfigOrderException + */ + public function testSavePluginConfigInvalid() + { + $data = array( + 'plugin2' => 0, + 'plugin3' => 0, + 'order_plugin3' => 0, + 'plugin4' => 0, + 'order_plugin4' => 0, + ); + + save_plugin_config($data); + } + + /** + * Test save_plugin_config without data. + */ + public function testSavePluginConfigEmpty() + { + $this->assertEquals(array(), save_plugin_config(array())); + } + + /** + * Test validate_plugin_order with valid data. + */ + public function testValidatePluginOrderValid() + { + $data = array( + 'order_plugin1' => 2, + 'plugin2' => 0, + 'plugin3' => 0, + 'order_plugin3' => 1, + 'plugin4' => 0, + 'order_plugin4' => 5, + ); + + $this->assertTrue(validate_plugin_order($data)); + } + + /** + * Test validate_plugin_order with invalid data. + */ + public function testValidatePluginOrderInvalid() + { + $data = array( + 'order_plugin1' => 2, + 'order_plugin3' => 1, + 'order_plugin4' => 1, + ); + + $this->assertFalse(validate_plugin_order($data)); + } + + /** + * Test load_plugin_parameter_values. + */ + public function testLoadPluginParameterValues() + { + $plugins = array( + 'plugin_name' => array( + 'parameters' => array( + 'param1' => true, + 'param2' => false, + 'param3' => '', + ) + ) + ); + + $parameters = array( + 'param1' => 'value1', + 'param2' => 'value2', + ); + + $result = load_plugin_parameter_values($plugins, $parameters); + $this->assertEquals('value1', $result['plugin_name']['parameters']['param1']); + $this->assertEquals('value2', $result['plugin_name']['parameters']['param2']); + $this->assertEquals('', $result['plugin_name']['parameters']['param3']); + } +} diff --git a/tests/config/php/configOK.php b/tests/config/php/configOK.php new file mode 100644 index 00000000..b91ad293 --- /dev/null +++ b/tests/config/php/configOK.php @@ -0,0 +1,14 @@ + Date: Wed, 18 May 2016 21:48:24 +0200 Subject: [PATCH 03/13] Replace $GLOBALS configuration with the configuration manager in the whole code base --- application/ApplicationUtils.php | 26 +- application/Config.php | 221 -------------- application/FileUtils.php | 8 +- application/PageBuilder.php | 28 +- application/Updater.php | 30 +- application/Utils.php | 2 +- application/config/ConfigIO.php | 2 + application/config/ConfigManager.php | 49 ++- application/config/ConfigPhp.php | 3 - application/config/ConfigPlugin.php | 4 +- index.php | 441 ++++++++++++--------------- tests/ApplicationUtilsTest.php | 54 ++-- tests/ConfigTest.php | 244 --------------- tests/FeedBuilderTest.php | 6 +- tests/LinkDBTest.php | 2 +- tests/Updater/DummyUpdater.php | 5 +- tests/Updater/UpdaterTest.php | 80 +++-- tests/config/ConfigPhpTest.php | 16 +- tests/config/php/configOK.php | 14 - tests/utils/config/configPhp.php | 14 + tests/utils/config/configUpdater.php | 15 + tpl/configure.html | 8 +- tpl/page.header.html | 4 +- 23 files changed, 421 insertions(+), 855 deletions(-) delete mode 100644 application/Config.php delete mode 100644 tests/ConfigTest.php delete mode 100644 tests/config/php/configOK.php create mode 100644 tests/utils/config/configPhp.php create mode 100644 tests/utils/config/configUpdater.php diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 978fc9da..ed9abc39 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -132,32 +132,32 @@ public static function checkPHPVersion($minVersion, $curVersion) /** * Checks Shaarli has the proper access permissions to its resources * - * @param array $globalConfig The $GLOBALS['config'] array - * * @return array A list of the detected configuration issues */ - public static function checkResourcePermissions($globalConfig) + public static function checkResourcePermissions() { $errors = array(); + $conf = ConfigManager::getInstance(); // Check script and template directories are readable foreach (array( 'application', 'inc', 'plugins', - $globalConfig['RAINTPL_TPL'] + $conf->get('config.RAINTPL_TPL'), ) as $path) { if (! is_readable(realpath($path))) { $errors[] = '"'.$path.'" directory is not readable'; } } + $datadir = $conf->get('config.DATADIR'); // Check cache and data directories are readable and writeable foreach (array( - $globalConfig['CACHEDIR'], - $globalConfig['DATADIR'], - $globalConfig['PAGECACHE'], - $globalConfig['RAINTPL_TMP'] + $conf->get('config.CACHEDIR'), + $datadir, + $conf->get('config.PAGECACHE'), + $conf->get('config.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->getConfigFile(), + $conf->get('config.DATASTORE'), + $conf->get('config.IPBANS_FILENAME'), + $conf->get('config.LOG_FILE'), + $conf->get('config.UPDATECHECK_FILENAME'), ) 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..cf13c714 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -29,21 +29,22 @@ function __construct() private function initialize() { $this->tpl = new RainTPL(); + $conf = ConfigManager::getInstance(); try { $version = ApplicationUtils::checkUpdate( shaarli_version, - $GLOBALS['config']['UPDATECHECK_FILENAME'], - $GLOBALS['config']['UPDATECHECK_INTERVAL'], - $GLOBALS['config']['ENABLE_UPDATECHECK'], + $conf->get('config.UPDATECHECK_FILENAME'), + $conf->get('config.UPDATECHECK_INTERVAL'), + $conf->get('config.ENABLE_UPDATECHECK'), isLoggedIn(), - $GLOBALS['config']['UPDATECHECK_BRANCH'] + $conf->get('config.UPDATECHECK_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($conf->get('config.LOG_FILE'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); $this->tpl->assign('newVersion', ''); $this->tpl->assign('versionError', escape($exc->getMessage())); } @@ -62,16 +63,19 @@ 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 ($conf->exists('title')) { + $this->tpl->assign('pagetitle', $conf->get('title')); } - if (!empty($GLOBALS['titleLink'])) { - $this->tpl->assign('titleLink', $GLOBALS['titleLink']); + if ($conf->exists('titleLink')) { + $this->tpl->assign('titleLink', $conf->get('titleLink')); } - if (!empty($GLOBALS['pagetitle'])) { - $this->tpl->assign('pagetitle', $GLOBALS['pagetitle']); + if ($conf->exists('pagetitle')) { + $this->tpl->assign('pagetitle', $conf->get('pagetitle')); } - $this->tpl->assign('shaarlititle', empty($GLOBALS['title']) ? 'Shaarli': $GLOBALS['title']); + $this->tpl->assign('shaarlititle', $conf->get('title', 'Shaarli')); + $this->tpl->assign('openshaarli', $conf->get('config.OPEN_SHAARLI', false)); + $this->tpl->assign('showatom', $conf->get('config.SHOW_ATOM', false)); + // FIXME! Globals if (!empty($GLOBALS['plugin_errors'])) { $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); } diff --git a/application/Updater.php b/application/Updater.php index 58c13c07..6b92af3d 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -12,11 +12,6 @@ class Updater */ protected $doneUpdates; - /** - * @var array Shaarli's configuration array. - */ - protected $config; - /** * @var LinkDB instance. */ @@ -36,14 +31,12 @@ 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. */ - public function __construct($doneUpdates, $config, $linkDB, $isLoggedIn) + public function __construct($doneUpdates, $linkDB, $isLoggedIn) { $this->doneUpdates = $doneUpdates; - $this->config = $config; $this->linkDB = $linkDB; $this->isLoggedIn = $isLoggedIn; @@ -114,19 +107,21 @@ public function getDoneUpdates() */ public function updateMethodMergeDeprecatedConfigFile() { - $config_file = $this->config['config']['CONFIG_FILE']; + $conf = ConfigManager::getInstance(); - if (is_file($this->config['config']['DATADIR'].'/options.php')) { - include $this->config['config']['DATADIR'].'/options.php'; + if (is_file($conf->get('config.DATADIR') . '/options.php')) { + include $conf->get('config.DATADIR') . '/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)) { + $conf->set($key, $value); + } } - $this->config['config']['CONFIG_FILE'] = $config_file; - writeConfig($this->config, $this->isLoggedIn); - - unlink($this->config['config']['DATADIR'].'/options.php'); + $conf->write($this->isLoggedIn); + unlink($conf->get('config.DATADIR').'/options.php'); } return true; @@ -137,13 +132,14 @@ public function updateMethodMergeDeprecatedConfigFile() */ public function updateMethodRenameDashTags() { + $conf = ConfigManager::getInstance(); $linklist = $this->linkDB->filterSearch(); foreach ($linklist as $link) { $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); $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($conf->get('config.PAGECACHE')); return true; } } 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 index 2b68fe6a..4b1c9901 100644 --- a/application/config/ConfigIO.php +++ b/application/config/ConfigIO.php @@ -21,6 +21,8 @@ function read($filepath); * * @param string $filepath Config file absolute path. * @param array $conf All configuration in an array. + * + * @return bool True if the configuration has been successfully written, false otherwise. */ function write($filepath, $conf); diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index dfe9eeb9..212aac05 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -62,16 +62,25 @@ public static function getInstance() return self::$instance; } + /** + * Reset the ConfigManager instance. + */ + public static function reset() + { + self::$instance = null; + return self::getInstance(); + } + /** * Rebuild the loaded config array from config files. */ public function reload() { - $this->initialize(); + $this->load(); } /** - * Initialize loaded conf in ConfigManager. + * Initialize the ConfigIO and loaded the conf. */ protected function initialize() { @@ -81,7 +90,15 @@ protected function initialize() $this->configIO = new ConfigPhp(); }*/ $this->configIO = new ConfigPhp(); - $this->loadedConfig = $this->configIO->read(self::$CONFIG_FILE); + $this->load(); + } + + /** + * Load configuration in the ConfigurationManager. + */ + protected function load() + { + $this->loadedConfig = $this->configIO->read($this->getConfigFile()); $this->setDefaultValues(); } @@ -117,9 +134,15 @@ public function get($setting, $default = '') * @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)); + } + $settings = explode('.', $setting); self::setConfig($settings, $value, $this->loadedConfig); if ($write) { @@ -151,6 +174,8 @@ public function exists($setting) * * @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. @@ -175,7 +200,7 @@ public function write($isLoggedIn) } } - $this->configIO->write(self::$CONFIG_FILE, $this->loadedConfig); + return $this->configIO->write($this->getConfigFile(), $this->loadedConfig); } /** @@ -327,6 +352,22 @@ protected function setEmpty($key, $value) $this->set($key, $value); } } + + /** + * @return ConfigIO + */ + public function getConfigIO() + { + return $this->configIO; + } + + /** + * @param ConfigIO $configIO + */ + public function setConfigIO($configIO) + { + $this->configIO = $configIO; + } } /** diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php index 311aeb81..19fecf2b 100644 --- a/application/config/ConfigPhp.php +++ b/application/config/ConfigPhp.php @@ -28,7 +28,6 @@ class ConfigPhp implements ConfigIO */ function read($filepath) { - $filepath .= $this->getExtension(); if (! file_exists($filepath) || ! is_readable($filepath)) { return array(); } @@ -49,8 +48,6 @@ function read($filepath) */ function write($filepath, $conf) { - $filepath .= $this->getExtension(); - $configStr = ' /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,12 +43,6 @@ // 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'; @@ -166,10 +59,12 @@ require_once 'application/TimeZone.php'; require_once 'application/Url.php'; require_once 'application/Utils.php'; -require_once 'application/Config.php'; +require_once 'application/config/ConfigManager.php'; +require_once 'application/config/ConfigPlugin.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,16 @@ $_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 = ConfigManager::getInstance(); + +RainTPL::$tpl_dir = $conf->get('config.RAINTPL_TPL'); // template directory +RainTPL::$cache_dir = $conf->get('config.RAINTPL_TMP'); // cache directory $pluginManager = PluginManager::getInstance(); -$pluginManager->load($GLOBALS['config']['ENABLED_PLUGINS']); +$pluginManager->load($conf->get('config.ENABLED_PLUGINS')); 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()) { @@ -236,17 +131,25 @@ function stripslashes_deep($value) { $value = is_array($value) ? array_map('stri 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 (! $conf->exists('title')) { + $conf->set('title', 'Shared links on '. escape(index_url($_SERVER))); +} +if (! $conf->exists('timezone')) { + $conf->set('timezone', date_default_timezone_get()); +} +if (! $conf->exists('disablesessionprotection')) { + $conf->set('disablesessionprotection', false); +} +if (! $conf->exists('privateLinkByDefault')) { + $conf->set('privateLinkByDefault', false); +} +if (! $conf->exists('titleLink')) { + $conf->set('titleLink', '?'); +} -if (! is_file($GLOBALS['config']['CONFIG_FILE'])) { +if (! is_file($conf->getConfigFile())) { // Ensure Shaarli has proper access to its resources - $errors = ApplicationUtils::checkResourcePermissions($GLOBALS['config']); + $errors = ApplicationUtils::checkResourcePermissions(); if ($errors != array()) { $message = '

Insufficient permissions: