diff options
author | Arthur <arthur@hoa.ro> | 2016-07-09 07:19:48 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-07-09 07:19:48 +0200 |
commit | 649af5b501d2a90448242f53764ff693e9854039 (patch) | |
tree | 23cde80a7ee2949e552c48939ae22fa462cfa0fc /application/config | |
parent | a9cfa38df92bd2e1e2c00a67b6ac1516a2116ade (diff) | |
parent | 5ff23f02b80ec6ddee28dee869171ee8e3656b7c (diff) | |
download | Shaarli-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/config')
-rw-r--r-- | application/config/ConfigIO.php | 33 | ||||
-rw-r--r-- | application/config/ConfigJson.php | 78 | ||||
-rw-r--r-- | application/config/ConfigManager.php | 392 | ||||
-rw-r--r-- | application/config/ConfigPhp.php | 132 | ||||
-rw-r--r-- | application/config/ConfigPlugin.php | 120 |
5 files changed, 755 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 | */ | ||
8 | interface 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 | */ | ||
8 | class 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... | ||
4 | require_once 'ConfigIO.php'; | ||
5 | require_once 'ConfigJson.php'; | ||
6 | require_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 | */ | ||
16 | class 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 | */ | ||
364 | class 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 | */ | ||
383 | class 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 | */ | ||
9 | class 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 | */ | ||
17 | function 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 | */ | ||
65 | function 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 | */ | ||
90 | function 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 | */ | ||
111 | class 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 | } | ||