aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthur <arthur@hoa.ro>2016-07-09 07:19:48 +0200
committerGitHub <noreply@github.com>2016-07-09 07:19:48 +0200
commit649af5b501d2a90448242f53764ff693e9854039 (patch)
tree23cde80a7ee2949e552c48939ae22fa462cfa0fc /application
parenta9cfa38df92bd2e1e2c00a67b6ac1516a2116ade (diff)
parent5ff23f02b80ec6ddee28dee869171ee8e3656b7c (diff)
downloadShaarli-649af5b501d2a90448242f53764ff693e9854039.tar.gz
Shaarli-649af5b501d2a90448242f53764ff693e9854039.tar.zst
Shaarli-649af5b501d2a90448242f53764ff693e9854039.zip
Merge pull request #570 from ArthurHoaro/config-manager
Introduce a configuration manager
Diffstat (limited to 'application')
-rw-r--r--application/ApplicationUtils.php24
-rw-r--r--application/Config.php221
-rw-r--r--application/FileUtils.php8
-rw-r--r--application/PageBuilder.php46
-rw-r--r--application/PluginManager.php50
-rw-r--r--application/Updater.php110
-rw-r--r--application/Utils.php2
-rw-r--r--application/config/ConfigIO.php33
-rw-r--r--application/config/ConfigJson.php78
-rw-r--r--application/config/ConfigManager.php392
-rw-r--r--application/config/ConfigPhp.php132
-rw-r--r--application/config/ConfigPlugin.php120
12 files changed, 905 insertions, 311 deletions
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 @@ class ApplicationUtils
132 /** 132 /**
133 * Checks Shaarli has the proper access permissions to its resources 133 * Checks Shaarli has the proper access permissions to its resources
134 * 134 *
135 * @param array $globalConfig The $GLOBALS['config'] array 135 * @param ConfigManager $conf Configuration Manager instance.
136 * 136 *
137 * @return array A list of the detected configuration issues 137 * @return array A list of the detected configuration issues
138 */ 138 */
139 public static function checkResourcePermissions($globalConfig) 139 public static function checkResourcePermissions($conf)
140 { 140 {
141 $errors = array(); 141 $errors = array();
142 142
@@ -145,7 +145,7 @@ class ApplicationUtils
145 'application', 145 'application',
146 'inc', 146 'inc',
147 'plugins', 147 'plugins',
148 $globalConfig['RAINTPL_TPL'] 148 $conf->get('resource.raintpl_tpl'),
149 ) as $path) { 149 ) as $path) {
150 if (! is_readable(realpath($path))) { 150 if (! is_readable(realpath($path))) {
151 $errors[] = '"'.$path.'" directory is not readable'; 151 $errors[] = '"'.$path.'" directory is not readable';
@@ -154,10 +154,10 @@ class ApplicationUtils
154 154
155 // Check cache and data directories are readable and writeable 155 // Check cache and data directories are readable and writeable
156 foreach (array( 156 foreach (array(
157 $globalConfig['CACHEDIR'], 157 $conf->get('resource.thumbnails_cache'),
158 $globalConfig['DATADIR'], 158 $conf->get('resource.data_dir'),
159 $globalConfig['PAGECACHE'], 159 $conf->get('resource.page_cache'),
160 $globalConfig['RAINTPL_TMP'] 160 $conf->get('resource.raintpl_tmp'),
161 ) as $path) { 161 ) as $path) {
162 if (! is_readable(realpath($path))) { 162 if (! is_readable(realpath($path))) {
163 $errors[] = '"'.$path.'" directory is not readable'; 163 $errors[] = '"'.$path.'" directory is not readable';
@@ -169,11 +169,11 @@ class ApplicationUtils
169 169
170 // Check configuration files are readable and writeable 170 // Check configuration files are readable and writeable
171 foreach (array( 171 foreach (array(
172 $globalConfig['CONFIG_FILE'], 172 $conf->getConfigFileExt(),
173 $globalConfig['DATASTORE'], 173 $conf->get('resource.datastore'),
174 $globalConfig['IPBANS_FILENAME'], 174 $conf->get('resource.ban_file'),
175 $globalConfig['LOG_FILE'], 175 $conf->get('resource.log'),
176 $globalConfig['UPDATECHECK_FILENAME'] 176 $conf->get('resource.update_check'),
177 ) as $path) { 177 ) as $path) {
178 if (! is_file(realpath($path))) { 178 if (! is_file(realpath($path))) {
179 # the file may not exist yet 179 # 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 @@
1<?php
2/**
3 * Functions related to configuration management.
4 */
5
6/**
7 * Re-write configuration file according to given array.
8 * Requires mandatory fields listed in $MANDATORY_FIELDS.
9 *
10 * @param array $config contains all configuration fields.
11 * @param bool $isLoggedIn true if user is logged in.
12 *
13 * @return void
14 *
15 * @throws MissingFieldConfigException: a mandatory field has not been provided in $config.
16 * @throws UnauthorizedConfigException: user is not authorize to change configuration.
17 * @throws Exception: an error occured while writing the new config file.
18 */
19function writeConfig($config, $isLoggedIn)
20{
21 // These fields are required in configuration.
22 $MANDATORY_FIELDS = array(
23 'login', 'hash', 'salt', 'timezone', 'title', 'titleLink',
24 'redirector', 'disablesessionprotection', 'privateLinkByDefault'
25 );
26
27 if (!isset($config['config']['CONFIG_FILE'])) {
28 throw new MissingFieldConfigException('CONFIG_FILE');
29 }
30
31 // Only logged in user can alter config.
32 if (is_file($config['config']['CONFIG_FILE']) && !$isLoggedIn) {
33 throw new UnauthorizedConfigException();
34 }
35
36 // Check that all mandatory fields are provided in $config.
37 foreach ($MANDATORY_FIELDS as $field) {
38 if (!isset($config[$field])) {
39 throw new MissingFieldConfigException($field);
40 }
41 }
42
43 $configStr = '<?php '. PHP_EOL;
44 $configStr .= '$GLOBALS[\'login\'] = '.var_export($config['login'], true).';'. PHP_EOL;
45 $configStr .= '$GLOBALS[\'hash\'] = '.var_export($config['hash'], true).';'. PHP_EOL;
46 $configStr .= '$GLOBALS[\'salt\'] = '.var_export($config['salt'], true).'; '. PHP_EOL;
47 $configStr .= '$GLOBALS[\'timezone\'] = '.var_export($config['timezone'], true).';'. PHP_EOL;
48 $configStr .= 'date_default_timezone_set('.var_export($config['timezone'], true).');'. PHP_EOL;
49 $configStr .= '$GLOBALS[\'title\'] = '.var_export($config['title'], true).';'. PHP_EOL;
50 $configStr .= '$GLOBALS[\'titleLink\'] = '.var_export($config['titleLink'], true).'; '. PHP_EOL;
51 $configStr .= '$GLOBALS[\'redirector\'] = '.var_export($config['redirector'], true).'; '. PHP_EOL;
52 $configStr .= '$GLOBALS[\'disablesessionprotection\'] = '.var_export($config['disablesessionprotection'], true).'; '. PHP_EOL;
53 $configStr .= '$GLOBALS[\'privateLinkByDefault\'] = '.var_export($config['privateLinkByDefault'], true).'; '. PHP_EOL;
54
55 // Store all $config['config']
56 foreach ($config['config'] as $key => $value) {
57 $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($config['config'][$key], true).';'. PHP_EOL;
58 }
59
60 if (isset($config['plugins'])) {
61 foreach ($config['plugins'] as $key => $value) {
62 $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($config['plugins'][$key], true).';'. PHP_EOL;
63 }
64 }
65
66 if (!file_put_contents($config['config']['CONFIG_FILE'], $configStr)
67 || strcmp(file_get_contents($config['config']['CONFIG_FILE']), $configStr) != 0
68 ) {
69 throw new Exception(
70 'Shaarli could not create the config file.
71 Please make sure Shaarli has the right to write in the folder is it installed in.'
72 );
73 }
74}
75
76/**
77 * Process plugin administration form data and save it in an array.
78 *
79 * @param array $formData Data sent by the plugin admin form.
80 *
81 * @return array New list of enabled plugin, ordered.
82 *
83 * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
84 */
85function save_plugin_config($formData)
86{
87 // Make sure there are no duplicates in orders.
88 if (!validate_plugin_order($formData)) {
89 throw new PluginConfigOrderException();
90 }
91
92 $plugins = array();
93 $newEnabledPlugins = array();
94 foreach ($formData as $key => $data) {
95 if (startsWith($key, 'order')) {
96 continue;
97 }
98
99 // If there is no order, it means a disabled plugin has been enabled.
100 if (isset($formData['order_' . $key])) {
101 $plugins[(int) $formData['order_' . $key]] = $key;
102 }
103 else {
104 $newEnabledPlugins[] = $key;
105 }
106 }
107
108 // New enabled plugins will be added at the end of order.
109 $plugins = array_merge($plugins, $newEnabledPlugins);
110
111 // Sort plugins by order.
112 if (!ksort($plugins)) {
113 throw new PluginConfigOrderException();
114 }
115
116 $finalPlugins = array();
117 // Make plugins order continuous.
118 foreach ($plugins as $plugin) {
119 $finalPlugins[] = $plugin;
120 }
121
122 return $finalPlugins;
123}
124
125/**
126 * Validate plugin array submitted.
127 * Will fail if there is duplicate orders value.
128 *
129 * @param array $formData Data from submitted form.
130 *
131 * @return bool true if ok, false otherwise.
132 */
133function validate_plugin_order($formData)
134{
135 $orders = array();
136 foreach ($formData as $key => $value) {
137 // No duplicate order allowed.
138 if (in_array($value, $orders)) {
139 return false;
140 }
141
142 if (startsWith($key, 'order')) {
143 $orders[] = $value;
144 }
145 }
146
147 return true;
148}
149
150/**
151 * Affect plugin parameters values into plugins array.
152 *
153 * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>.
154 * @param mixed $config Plugins configuration.
155 *
156 * @return mixed Updated $plugins array.
157 */
158function load_plugin_parameter_values($plugins, $config)
159{
160 $out = $plugins;
161 foreach ($plugins as $name => $plugin) {
162 if (empty($plugin['parameters'])) {
163 continue;
164 }
165
166 foreach ($plugin['parameters'] as $key => $param) {
167 if (!empty($config[$key])) {
168 $out[$name]['parameters'][$key] = $config[$key];
169 }
170 }
171 }
172
173 return $out;
174}
175
176/**
177 * Exception used if a mandatory field is missing in given configuration.
178 */
179class MissingFieldConfigException extends Exception
180{
181 public $field;
182
183 /**
184 * Construct exception.
185 *
186 * @param string $field field name missing.
187 */
188 public function __construct($field)
189 {
190 $this->field = $field;
191 $this->message = 'Configuration value is required for '. $this->field;
192 }
193}
194
195/**
196 * Exception used if an unauthorized attempt to edit configuration has been made.
197 */
198class UnauthorizedConfigException extends Exception
199{
200 /**
201 * Construct exception.
202 */
203 public function __construct()
204 {
205 $this->message = 'You are not authorized to alter config.';
206 }
207}
208
209/**
210 * Exception used if an error occur while saving plugin configuration.
211 */
212class PluginConfigOrderException extends Exception
213{
214 /**
215 * Construct exception.
216 */
217 public function __construct()
218 {
219 $this->message = 'An error occurred while trying to save plugins loading order.';
220 }
221}
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
9 /** 9 /**
10 * Construct a new IOException 10 * Construct a new IOException
11 * 11 *
12 * @param string $path path to the ressource that cannot be accessed 12 * @param string $path path to the resource that cannot be accessed
13 * @param string $message Custom exception message.
13 */ 14 */
14 public function __construct($path) 15 public function __construct($path, $message = '')
15 { 16 {
16 $this->path = $path; 17 $this->path = $path;
17 $this->message = 'Error accessing '.$this->path; 18 $this->message = empty($message) ? 'Error accessing' : $message;
19 $this->message .= PHP_EOL . $this->path;
18 } 20 }
19} 21}
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index 82580787..7cd88370 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -15,12 +15,20 @@ class PageBuilder
15 private $tpl; 15 private $tpl;
16 16
17 /** 17 /**
18 * @var ConfigManager $conf Configuration Manager instance.
19 */
20 protected $conf;
21
22 /**
18 * PageBuilder constructor. 23 * PageBuilder constructor.
19 * $tpl is initialized at false for lazy loading. 24 * $tpl is initialized at false for lazy loading.
25 *
26 * @param ConfigManager $conf Configuration Manager instance (reference).
20 */ 27 */
21 function __construct() 28 function __construct(&$conf)
22 { 29 {
23 $this->tpl = false; 30 $this->tpl = false;
31 $this->conf = $conf;
24 } 32 }
25 33
26 /** 34 /**
@@ -33,17 +41,17 @@ class PageBuilder
33 try { 41 try {
34 $version = ApplicationUtils::checkUpdate( 42 $version = ApplicationUtils::checkUpdate(
35 shaarli_version, 43 shaarli_version,
36 $GLOBALS['config']['UPDATECHECK_FILENAME'], 44 $this->conf->get('resource.update_check'),
37 $GLOBALS['config']['UPDATECHECK_INTERVAL'], 45 $this->conf->get('updates.check_updates_interval'),
38 $GLOBALS['config']['ENABLE_UPDATECHECK'], 46 $this->conf->get('updates.check_updates'),
39 isLoggedIn(), 47 isLoggedIn(),
40 $GLOBALS['config']['UPDATECHECK_BRANCH'] 48 $this->conf->get('updates.check_updates_branch')
41 ); 49 );
42 $this->tpl->assign('newVersion', escape($version)); 50 $this->tpl->assign('newVersion', escape($version));
43 $this->tpl->assign('versionError', ''); 51 $this->tpl->assign('versionError', '');
44 52
45 } catch (Exception $exc) { 53 } catch (Exception $exc) {
46 logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], $exc->getMessage()); 54 logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage());
47 $this->tpl->assign('newVersion', ''); 55 $this->tpl->assign('newVersion', '');
48 $this->tpl->assign('versionError', escape($exc->getMessage())); 56 $this->tpl->assign('versionError', escape($exc->getMessage()));
49 } 57 }
@@ -62,19 +70,24 @@ class PageBuilder
62 $this->tpl->assign('scripturl', index_url($_SERVER)); 70 $this->tpl->assign('scripturl', index_url($_SERVER));
63 $this->tpl->assign('pagetitle', 'Shaarli'); 71 $this->tpl->assign('pagetitle', 'Shaarli');
64 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? 72 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
65 if (!empty($GLOBALS['title'])) { 73 if ($this->conf->exists('general.title')) {
66 $this->tpl->assign('pagetitle', $GLOBALS['title']); 74 $this->tpl->assign('pagetitle', $this->conf->get('general.title'));
67 } 75 }
68 if (!empty($GLOBALS['titleLink'])) { 76 if ($this->conf->exists('general.header_link')) {
69 $this->tpl->assign('titleLink', $GLOBALS['titleLink']); 77 $this->tpl->assign('titleLink', $this->conf->get('general.header_link'));
70 } 78 }
71 if (!empty($GLOBALS['pagetitle'])) { 79 if ($this->conf->exists('pagetitle')) {
72 $this->tpl->assign('pagetitle', $GLOBALS['pagetitle']); 80 $this->tpl->assign('pagetitle', $this->conf->get('pagetitle'));
73 } 81 }
74 $this->tpl->assign('shaarlititle', empty($GLOBALS['title']) ? 'Shaarli': $GLOBALS['title']); 82 $this->tpl->assign('shaarlititle', $this->conf->get('title', 'Shaarli'));
83 $this->tpl->assign('openshaarli', $this->conf->get('security.open_shaarli', false));
84 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', false));
85 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
75 if (!empty($GLOBALS['plugin_errors'])) { 86 if (!empty($GLOBALS['plugin_errors'])) {
76 $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); 87 $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']);
77 } 88 }
89 // To be removed with a proper theme configuration.
90 $this->tpl->assign('conf', $this->conf);
78 } 91 }
79 92
80 /** 93 /**
@@ -85,7 +98,6 @@ class PageBuilder
85 */ 98 */
86 public function assign($placeholder, $value) 99 public function assign($placeholder, $value)
87 { 100 {
88 // Lazy initialization
89 if ($this->tpl === false) { 101 if ($this->tpl === false) {
90 $this->initialize(); 102 $this->initialize();
91 } 103 }
@@ -101,7 +113,6 @@ class PageBuilder
101 */ 113 */
102 public function assignAll($data) 114 public function assignAll($data)
103 { 115 {
104 // Lazy initialization
105 if ($this->tpl === false) { 116 if ($this->tpl === false) {
106 $this->initialize(); 117 $this->initialize();
107 } 118 }
@@ -113,6 +124,7 @@ class PageBuilder
113 foreach ($data as $key => $value) { 124 foreach ($data as $key => $value) {
114 $this->assign($key, $value); 125 $this->assign($key, $value);
115 } 126 }
127 return true;
116 } 128 }
117 129
118 /** 130 /**
@@ -123,10 +135,10 @@ class PageBuilder
123 */ 135 */
124 public function renderPage($page) 136 public function renderPage($page)
125 { 137 {
126 // Lazy initialization 138 if ($this->tpl === false) {
127 if ($this->tpl===false) {
128 $this->initialize(); 139 $this->initialize();
129 } 140 }
141
130 $this->tpl->draw($page); 142 $this->tpl->draw($page);
131 } 143 }
132 144
diff --git a/application/PluginManager.php b/application/PluginManager.php
index 787ac6a9..dca7e63e 100644
--- a/application/PluginManager.php
+++ b/application/PluginManager.php
@@ -4,18 +4,10 @@
4 * Class PluginManager 4 * Class PluginManager
5 * 5 *
6 * Use to manage, load and execute plugins. 6 * Use to manage, load and execute plugins.
7 *
8 * Using Singleton design pattern.
9 */ 7 */
10class PluginManager 8class PluginManager
11{ 9{
12 /** 10 /**
13 * PluginManager singleton instance.
14 * @var PluginManager $instance
15 */
16 private static $instance;
17
18 /**
19 * List of authorized plugins from configuration file. 11 * List of authorized plugins from configuration file.
20 * @var array $authorizedPlugins 12 * @var array $authorizedPlugins
21 */ 13 */
@@ -28,6 +20,11 @@ class PluginManager
28 private $loadedPlugins = array(); 20 private $loadedPlugins = array();
29 21
30 /** 22 /**
23 * @var ConfigManager Configuration Manager instance.
24 */
25 protected $conf;
26
27 /**
31 * Plugins subdirectory. 28 * Plugins subdirectory.
32 * @var string $PLUGINS_PATH 29 * @var string $PLUGINS_PATH
33 */ 30 */
@@ -40,33 +37,13 @@ class PluginManager
40 public static $META_EXT = 'meta'; 37 public static $META_EXT = 'meta';
41 38
42 /** 39 /**
43 * Private constructor: new instances not allowed. 40 * Constructor.
44 */
45 private function __construct()
46 {
47 }
48
49 /**
50 * Cloning isn't allowed either.
51 *
52 * @return void
53 */
54 private function __clone()
55 {
56 }
57
58 /**
59 * Return existing instance of PluginManager, or create it.
60 * 41 *
61 * @return PluginManager instance. 42 * @param ConfigManager $conf Configuration Manager instance.
62 */ 43 */
63 public static function getInstance() 44 public function __construct(&$conf)
64 { 45 {
65 if (!(self::$instance instanceof self)) { 46 $this->conf = $conf;
66 self::$instance = new self();
67 }
68
69 return self::$instance;
70 } 47 }
71 48
72 /** 49 /**
@@ -102,9 +79,9 @@ class PluginManager
102 /** 79 /**
103 * Execute all plugins registered hook. 80 * Execute all plugins registered hook.
104 * 81 *
105 * @param string $hook name of the hook to trigger. 82 * @param string $hook name of the hook to trigger.
106 * @param array $data list of data to manipulate passed by reference. 83 * @param array $data list of data to manipulate passed by reference.
107 * @param array $params additional parameters such as page target. 84 * @param array $params additional parameters such as page target.
108 * 85 *
109 * @return void 86 * @return void
110 */ 87 */
@@ -122,7 +99,7 @@ class PluginManager
122 $hookFunction = $this->buildHookName($hook, $plugin); 99 $hookFunction = $this->buildHookName($hook, $plugin);
123 100
124 if (function_exists($hookFunction)) { 101 if (function_exists($hookFunction)) {
125 $data = call_user_func($hookFunction, $data); 102 $data = call_user_func($hookFunction, $data, $this->conf);
126 } 103 }
127 } 104 }
128 } 105 }
@@ -148,6 +125,7 @@ class PluginManager
148 throw new PluginFileNotFoundException($pluginName); 125 throw new PluginFileNotFoundException($pluginName);
149 } 126 }
150 127
128 $conf = $this->conf;
151 include_once $pluginFilePath; 129 include_once $pluginFilePath;
152 130
153 $this->loadedPlugins[] = $pluginName; 131 $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
@@ -13,14 +13,14 @@ class Updater
13 protected $doneUpdates; 13 protected $doneUpdates;
14 14
15 /** 15 /**
16 * @var array Shaarli's configuration array. 16 * @var LinkDB instance.
17 */ 17 */
18 protected $config; 18 protected $linkDB;
19 19
20 /** 20 /**
21 * @var LinkDB instance. 21 * @var ConfigManager $conf Configuration Manager instance.
22 */ 22 */
23 protected $linkDB; 23 protected $conf;
24 24
25 /** 25 /**
26 * @var bool True if the user is logged in, false otherwise. 26 * @var bool True if the user is logged in, false otherwise.
@@ -35,16 +35,16 @@ class Updater
35 /** 35 /**
36 * Object constructor. 36 * Object constructor.
37 * 37 *
38 * @param array $doneUpdates Updates which are already done. 38 * @param array $doneUpdates Updates which are already done.
39 * @param array $config Shaarli's configuration array. 39 * @param LinkDB $linkDB LinkDB instance.
40 * @param LinkDB $linkDB LinkDB instance. 40 * @oaram ConfigManager $conf Configuration Manager instance.
41 * @param boolean $isLoggedIn True if the user is logged in. 41 * @param boolean $isLoggedIn True if the user is logged in.
42 */ 42 */
43 public function __construct($doneUpdates, $config, $linkDB, $isLoggedIn) 43 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
44 { 44 {
45 $this->doneUpdates = $doneUpdates; 45 $this->doneUpdates = $doneUpdates;
46 $this->config = $config;
47 $this->linkDB = $linkDB; 46 $this->linkDB = $linkDB;
47 $this->conf = $conf;
48 $this->isLoggedIn = $isLoggedIn; 48 $this->isLoggedIn = $isLoggedIn;
49 49
50 // Retrieve all update methods. 50 // Retrieve all update methods.
@@ -114,19 +114,19 @@ class Updater
114 */ 114 */
115 public function updateMethodMergeDeprecatedConfigFile() 115 public function updateMethodMergeDeprecatedConfigFile()
116 { 116 {
117 $config_file = $this->config['config']['CONFIG_FILE']; 117 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
118 118 include $this->conf->get('resource.data_dir') . '/options.php';
119 if (is_file($this->config['config']['DATADIR'].'/options.php')) {
120 include $this->config['config']['DATADIR'].'/options.php';
121 119
122 // Load GLOBALS into config 120 // Load GLOBALS into config
121 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
122 $allowedKeys[] = 'config';
123 foreach ($GLOBALS as $key => $value) { 123 foreach ($GLOBALS as $key => $value) {
124 $this->config[$key] = $value; 124 if (in_array($key, $allowedKeys)) {
125 $this->conf->set($key, $value);
126 }
125 } 127 }
126 $this->config['config']['CONFIG_FILE'] = $config_file; 128 $this->conf->write($this->isLoggedIn);
127 writeConfig($this->config, $this->isLoggedIn); 129 unlink($this->conf->get('resource.data_dir').'/options.php');
128
129 unlink($this->config['config']['DATADIR'].'/options.php');
130 } 130 }
131 131
132 return true; 132 return true;
@@ -143,7 +143,76 @@ class Updater
143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); 143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
144 $this->linkDB[$link['linkdate']] = $link; 144 $this->linkDB[$link['linkdate']] = $link;
145 } 145 }
146 $this->linkDB->savedb($this->config['config']['PAGECACHE']); 146 $this->linkDB->savedb($this->conf->get('resource.page_cache'));
147 return true;
148 }
149
150 /**
151 * Move old configuration in PHP to the new config system in JSON format.
152 *
153 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
154 * It will also convert legacy setting keys to the new ones.
155 */
156 public function updateMethodConfigToJson()
157 {
158 // JSON config already exists, nothing to do.
159 if ($this->conf->getConfigIO() instanceof ConfigJson) {
160 return true;
161 }
162
163 $configPhp = new ConfigPhp();
164 $configJson = new ConfigJson();
165 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
166 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
167 $this->conf->setConfigIO($configJson);
168 $this->conf->reload();
169
170 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
171 foreach (ConfigPhp::$ROOT_KEYS as $key) {
172 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
173 }
174
175 // Set sub config keys (config and plugins)
176 $subConfig = array('config', 'plugins');
177 foreach ($subConfig as $sub) {
178 foreach ($oldConfig[$sub] as $key => $value) {
179 if (isset($legacyMap[$sub .'.'. $key])) {
180 $configKey = $legacyMap[$sub .'.'. $key];
181 } else {
182 $configKey = $sub .'.'. $key;
183 }
184 $this->conf->set($configKey, $value);
185 }
186 }
187
188 try{
189 $this->conf->write($this->isLoggedIn);
190 return true;
191 } catch (IOException $e) {
192 error_log($e->getMessage());
193 return false;
194 }
195 }
196
197 /**
198 * Escape settings which have been manually escaped in every request in previous versions:
199 * - general.title
200 * - general.header_link
201 * - extras.redirector
202 *
203 * @return bool true if the update is successful, false otherwise.
204 */
205 public function escapeUnescapedConfig()
206 {
207 try {
208 $this->conf->set('general.title', escape($this->conf->get('general.title')));
209 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
210 $this->conf->set('redirector.url', escape($this->conf->get('redirector.url')));
211 $this->conf->write($this->isLoggedIn);
212 } catch (Exception $e) {
213 error_log($e->getMessage());
214 return false;
215 }
147 return true; 216 return true;
148 } 217 }
149} 218}
@@ -203,7 +272,6 @@ class UpdaterException extends Exception
203 } 272 }
204} 273}
205 274
206
207/** 275/**
208 * Read the updates file, and return already done updates. 276 * Read the updates file, and return already done updates.
209 * 277 *
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)
273 } 273 }
274 } 274 }
275 setlocale(LC_ALL, $attempts); 275 setlocale(LC_ALL, $attempts);
276} \ No newline at end of file 276}
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 @@
1<?php
2
3/**
4 * Interface ConfigIO
5 *
6 * This describes how Config types should store their configuration.
7 */
8interface ConfigIO
9{
10 /**
11 * Read configuration.
12 *
13 * @param string $filepath Config file absolute path.
14 *
15 * @return array All configuration in an array.
16 */
17 function read($filepath);
18
19 /**
20 * Write configuration.
21 *
22 * @param string $filepath Config file absolute path.
23 * @param array $conf All configuration in an array.
24 */
25 function write($filepath, $conf);
26
27 /**
28 * Get config file extension according to config type.
29 *
30 * @return string Config file extension.
31 */
32 function getExtension();
33}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
new file mode 100644
index 00000000..d07fefee
--- /dev/null
+++ b/application/config/ConfigJson.php
@@ -0,0 +1,78 @@
1<?php
2
3/**
4 * Class ConfigJson (ConfigIO implementation)
5 *
6 * Handle Shaarli's JSON configuration file.
7 */
8class ConfigJson implements ConfigIO
9{
10 /**
11 * @inheritdoc
12 */
13 function read($filepath)
14 {
15 if (! is_readable($filepath)) {
16 return array();
17 }
18 $data = file_get_contents($filepath);
19 $data = str_replace(self::getPhpHeaders(), '', $data);
20 $data = str_replace(self::getPhpSuffix(), '', $data);
21 $data = json_decode($data, true);
22 if ($data === null) {
23 $error = json_last_error();
24 throw new Exception('An error occured while parsing JSON file: error code #'. $error);
25 }
26 return $data;
27 }
28
29 /**
30 * @inheritdoc
31 */
32 function write($filepath, $conf)
33 {
34 // JSON_PRETTY_PRINT is available from PHP 5.4.
35 $print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
36 $data = self::getPhpHeaders() . json_encode($conf, $print) . self::getPhpSuffix();
37 if (!file_put_contents($filepath, $data)) {
38 throw new IOException(
39 $filepath,
40 'Shaarli could not create the config file.
41 Please make sure Shaarli has the right to write in the folder is it installed in.'
42 );
43 }
44 }
45
46 /**
47 * @inheritdoc
48 */
49 function getExtension()
50 {
51 return '.json.php';
52 }
53
54 /**
55 * The JSON data is wrapped in a PHP file for security purpose.
56 * This way, even if the file is accessible, credentials and configuration won't be exposed.
57 *
58 * Note: this isn't a static field because concatenation isn't supported in field declaration before PHP 5.6.
59 *
60 * @return string PHP start tag and comment tag.
61 */
62 public static function getPhpHeaders()
63 {
64 return '<?php /*'. PHP_EOL;
65 }
66
67 /**
68 * Get PHP comment closing tags.
69 *
70 * Static method for consistency with getPhpHeaders.
71 *
72 * @return string PHP comment closing.
73 */
74 public static function getPhpSuffix()
75 {
76 return PHP_EOL . '*/ ?>';
77 }
78}
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 @@
1<?php
2
3// FIXME! Namespaces...
4require_once 'ConfigIO.php';
5require_once 'ConfigJson.php';
6require_once 'ConfigPhp.php';
7
8/**
9 * Class ConfigManager
10 *
11 * Manages all Shaarli's settings.
12 * See the documentation for more information on settings:
13 * - doc/Shaarli-configuration.html
14 * - https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration
15 */
16class ConfigManager
17{
18 /**
19 * @var string Flag telling a setting is not found.
20 */
21 protected static $NOT_FOUND = 'NOT_FOUND';
22
23 /**
24 * @var string Config folder.
25 */
26 protected $configFile;
27
28 /**
29 * @var array Loaded config array.
30 */
31 protected $loadedConfig;
32
33 /**
34 * @var ConfigIO implementation instance.
35 */
36 protected $configIO;
37
38 /**
39 * Constructor.
40 */
41 public function __construct($configFile = 'data/config')
42 {
43 $this->configFile = $configFile;
44 $this->initialize();
45 }
46
47 /**
48 * Reset the ConfigManager instance.
49 */
50 public function reset()
51 {
52 $this->initialize();
53 }
54
55 /**
56 * Rebuild the loaded config array from config files.
57 */
58 public function reload()
59 {
60 $this->load();
61 }
62
63 /**
64 * Initialize the ConfigIO and loaded the conf.
65 */
66 protected function initialize()
67 {
68 if (file_exists($this->configFile . '.php')) {
69 $this->configIO = new ConfigPhp();
70 } else {
71 $this->configIO = new ConfigJson();
72 }
73 $this->load();
74 }
75
76 /**
77 * Load configuration in the ConfigurationManager.
78 */
79 protected function load()
80 {
81 $this->loadedConfig = $this->configIO->read($this->getConfigFileExt());
82 $this->setDefaultValues();
83 }
84
85 /**
86 * Get a setting.
87 *
88 * Supports nested settings with dot separated keys.
89 * Eg. 'config.stuff.option' will find $conf[config][stuff][option],
90 * or in JSON:
91 * { "config": { "stuff": {"option": "mysetting" } } } }
92 *
93 * @param string $setting Asked setting, keys separated with dots.
94 * @param string $default Default value if not found.
95 *
96 * @return mixed Found setting, or the default value.
97 */
98 public function get($setting, $default = '')
99 {
100 // During the ConfigIO transition, map legacy settings to the new ones.
101 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
102 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
103 }
104
105 $settings = explode('.', $setting);
106 $value = self::getConfig($settings, $this->loadedConfig);
107 if ($value === self::$NOT_FOUND) {
108 return $default;
109 }
110 return $value;
111 }
112
113 /**
114 * Set a setting, and eventually write it.
115 *
116 * Supports nested settings with dot separated keys.
117 *
118 * @param string $setting Asked setting, keys separated with dots.
119 * @param string $value Value to set.
120 * @param bool $write Write the new setting in the config file, default false.
121 * @param bool $isLoggedIn User login state, default false.
122 *
123 * @throws Exception Invalid
124 */
125 public function set($setting, $value, $write = false, $isLoggedIn = false)
126 {
127 if (empty($setting) || ! is_string($setting)) {
128 throw new Exception('Invalid setting key parameter. String expected, got: '. gettype($setting));
129 }
130
131 // During the ConfigIO transition, map legacy settings to the new ones.
132 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
133 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
134 }
135
136 $settings = explode('.', $setting);
137 self::setConfig($settings, $value, $this->loadedConfig);
138 if ($write) {
139 $this->write($isLoggedIn);
140 }
141 }
142
143 /**
144 * Check if a settings exists.
145 *
146 * Supports nested settings with dot separated keys.
147 *
148 * @param string $setting Asked setting, keys separated with dots.
149 *
150 * @return bool true if the setting exists, false otherwise.
151 */
152 public function exists($setting)
153 {
154 // During the ConfigIO transition, map legacy settings to the new ones.
155 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
156 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
157 }
158
159 $settings = explode('.', $setting);
160 $value = self::getConfig($settings, $this->loadedConfig);
161 if ($value === self::$NOT_FOUND) {
162 return false;
163 }
164 return true;
165 }
166
167 /**
168 * Call the config writer.
169 *
170 * @param bool $isLoggedIn User login state.
171 *
172 * @return bool True if the configuration has been successfully written, false otherwise.
173 *
174 * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf.
175 * @throws UnauthorizedConfigException: user is not authorize to change configuration.
176 * @throws IOException: an error occurred while writing the new config file.
177 */
178 public function write($isLoggedIn)
179 {
180 // These fields are required in configuration.
181 $mandatoryFields = array(
182 'credentials.login',
183 'credentials.hash',
184 'credentials.salt',
185 'security.session_protection_disabled',
186 'general.timezone',
187 'general.title',
188 'general.header_link',
189 'privacy.default_private_links',
190 'redirector.url',
191 );
192
193 // Only logged in user can alter config.
194 if (is_file($this->getConfigFileExt()) && !$isLoggedIn) {
195 throw new UnauthorizedConfigException();
196 }
197
198 // Check that all mandatory fields are provided in $conf.
199 foreach ($mandatoryFields as $field) {
200 if (! $this->exists($field)) {
201 throw new MissingFieldConfigException($field);
202 }
203 }
204
205 return $this->configIO->write($this->getConfigFileExt(), $this->loadedConfig);
206 }
207
208 /**
209 * Set the config file path (without extension).
210 *
211 * @param string $configFile File path.
212 */
213 public function setConfigFile($configFile)
214 {
215 $this->configFile = $configFile;
216 }
217
218 /**
219 * Return the configuration file path (without extension).
220 *
221 * @return string Config path.
222 */
223 public function getConfigFile()
224 {
225 return $this->configFile;
226 }
227
228 /**
229 * Get the configuration file path with its extension.
230 *
231 * @return string Config file path.
232 */
233 public function getConfigFileExt()
234 {
235 return $this->configFile . $this->configIO->getExtension();
236 }
237
238 /**
239 * Recursive function which find asked setting in the loaded config.
240 *
241 * @param array $settings Ordered array which contains keys to find.
242 * @param array $conf Loaded settings, then sub-array.
243 *
244 * @return mixed Found setting or NOT_FOUND flag.
245 */
246 protected static function getConfig($settings, $conf)
247 {
248 if (!is_array($settings) || count($settings) == 0) {
249 return self::$NOT_FOUND;
250 }
251
252 $setting = array_shift($settings);
253 if (!isset($conf[$setting])) {
254 return self::$NOT_FOUND;
255 }
256
257 if (count($settings) > 0) {
258 return self::getConfig($settings, $conf[$setting]);
259 }
260 return $conf[$setting];
261 }
262
263 /**
264 * Recursive function which find asked setting in the loaded config.
265 *
266 * @param array $settings Ordered array which contains keys to find.
267 * @param mixed $value
268 * @param array $conf Loaded settings, then sub-array.
269 *
270 * @return mixed Found setting or NOT_FOUND flag.
271 */
272 protected static function setConfig($settings, $value, &$conf)
273 {
274 if (!is_array($settings) || count($settings) == 0) {
275 return self::$NOT_FOUND;
276 }
277
278 $setting = array_shift($settings);
279 if (count($settings) > 0) {
280 return self::setConfig($settings, $value, $conf[$setting]);
281 }
282 $conf[$setting] = $value;
283 }
284
285 /**
286 * Set a bunch of default values allowing Shaarli to start without a config file.
287 */
288 protected function setDefaultValues()
289 {
290 $this->setEmpty('resource.data_dir', 'data');
291 $this->setEmpty('resource.config', 'data/config.php');
292 $this->setEmpty('resource.datastore', 'data/datastore.php');
293 $this->setEmpty('resource.ban_file', 'data/ipbans.php');
294 $this->setEmpty('resource.updates', 'data/updates.txt');
295 $this->setEmpty('resource.log', 'data/log.txt');
296 $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
297 $this->setEmpty('resource.raintpl_tpl', 'tpl/');
298 $this->setEmpty('resource.raintpl_tmp', 'tmp/');
299 $this->setEmpty('resource.thumbnails_cache', 'cache');
300 $this->setEmpty('resource.page_cache', 'pagecache');
301
302 $this->setEmpty('security.ban_after', 4);
303 $this->setEmpty('security.ban_duration', 1800);
304 $this->setEmpty('security.session_protection_disabled', false);
305 $this->setEmpty('security.open_shaarli', false);
306
307 $this->setEmpty('general.header_link', '?');
308 $this->setEmpty('general.links_per_page', 20);
309 $this->setEmpty('general.enabled_plugins', array('qrcode'));
310
311 $this->setEmpty('updates.check_updates', false);
312 $this->setEmpty('updates.check_updates_branch', 'stable');
313 $this->setEmpty('updates.check_updates_interval', 86400);
314
315 $this->setEmpty('feed.rss_permalinks', true);
316 $this->setEmpty('feed.show_atom', false);
317
318 $this->setEmpty('privacy.default_private_links', false);
319 $this->setEmpty('privacy.hide_public_links', false);
320 $this->setEmpty('privacy.hide_timestamps', false);
321
322 $this->setEmpty('thumbnail.enable_thumbnails', true);
323 $this->setEmpty('thumbnail.enable_localcache', true);
324
325 $this->setEmpty('redirector.url', '');
326 $this->setEmpty('redirector.encode_url', true);
327
328 $this->setEmpty('plugins', array());
329 }
330
331 /**
332 * Set only if the setting does not exists.
333 *
334 * @param string $key Setting key.
335 * @param mixed $value Setting value.
336 */
337 public function setEmpty($key, $value)
338 {
339 if (! $this->exists($key)) {
340 $this->set($key, $value);
341 }
342 }
343
344 /**
345 * @return ConfigIO
346 */
347 public function getConfigIO()
348 {
349 return $this->configIO;
350 }
351
352 /**
353 * @param ConfigIO $configIO
354 */
355 public function setConfigIO($configIO)
356 {
357 $this->configIO = $configIO;
358 }
359}
360
361/**
362 * Exception used if a mandatory field is missing in given configuration.
363 */
364class MissingFieldConfigException extends Exception
365{
366 public $field;
367
368 /**
369 * Construct exception.
370 *
371 * @param string $field field name missing.
372 */
373 public function __construct($field)
374 {
375 $this->field = $field;
376 $this->message = 'Configuration value is required for '. $this->field;
377 }
378}
379
380/**
381 * Exception used if an unauthorized attempt to edit configuration has been made.
382 */
383class UnauthorizedConfigException extends Exception
384{
385 /**
386 * Construct exception.
387 */
388 public function __construct()
389 {
390 $this->message = 'You are not authorized to alter config.';
391 }
392}
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 @@
1<?php
2
3/**
4 * Class ConfigPhp (ConfigIO implementation)
5 *
6 * Handle Shaarli's legacy PHP configuration file.
7 * Note: this is only designed to support the transition to JSON configuration.
8 */
9class ConfigPhp implements ConfigIO
10{
11 /**
12 * @var array List of config key without group.
13 */
14 public static $ROOT_KEYS = array(
15 'login',
16 'hash',
17 'salt',
18 'timezone',
19 'title',
20 'titleLink',
21 'redirector',
22 'disablesessionprotection',
23 'privateLinkByDefault',
24 );
25
26 /**
27 * Map legacy config keys with the new ones.
28 * If ConfigPhp is used, getting <newkey> will actually look for <legacykey>.
29 * The Updater will use this array to transform keys when switching to JSON.
30 *
31 * @var array current key => legacy key.
32 */
33 public static $LEGACY_KEYS_MAPPING = array(
34 'credentials.login' => 'login',
35 'credentials.hash' => 'hash',
36 'credentials.salt' => 'salt',
37 'resource.data_dir' => 'config.DATADIR',
38 'resource.config' => 'config.CONFIG_FILE',
39 'resource.datastore' => 'config.DATASTORE',
40 'resource.updates' => 'config.UPDATES_FILE',
41 'resource.log' => 'config.LOG_FILE',
42 'resource.update_check' => 'config.UPDATECHECK_FILENAME',
43 'resource.raintpl_tpl' => 'config.RAINTPL_TPL',
44 'resource.raintpl_tmp' => 'config.RAINTPL_TMP',
45 'resource.thumbnails_cache' => 'config.CACHEDIR',
46 'resource.page_cache' => 'config.PAGECACHE',
47 'resource.ban_file' => 'config.IPBANS_FILENAME',
48 'security.session_protection_disabled' => 'disablesessionprotection',
49 'security.ban_after' => 'config.BAN_AFTER',
50 'security.ban_duration' => 'config.BAN_DURATION',
51 'general.title' => 'title',
52 'general.timezone' => 'timezone',
53 'general.header_link' => 'titleLink',
54 'updates.check_updates' => 'config.ENABLE_UPDATECHECK',
55 'updates.check_updates_branch' => 'config.UPDATECHECK_BRANCH',
56 'updates.check_updates_interval' => 'config.UPDATECHECK_INTERVAL',
57 'privacy.default_private_links' => 'privateLinkByDefault',
58 'feed.rss_permalinks' => 'config.ENABLE_RSS_PERMALINKS',
59 'general.links_per_page' => 'config.LINKS_PER_PAGE',
60 'thumbnail.enable_thumbnails' => 'config.ENABLE_THUMBNAILS',
61 'thumbnail.enable_localcache' => 'config.ENABLE_LOCALCACHE',
62 'general.enabled_plugins' => 'config.ENABLED_PLUGINS',
63 'redirector.url' => 'redirector',
64 'redirector.encode_url' => 'config.REDIRECTOR_URLENCODE',
65 'feed.show_atom' => 'config.SHOW_ATOM',
66 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS',
67 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS',
68 'security.open_shaarli' => 'config.OPEN_SHAARLI',
69 );
70
71 /**
72 * @inheritdoc
73 */
74 function read($filepath)
75 {
76 if (! file_exists($filepath) || ! is_readable($filepath)) {
77 return array();
78 }
79
80 include $filepath;
81
82 $out = array();
83 foreach (self::$ROOT_KEYS as $key) {
84 $out[$key] = $GLOBALS[$key];
85 }
86 $out['config'] = $GLOBALS['config'];
87 $out['plugins'] = !empty($GLOBALS['plugins']) ? $GLOBALS['plugins'] : array();
88 return $out;
89 }
90
91 /**
92 * @inheritdoc
93 */
94 function write($filepath, $conf)
95 {
96 $configStr = '<?php '. PHP_EOL;
97 foreach (self::$ROOT_KEYS as $key) {
98 if (isset($conf[$key])) {
99 $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
100 }
101 }
102
103 // Store all $conf['config']
104 foreach ($conf['config'] as $key => $value) {
105 $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL;
106 }
107
108 if (isset($conf['plugins'])) {
109 foreach ($conf['plugins'] as $key => $value) {
110 $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($conf['plugins'][$key], true).';'. PHP_EOL;
111 }
112 }
113
114 if (!file_put_contents($filepath, $configStr)
115 || strcmp(file_get_contents($filepath), $configStr) != 0
116 ) {
117 throw new IOException(
118 $filepath,
119 'Shaarli could not create the config file.
120 Please make sure Shaarli has the right to write in the folder is it installed in.'
121 );
122 }
123 }
124
125 /**
126 * @inheritdoc
127 */
128 function getExtension()
129 {
130 return '.php';
131 }
132}
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 @@
1<?php
2/**
3 * Plugin configuration helper functions.
4 *
5 * Note: no access to configuration files here.
6 */
7
8/**
9 * Process plugin administration form data and save it in an array.
10 *
11 * @param array $formData Data sent by the plugin admin form.
12 *
13 * @return array New list of enabled plugin, ordered.
14 *
15 * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
16 */
17function save_plugin_config($formData)
18{
19 // Make sure there are no duplicates in orders.
20 if (!validate_plugin_order($formData)) {
21 throw new PluginConfigOrderException();
22 }
23
24 $plugins = array();
25 $newEnabledPlugins = array();
26 foreach ($formData as $key => $data) {
27 if (startsWith($key, 'order')) {
28 continue;
29 }
30
31 // If there is no order, it means a disabled plugin has been enabled.
32 if (isset($formData['order_' . $key])) {
33 $plugins[(int) $formData['order_' . $key]] = $key;
34 }
35 else {
36 $newEnabledPlugins[] = $key;
37 }
38 }
39
40 // New enabled plugins will be added at the end of order.
41 $plugins = array_merge($plugins, $newEnabledPlugins);
42
43 // Sort plugins by order.
44 if (!ksort($plugins)) {
45 throw new PluginConfigOrderException();
46 }
47
48 $finalPlugins = array();
49 // Make plugins order continuous.
50 foreach ($plugins as $plugin) {
51 $finalPlugins[] = $plugin;
52 }
53
54 return $finalPlugins;
55}
56
57/**
58 * Validate plugin array submitted.
59 * Will fail if there is duplicate orders value.
60 *
61 * @param array $formData Data from submitted form.
62 *
63 * @return bool true if ok, false otherwise.
64 */
65function validate_plugin_order($formData)
66{
67 $orders = array();
68 foreach ($formData as $key => $value) {
69 // No duplicate order allowed.
70 if (in_array($value, $orders)) {
71 return false;
72 }
73
74 if (startsWith($key, 'order')) {
75 $orders[] = $value;
76 }
77 }
78
79 return true;
80}
81
82/**
83 * Affect plugin parameters values into plugins array.
84 *
85 * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>.
86 * @param mixed $conf Plugins configuration.
87 *
88 * @return mixed Updated $plugins array.
89 */
90function load_plugin_parameter_values($plugins, $conf)
91{
92 $out = $plugins;
93 foreach ($plugins as $name => $plugin) {
94 if (empty($plugin['parameters'])) {
95 continue;
96 }
97
98 foreach ($plugin['parameters'] as $key => $param) {
99 if (!empty($conf[$key])) {
100 $out[$name]['parameters'][$key] = $conf[$key];
101 }
102 }
103 }
104
105 return $out;
106}
107
108/**
109 * Exception used if an error occur while saving plugin configuration.
110 */
111class PluginConfigOrderException extends Exception
112{
113 /**
114 * Construct exception.
115 */
116 public function __construct()
117 {
118 $this->message = 'An error occurred while trying to save plugins loading order.';
119 }
120}