]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Introduce a configuration manager (not plugged yet)
authorArthurHoaro <arthur@hoa.ro>
Wed, 18 May 2016 19:43:59 +0000 (21:43 +0200)
committerArthurHoaro <arthur@hoa.ro>
Sat, 11 Jun 2016 07:30:56 +0000 (09:30 +0200)
application/config/ConfigIO.php [new file with mode: 0644]
application/config/ConfigManager.php [new file with mode: 0644]
application/config/ConfigPhp.php [new file with mode: 0644]
application/config/ConfigPlugin.php [new file with mode: 0644]
tests/config/ConfigManagerTest.php [new file with mode: 0644]
tests/config/ConfigPhpTest.php [new file with mode: 0644]
tests/config/ConfigPluginTest.php [new file with mode: 0644]
tests/config/php/configOK.php [new file with mode: 0644]

diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php
new file mode 100644 (file)
index 0000000..2b68fe6
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * Interface ConfigIO
+ *
+ * This describes how Config types should store their configuration.
+ */
+interface ConfigIO
+{
+    /**
+     * Read configuration.
+     *
+     * @param string $filepath Config file absolute path.
+     *
+     * @return array All configuration in an array.
+     */
+    function read($filepath);
+
+    /**
+     * Write configuration.
+     *
+     * @param string $filepath Config file absolute path.
+     * @param array  $conf   All configuration in an array.
+     */
+    function write($filepath, $conf);
+
+    /**
+     * Get config file extension according to config type.
+     *
+     * @return string Config file extension.
+     */
+    function getExtension();
+}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
new file mode 100644 (file)
index 0000000..dfe9eeb
--- /dev/null
@@ -0,0 +1,363 @@
+<?php
+
+// FIXME! Namespaces...
+require_once 'ConfigIO.php';
+require_once 'ConfigPhp.php';
+#require_once 'ConfigJson.php';
+
+/**
+ * Class ConfigManager
+ *
+ * Singleton, manages all Shaarli's settings.
+ */
+class ConfigManager
+{
+    /**
+     * @var ConfigManager instance.
+     */
+    protected static $instance = null;
+
+    /**
+     * @var string Config folder.
+     */
+    public static $CONFIG_FILE = 'data/config';
+
+    /**
+     * @var string Flag telling a setting is not found.
+     */
+    protected static $NOT_FOUND = 'NOT_FOUND';
+
+    /**
+     * @var array Loaded config array.
+     */
+    protected $loadedConfig;
+
+    /**
+     * @var ConfigIO implementation instance.
+     */
+    protected $configIO;
+
+    /**
+     * Private constructor: new instances not allowed.
+     */
+    private function __construct() {}
+
+    /**
+     * Cloning isn't allowed either.
+     */
+    private function __clone() {}
+
+    /**
+     * Return existing instance of PluginManager, or create it.
+     *
+     * @return ConfigManager instance.
+     */
+    public static function getInstance()
+    {
+        if (!(self::$instance instanceof self)) {
+            self::$instance = new self();
+            self::$instance->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 (file)
index 0000000..311aeb8
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Class ConfigPhp (ConfigIO implementation)
+ *
+ * Handle Shaarli's legacy PHP configuration file.
+ * Note: this is only designed to support the transition to JSON configuration.
+ */
+class ConfigPhp implements ConfigIO
+{
+    /**
+     * @var array List of config key without group.
+     */
+    public static $ROOT_KEYS = array(
+        'login',
+        'hash',
+        'salt',
+        'timezone',
+        'title',
+        'titleLink',
+        'redirector',
+        'disablesessionprotection',
+        'privateLinkByDefault',
+    );
+
+    /**
+     * @inheritdoc
+     */
+    function read($filepath)
+    {
+        $filepath .= $this->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 = '<?php '. PHP_EOL;
+        foreach (self::$ROOT_KEYS as $key) {
+            if (isset($conf[$key])) {
+                $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
+            }
+        }
+        
+        // Store all $conf['config']
+        foreach ($conf['config'] as $key => $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 (file)
index 0000000..8af89d0
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Functions related to configuration management.
+ */
+
+/**
+ * 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[<plugin_name>]['parameters']['param_name'] = <value>.
+ * @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 (file)
index 0000000..1b6358f
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Unit tests for Class ConfigManagerTest
+ *
+ * Note: it only test the manager with ConfigJson,
+ *  ConfigPhp is only a workaround to handle the transition to JSON type.
+ */
+class ConfigManagerTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @var ConfigManager
+     */
+    protected $conf;
+
+    public function setUp()
+    {
+        ConfigManager::$CONFIG_FILE = 'tests/config/config';
+        $this->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 (file)
index 0000000..0f849bd
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+require_once 'application/config/ConfigPhp.php';
+
+/**
+ * Class ConfigPhpTest
+ */
+class ConfigPhpTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var ConfigPhp
+     */
+    protected $configIO;
+
+    public function setUp()
+    {
+        $this->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 = '<?php 
+$GLOBALS[\'login\'] = \'root\';
+$GLOBALS[\'redirector\'] = \'lala\';
+$GLOBALS[\'config\'][\'DATASTORE\'] = \'data/datastore.php\';
+$GLOBALS[\'plugins\'][\'WALLABAG_VERSION\'] = \'1\';
+';
+        $this->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 (file)
index 0000000..716631b
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Config' tests
+ */
+
+require_once 'application/config/ConfigPlugin.php';
+
+/**
+ * Unitary tests for Shaarli config related functions
+ */
+class ConfigPluginTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test save_plugin_config with valid data.
+     *
+     * @throws PluginConfigOrderException
+     */
+    public function testSavePluginConfigValid()
+    {
+        $data = array(
+            'order_plugin1' => 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 (file)
index 0000000..b91ad29
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+$GLOBALS['login'] = 'root';
+$GLOBALS['hash'] = 'hash';
+$GLOBALS['salt'] = 'salt';
+$GLOBALS['timezone'] = 'Europe/Paris';
+$GLOBALS['title'] = 'BIGBANG';
+$GLOBALS['titleLink'] = '?';
+$GLOBALS['redirector'] = 'lala';
+$GLOBALS['disablesessionprotection'] = false;
+$GLOBALS['privateLinkByDefault'] = true;
+$GLOBALS['config']['DATADIR'] = 'data';
+$GLOBALS['config']['DATASTORE'] = 'data/datastore.php';
+$GLOBALS['plugins']['WALLABAG_URL'] = 'ghf';
+$GLOBALS['plugins']['WALLABAG_VERSION'] = '1';
\ No newline at end of file