]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/config/ConfigManager.php
Adds ConfigJson which handle the configuration in JSON format.
[github/shaarli/Shaarli.git] / application / config / ConfigManager.php
1 <?php
2
3 // FIXME! Namespaces...
4 require_once 'ConfigIO.php';
5 require_once 'ConfigPhp.php';
6 require_once 'ConfigJson.php';
7
8 /**
9 * Class ConfigManager
10 *
11 * Singleton, manages all Shaarli's settings.
12 */
13 class 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 * Reset the ConfigManager instance.
67 */
68 public static function reset()
69 {
70 self::$instance = null;
71 return self::getInstance();
72 }
73
74 /**
75 * Rebuild the loaded config array from config files.
76 */
77 public function reload()
78 {
79 $this->load();
80 }
81
82 /**
83 * Initialize the ConfigIO and loaded the conf.
84 */
85 protected function initialize()
86 {
87 if (! file_exists(self::$CONFIG_FILE .'.php')) {
88 $this->configIO = new ConfigJson();
89 } else {
90 $this->configIO = new ConfigPhp();
91 }
92 $this->load();
93 }
94
95 /**
96 * Load configuration in the ConfigurationManager.
97 */
98 protected function load()
99 {
100 $this->loadedConfig = $this->configIO->read($this->getConfigFile());
101 $this->setDefaultValues();
102 }
103
104 /**
105 * Get a setting.
106 *
107 * Supports nested settings with dot separated keys.
108 * Eg. 'config.stuff.option' will find $conf[config][stuff][option],
109 * or in JSON:
110 * { "config": { "stuff": {"option": "mysetting" } } } }
111 *
112 * @param string $setting Asked setting, keys separated with dots.
113 * @param string $default Default value if not found.
114 *
115 * @return mixed Found setting, or the default value.
116 */
117 public function get($setting, $default = '')
118 {
119 $settings = explode('.', $setting);
120 $value = self::getConfig($settings, $this->loadedConfig);
121 if ($value === self::$NOT_FOUND) {
122 return $default;
123 }
124 return $value;
125 }
126
127 /**
128 * Set a setting, and eventually write it.
129 *
130 * Supports nested settings with dot separated keys.
131 *
132 * @param string $setting Asked setting, keys separated with dots.
133 * @param string $value Value to set.
134 * @param bool $write Write the new setting in the config file, default false.
135 * @param bool $isLoggedIn User login state, default false.
136 *
137 * @throws Exception Invalid
138 */
139 public function set($setting, $value, $write = false, $isLoggedIn = false)
140 {
141 if (empty($setting) || ! is_string($setting)) {
142 throw new Exception('Invalid setting key parameter. String expected, got: '. gettype($setting));
143 }
144
145 $settings = explode('.', $setting);
146 self::setConfig($settings, $value, $this->loadedConfig);
147 if ($write) {
148 $this->write($isLoggedIn);
149 }
150 }
151
152 /**
153 * Check if a settings exists.
154 *
155 * Supports nested settings with dot separated keys.
156 *
157 * @param string $setting Asked setting, keys separated with dots.
158 *
159 * @return bool true if the setting exists, false otherwise.
160 */
161 public function exists($setting)
162 {
163 $settings = explode('.', $setting);
164 $value = self::getConfig($settings, $this->loadedConfig);
165 if ($value === self::$NOT_FOUND) {
166 return false;
167 }
168 return true;
169 }
170
171 /**
172 * Call the config writer.
173 *
174 * @param bool $isLoggedIn User login state.
175 *
176 * @return bool True if the configuration has been successfully written, false otherwise.
177 *
178 * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf.
179 * @throws UnauthorizedConfigException: user is not authorize to change configuration.
180 * @throws IOException: an error occurred while writing the new config file.
181 */
182 public function write($isLoggedIn)
183 {
184 // These fields are required in configuration.
185 $mandatoryFields = array(
186 'login', 'hash', 'salt', 'timezone', 'title', 'titleLink',
187 'redirector', 'disablesessionprotection', 'privateLinkByDefault'
188 );
189
190 // Only logged in user can alter config.
191 if (is_file(self::$CONFIG_FILE) && !$isLoggedIn) {
192 throw new UnauthorizedConfigException();
193 }
194
195 // Check that all mandatory fields are provided in $conf.
196 foreach ($mandatoryFields as $field) {
197 if (! $this->exists($field)) {
198 throw new MissingFieldConfigException($field);
199 }
200 }
201
202 return $this->configIO->write($this->getConfigFile(), $this->loadedConfig);
203 }
204
205 /**
206 * Get the configuration file path.
207 *
208 * @return string Config file path.
209 */
210 public function getConfigFile()
211 {
212 return self::$CONFIG_FILE . $this->configIO->getExtension();
213 }
214
215 /**
216 * Recursive function which find asked setting in the loaded config.
217 *
218 * @param array $settings Ordered array which contains keys to find.
219 * @param array $conf Loaded settings, then sub-array.
220 *
221 * @return mixed Found setting or NOT_FOUND flag.
222 */
223 protected static function getConfig($settings, $conf)
224 {
225 if (!is_array($settings) || count($settings) == 0) {
226 return self::$NOT_FOUND;
227 }
228
229 $setting = array_shift($settings);
230 if (!isset($conf[$setting])) {
231 return self::$NOT_FOUND;
232 }
233
234 if (count($settings) > 0) {
235 return self::getConfig($settings, $conf[$setting]);
236 }
237 return $conf[$setting];
238 }
239
240 /**
241 * Recursive function which find asked setting in the loaded config.
242 *
243 * @param array $settings Ordered array which contains keys to find.
244 * @param mixed $value
245 * @param array $conf Loaded settings, then sub-array.
246 *
247 * @return mixed Found setting or NOT_FOUND flag.
248 */
249 protected static function setConfig($settings, $value, &$conf)
250 {
251 if (!is_array($settings) || count($settings) == 0) {
252 return self::$NOT_FOUND;
253 }
254
255 $setting = array_shift($settings);
256 if (count($settings) > 0) {
257 return self::setConfig($settings, $value, $conf[$setting]);
258 }
259 $conf[$setting] = $value;
260 }
261
262 /**
263 * Set a bunch of default values allowing Shaarli to start without a config file.
264 */
265 protected function setDefaultValues()
266 {
267 // Data subdirectory
268 $this->setEmpty('config.DATADIR', 'data');
269
270 // Main configuration file
271 $this->setEmpty('config.CONFIG_FILE', 'data/config.php');
272
273 // Link datastore
274 $this->setEmpty('config.DATASTORE', 'data/datastore.php');
275
276 // Banned IPs
277 $this->setEmpty('config.IPBANS_FILENAME', 'data/ipbans.php');
278
279 // Processed updates file.
280 $this->setEmpty('config.UPDATES_FILE', 'data/updates.txt');
281
282 // Access log
283 $this->setEmpty('config.LOG_FILE', 'data/log.txt');
284
285 // For updates check of Shaarli
286 $this->setEmpty('config.UPDATECHECK_FILENAME', 'data/lastupdatecheck.txt');
287
288 // Set ENABLE_UPDATECHECK to disabled by default.
289 $this->setEmpty('config.ENABLE_UPDATECHECK', false);
290
291 // RainTPL cache directory (keep the trailing slash!)
292 $this->setEmpty('config.RAINTPL_TMP', 'tmp/');
293 // Raintpl template directory (keep the trailing slash!)
294 $this->setEmpty('config.RAINTPL_TPL', 'tpl/');
295
296 // Thumbnail cache directory
297 $this->setEmpty('config.CACHEDIR', 'cache');
298
299 // Atom & RSS feed cache directory
300 $this->setEmpty('config.PAGECACHE', 'pagecache');
301
302 // Ban IP after this many failures
303 $this->setEmpty('config.BAN_AFTER', 4);
304 // Ban duration for IP address after login failures (in seconds)
305 $this->setEmpty('config.BAN_DURATION', 1800);
306
307 // Feed options
308 // Enable RSS permalinks by default.
309 // This corresponds to the default behavior of shaarli before this was added as an option.
310 $this->setEmpty('config.ENABLE_RSS_PERMALINKS', true);
311 // If true, an extra "ATOM feed" button will be displayed in the toolbar
312 $this->setEmpty('config.SHOW_ATOM', false);
313
314 // Link display options
315 $this->setEmpty('config.HIDE_PUBLIC_LINKS', false);
316 $this->setEmpty('config.HIDE_TIMESTAMPS', false);
317 $this->setEmpty('config.LINKS_PER_PAGE', 20);
318
319 // Open Shaarli (true): anyone can add/edit/delete links without having to login
320 $this->setEmpty('config.OPEN_SHAARLI', false);
321
322 // Thumbnails
323 // Display thumbnails in links
324 $this->setEmpty('config.ENABLE_THUMBNAILS', true);
325 // Store thumbnails in a local cache
326 $this->setEmpty('config.ENABLE_LOCALCACHE', true);
327
328 // Update check frequency for Shaarli. 86400 seconds=24 hours
329 $this->setEmpty('config.UPDATECHECK_BRANCH', 'stable');
330 $this->setEmpty('config.UPDATECHECK_INTERVAL', 86400);
331
332 $this->setEmpty('redirector', '');
333 $this->setEmpty('config.REDIRECTOR_URLENCODE', true);
334
335 // Enabled plugins.
336 $this->setEmpty('config.ENABLED_PLUGINS', array('qrcode'));
337
338 // Initialize plugin parameters array.
339 $this->setEmpty('plugins', array());
340 }
341
342 /**
343 * Set only if the setting does not exists.
344 *
345 * @param string $key Setting key.
346 * @param mixed $value Setting value.
347 */
348 protected function setEmpty($key, $value)
349 {
350 if (! $this->exists($key)) {
351 $this->set($key, $value);
352 }
353 }
354
355 /**
356 * @return ConfigIO
357 */
358 public function getConfigIO()
359 {
360 return $this->configIO;
361 }
362
363 /**
364 * @param ConfigIO $configIO
365 */
366 public function setConfigIO($configIO)
367 {
368 $this->configIO = $configIO;
369 }
370 }
371
372 /**
373 * Exception used if a mandatory field is missing in given configuration.
374 */
375 class MissingFieldConfigException extends Exception
376 {
377 public $field;
378
379 /**
380 * Construct exception.
381 *
382 * @param string $field field name missing.
383 */
384 public function __construct($field)
385 {
386 $this->field = $field;
387 $this->message = 'Configuration value is required for '. $this->field;
388 }
389 }
390
391 /**
392 * Exception used if an unauthorized attempt to edit configuration has been made.
393 */
394 class UnauthorizedConfigException extends Exception
395 {
396 /**
397 * Construct exception.
398 */
399 public function __construct()
400 {
401 $this->message = 'You are not authorized to alter config.';
402 }
403 }