aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/config
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2016-05-18 21:43:59 +0200
committerArthurHoaro <arthur@hoa.ro>2016-06-11 09:30:56 +0200
commit59404d7909b21682ec0782778452a8a70e38b25e (patch)
tree52bbe0b390ecd37f80128d1f8b4f80c3834734a8 /application/config
parent823a363c3b2e10008a607c8b69c1a3d4e9b44ea1 (diff)
downloadShaarli-59404d7909b21682ec0782778452a8a70e38b25e.tar.gz
Shaarli-59404d7909b21682ec0782778452a8a70e38b25e.tar.zst
Shaarli-59404d7909b21682ec0782778452a8a70e38b25e.zip
Introduce a configuration manager (not plugged yet)
Diffstat (limited to 'application/config')
-rw-r--r--application/config/ConfigIO.php33
-rw-r--r--application/config/ConfigManager.php363
-rw-r--r--application/config/ConfigPhp.php93
-rw-r--r--application/config/ConfigPlugin.php118
4 files changed, 607 insertions, 0 deletions
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/ConfigManager.php b/application/config/ConfigManager.php
new file mode 100644
index 00000000..dfe9eeb9
--- /dev/null
+++ b/application/config/ConfigManager.php
@@ -0,0 +1,363 @@
1<?php
2
3// FIXME! Namespaces...
4require_once 'ConfigIO.php';
5require_once 'ConfigPhp.php';
6#require_once 'ConfigJson.php';
7
8/**
9 * Class ConfigManager
10 *
11 * Singleton, manages all Shaarli's settings.
12 */
13class ConfigManager
14{
15 /**
16 * @var ConfigManager instance.
17 */
18 protected static $instance = null;
19
20 /**
21 * @var string Config folder.
22 */
23 public static $CONFIG_FILE = 'data/config';
24
25 /**
26 * @var string Flag telling a setting is not found.
27 */
28 protected static $NOT_FOUND = 'NOT_FOUND';
29
30 /**
31 * @var array Loaded config array.
32 */
33 protected $loadedConfig;
34
35 /**
36 * @var ConfigIO implementation instance.
37 */
38 protected $configIO;
39
40 /**
41 * Private constructor: new instances not allowed.
42 */
43 private function __construct() {}
44
45 /**
46 * Cloning isn't allowed either.
47 */
48 private function __clone() {}
49
50 /**
51 * Return existing instance of PluginManager, or create it.
52 *
53 * @return ConfigManager instance.
54 */
55 public static function getInstance()
56 {
57 if (!(self::$instance instanceof self)) {
58 self::$instance = new self();
59 self::$instance->initialize();
60 }
61
62 return self::$instance;
63 }
64
65 /**
66 * Rebuild the loaded config array from config files.
67 */
68 public function reload()
69 {
70 $this->initialize();
71 }
72
73 /**
74 * Initialize loaded conf in ConfigManager.
75 */
76 protected function initialize()
77 {
78 /*if (! file_exists(self::$CONFIG_FILE .'.php')) {
79 $this->configIO = new ConfigJson();
80 } else {
81 $this->configIO = new ConfigPhp();
82 }*/
83 $this->configIO = new ConfigPhp();
84 $this->loadedConfig = $this->configIO->read(self::$CONFIG_FILE);
85 $this->setDefaultValues();
86 }
87
88 /**
89 * Get a setting.
90 *
91 * Supports nested settings with dot separated keys.
92 * Eg. 'config.stuff.option' will find $conf[config][stuff][option],
93 * or in JSON:
94 * { "config": { "stuff": {"option": "mysetting" } } } }
95 *
96 * @param string $setting Asked setting, keys separated with dots.
97 * @param string $default Default value if not found.
98 *
99 * @return mixed Found setting, or the default value.
100 */
101 public function get($setting, $default = '')
102 {
103 $settings = explode('.', $setting);
104 $value = self::getConfig($settings, $this->loadedConfig);
105 if ($value === self::$NOT_FOUND) {
106 return $default;
107 }
108 return $value;
109 }
110
111 /**
112 * Set a setting, and eventually write it.
113 *
114 * Supports nested settings with dot separated keys.
115 *
116 * @param string $setting Asked setting, keys separated with dots.
117 * @param string $value Value to set.
118 * @param bool $write Write the new setting in the config file, default false.
119 * @param bool $isLoggedIn User login state, default false.
120 */
121 public function set($setting, $value, $write = false, $isLoggedIn = false)
122 {
123 $settings = explode('.', $setting);
124 self::setConfig($settings, $value, $this->loadedConfig);
125 if ($write) {
126 $this->write($isLoggedIn);
127 }
128 }
129
130 /**
131 * Check if a settings exists.
132 *
133 * Supports nested settings with dot separated keys.
134 *
135 * @param string $setting Asked setting, keys separated with dots.
136 *
137 * @return bool true if the setting exists, false otherwise.
138 */
139 public function exists($setting)
140 {
141 $settings = explode('.', $setting);
142 $value = self::getConfig($settings, $this->loadedConfig);
143 if ($value === self::$NOT_FOUND) {
144 return false;
145 }
146 return true;
147 }
148
149 /**
150 * Call the config writer.
151 *
152 * @param bool $isLoggedIn User login state.
153 *
154 * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf.
155 * @throws UnauthorizedConfigException: user is not authorize to change configuration.
156 * @throws IOException: an error occurred while writing the new config file.
157 */
158 public function write($isLoggedIn)
159 {
160 // These fields are required in configuration.
161 $mandatoryFields = array(
162 'login', 'hash', 'salt', 'timezone', 'title', 'titleLink',
163 'redirector', 'disablesessionprotection', 'privateLinkByDefault'
164 );
165
166 // Only logged in user can alter config.
167 if (is_file(self::$CONFIG_FILE) && !$isLoggedIn) {
168 throw new UnauthorizedConfigException();
169 }
170
171 // Check that all mandatory fields are provided in $conf.
172 foreach ($mandatoryFields as $field) {
173 if (! $this->exists($field)) {
174 throw new MissingFieldConfigException($field);
175 }
176 }
177
178 $this->configIO->write(self::$CONFIG_FILE, $this->loadedConfig);
179 }
180
181 /**
182 * Get the configuration file path.
183 *
184 * @return string Config file path.
185 */
186 public function getConfigFile()
187 {
188 return self::$CONFIG_FILE . $this->configIO->getExtension();
189 }
190
191 /**
192 * Recursive function which find asked setting in the loaded config.
193 *
194 * @param array $settings Ordered array which contains keys to find.
195 * @param array $conf Loaded settings, then sub-array.
196 *
197 * @return mixed Found setting or NOT_FOUND flag.
198 */
199 protected static function getConfig($settings, $conf)
200 {
201 if (!is_array($settings) || count($settings) == 0) {
202 return self::$NOT_FOUND;
203 }
204
205 $setting = array_shift($settings);
206 if (!isset($conf[$setting])) {
207 return self::$NOT_FOUND;
208 }
209
210 if (count($settings) > 0) {
211 return self::getConfig($settings, $conf[$setting]);
212 }
213 return $conf[$setting];
214 }
215
216 /**
217 * Recursive function which find asked setting in the loaded config.
218 *
219 * @param array $settings Ordered array which contains keys to find.
220 * @param mixed $value
221 * @param array $conf Loaded settings, then sub-array.
222 *
223 * @return mixed Found setting or NOT_FOUND flag.
224 */
225 protected static function setConfig($settings, $value, &$conf)
226 {
227 if (!is_array($settings) || count($settings) == 0) {
228 return self::$NOT_FOUND;
229 }
230
231 $setting = array_shift($settings);
232 if (count($settings) > 0) {
233 return self::setConfig($settings, $value, $conf[$setting]);
234 }
235 $conf[$setting] = $value;
236 }
237
238 /**
239 * Set a bunch of default values allowing Shaarli to start without a config file.
240 */
241 protected function setDefaultValues()
242 {
243 // Data subdirectory
244 $this->setEmpty('config.DATADIR', 'data');
245
246 // Main configuration file
247 $this->setEmpty('config.CONFIG_FILE', 'data/config.php');
248
249 // Link datastore
250 $this->setEmpty('config.DATASTORE', 'data/datastore.php');
251
252 // Banned IPs
253 $this->setEmpty('config.IPBANS_FILENAME', 'data/ipbans.php');
254
255 // Processed updates file.
256 $this->setEmpty('config.UPDATES_FILE', 'data/updates.txt');
257
258 // Access log
259 $this->setEmpty('config.LOG_FILE', 'data/log.txt');
260
261 // For updates check of Shaarli
262 $this->setEmpty('config.UPDATECHECK_FILENAME', 'data/lastupdatecheck.txt');
263
264 // Set ENABLE_UPDATECHECK to disabled by default.
265 $this->setEmpty('config.ENABLE_UPDATECHECK', false);
266
267 // RainTPL cache directory (keep the trailing slash!)
268 $this->setEmpty('config.RAINTPL_TMP', 'tmp/');
269 // Raintpl template directory (keep the trailing slash!)
270 $this->setEmpty('config.RAINTPL_TPL', 'tpl/');
271
272 // Thumbnail cache directory
273 $this->setEmpty('config.CACHEDIR', 'cache');
274
275 // Atom & RSS feed cache directory
276 $this->setEmpty('config.PAGECACHE', 'pagecache');
277
278 // Ban IP after this many failures
279 $this->setEmpty('config.BAN_AFTER', 4);
280 // Ban duration for IP address after login failures (in seconds)
281 $this->setEmpty('config.BAN_DURATION', 1800);
282
283 // Feed options
284 // Enable RSS permalinks by default.
285 // This corresponds to the default behavior of shaarli before this was added as an option.
286 $this->setEmpty('config.ENABLE_RSS_PERMALINKS', true);
287 // If true, an extra "ATOM feed" button will be displayed in the toolbar
288 $this->setEmpty('config.SHOW_ATOM', false);
289
290 // Link display options
291 $this->setEmpty('config.HIDE_PUBLIC_LINKS', false);
292 $this->setEmpty('config.HIDE_TIMESTAMPS', false);
293 $this->setEmpty('config.LINKS_PER_PAGE', 20);
294
295 // Open Shaarli (true): anyone can add/edit/delete links without having to login
296 $this->setEmpty('config.OPEN_SHAARLI', false);
297
298 // Thumbnails
299 // Display thumbnails in links
300 $this->setEmpty('config.ENABLE_THUMBNAILS', true);
301 // Store thumbnails in a local cache
302 $this->setEmpty('config.ENABLE_LOCALCACHE', true);
303
304 // Update check frequency for Shaarli. 86400 seconds=24 hours
305 $this->setEmpty('config.UPDATECHECK_BRANCH', 'stable');
306 $this->setEmpty('config.UPDATECHECK_INTERVAL', 86400);
307
308 $this->setEmpty('redirector', '');
309 $this->setEmpty('config.REDIRECTOR_URLENCODE', true);
310
311 // Enabled plugins.
312 $this->setEmpty('config.ENABLED_PLUGINS', array('qrcode'));
313
314 // Initialize plugin parameters array.
315 $this->setEmpty('plugins', array());
316 }
317
318 /**
319 * Set only if the setting does not exists.
320 *
321 * @param string $key Setting key.
322 * @param mixed $value Setting value.
323 */
324 protected function setEmpty($key, $value)
325 {
326 if (! $this->exists($key)) {
327 $this->set($key, $value);
328 }
329 }
330}
331
332/**
333 * Exception used if a mandatory field is missing in given configuration.
334 */
335class MissingFieldConfigException extends Exception
336{
337 public $field;
338
339 /**
340 * Construct exception.
341 *
342 * @param string $field field name missing.
343 */
344 public function __construct($field)
345 {
346 $this->field = $field;
347 $this->message = 'Configuration value is required for '. $this->field;
348 }
349}
350
351/**
352 * Exception used if an unauthorized attempt to edit configuration has been made.
353 */
354class UnauthorizedConfigException extends Exception
355{
356 /**
357 * Construct exception.
358 */
359 public function __construct()
360 {
361 $this->message = 'You are not authorized to alter config.';
362 }
363}
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 @@
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 * @inheritdoc
28 */
29 function read($filepath)
30 {
31 $filepath .= $this->getExtension();
32 if (! file_exists($filepath) || ! is_readable($filepath)) {
33 return array();
34 }
35
36 include $filepath;
37
38 $out = array();
39 foreach (self::$ROOT_KEYS as $key) {
40 $out[$key] = $GLOBALS[$key];
41 }
42 $out['config'] = $GLOBALS['config'];
43 $out['plugins'] = !empty($GLOBALS['plugins']) ? $GLOBALS['plugins'] : array();
44 return $out;
45 }
46
47 /**
48 * @inheritdoc
49 */
50 function write($filepath, $conf)
51 {
52 $filepath .= $this->getExtension();
53
54 $configStr = '<?php '. PHP_EOL;
55 foreach (self::$ROOT_KEYS as $key) {
56 if (isset($conf[$key])) {
57 $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
58 }
59 }
60
61 // Store all $conf['config']
62 foreach ($conf['config'] as $key => $value) {
63 $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL;
64 }
65
66 if (isset($conf['plugins'])) {
67 foreach ($conf['plugins'] as $key => $value) {
68 $configStr .= '$GLOBALS[\'plugins\'][\''. $key .'\'] = '.var_export($conf['plugins'][$key], true).';'. PHP_EOL;
69 }
70 }
71
72 // FIXME!
73 //$configStr .= 'date_default_timezone_set('.var_export($conf['timezone'], true).');'. PHP_EOL;
74
75 if (!file_put_contents($filepath, $configStr)
76 || strcmp(file_get_contents($filepath), $configStr) != 0
77 ) {
78 throw new IOException(
79 $filepath,
80 'Shaarli could not create the config file.
81 Please make sure Shaarli has the right to write in the folder is it installed in.'
82 );
83 }
84 }
85
86 /**
87 * @inheritdoc
88 */
89 function getExtension()
90 {
91 return '.php';
92 }
93}
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 @@
1<?php
2/**
3 * Functions related to configuration management.
4 */
5
6/**
7 * Process plugin administration form data and save it in an array.
8 *
9 * @param array $formData Data sent by the plugin admin form.
10 *
11 * @return array New list of enabled plugin, ordered.
12 *
13 * @throws PluginConfigOrderException Plugins can't be sorted because their order is invalid.
14 */
15function save_plugin_config($formData)
16{
17 // Make sure there are no duplicates in orders.
18 if (!validate_plugin_order($formData)) {
19 throw new PluginConfigOrderException();
20 }
21
22 $plugins = array();
23 $newEnabledPlugins = array();
24 foreach ($formData as $key => $data) {
25 if (startsWith($key, 'order')) {
26 continue;
27 }
28
29 // If there is no order, it means a disabled plugin has been enabled.
30 if (isset($formData['order_' . $key])) {
31 $plugins[(int) $formData['order_' . $key]] = $key;
32 }
33 else {
34 $newEnabledPlugins[] = $key;
35 }
36 }
37
38 // New enabled plugins will be added at the end of order.
39 $plugins = array_merge($plugins, $newEnabledPlugins);
40
41 // Sort plugins by order.
42 if (!ksort($plugins)) {
43 throw new PluginConfigOrderException();
44 }
45
46 $finalPlugins = array();
47 // Make plugins order continuous.
48 foreach ($plugins as $plugin) {
49 $finalPlugins[] = $plugin;
50 }
51
52 return $finalPlugins;
53}
54
55/**
56 * Validate plugin array submitted.
57 * Will fail if there is duplicate orders value.
58 *
59 * @param array $formData Data from submitted form.
60 *
61 * @return bool true if ok, false otherwise.
62 */
63function validate_plugin_order($formData)
64{
65 $orders = array();
66 foreach ($formData as $key => $value) {
67 // No duplicate order allowed.
68 if (in_array($value, $orders)) {
69 return false;
70 }
71
72 if (startsWith($key, 'order')) {
73 $orders[] = $value;
74 }
75 }
76
77 return true;
78}
79
80/**
81 * Affect plugin parameters values into plugins array.
82 *
83 * @param mixed $plugins Plugins array ($plugins[<plugin_name>]['parameters']['param_name'] = <value>.
84 * @param mixed $conf Plugins configuration.
85 *
86 * @return mixed Updated $plugins array.
87 */
88function load_plugin_parameter_values($plugins, $conf)
89{
90 $out = $plugins;
91 foreach ($plugins as $name => $plugin) {
92 if (empty($plugin['parameters'])) {
93 continue;
94 }
95
96 foreach ($plugin['parameters'] as $key => $param) {
97 if (!empty($conf[$key])) {
98 $out[$name]['parameters'][$key] = $conf[$key];
99 }
100 }
101 }
102
103 return $out;
104}
105
106/**
107 * Exception used if an error occur while saving plugin configuration.
108 */
109class PluginConfigOrderException extends Exception
110{
111 /**
112 * Construct exception.
113 */
114 public function __construct()
115 {
116 $this->message = 'An error occurred while trying to save plugins loading order.';
117 }
118}