1<?php
2
3/**
4 * Configuration objects
5 * @package framework
6 * @subpackage config
7 */
8
9/**
10 * Base class for both site and user configuration data management
11 */
12abstract class Hm_Config {
13
14    /* config source */
15    protected $source = '';
16
17    /* config data */
18    protected $config = array('version' => VERSION);
19
20    /* flag indicating failed decryption */
21    public $decrypt_failed = false;
22
23    /* if decryption fails, save the encrypted payload */
24    public $encrypted_str;
25
26    /**
27     * This method must be overriden by classes extending this one
28     * @param string $source source or identifier to determine the source
29     * @param string $key encryption key
30     */
31    abstract public function load($source, $key);
32
33    /**
34     * Return all config values
35     * @return array list of config values
36     */
37    public function dump() {
38        return $this->config;
39    }
40
41    /**
42     * Delete a setting
43     * @param string $name config option name
44     * @return bool true on success
45     */
46    public function del($name) {
47        if (array_key_exists($name, $this->config)) {
48            unset($this->config[$name]);
49            return true;
50        }
51        return false;
52    }
53
54    /**
55     * Return a versoin number
56     * @return float
57     */
58    public function version() {
59        if (array_key_exists('version', $this->config)) {
60            return $this->config['version'];
61        }
62        return .1;
63    }
64
65    /**
66     * Set a config value
67     * @param string $name config value name
68     * @param string $value config value
69     * @return void
70     */
71    public function set($name, $value) {
72        $this->config[$name] = $value;
73    }
74
75    /**
76     * Return a config value if it exists
77     * @param string $name config value name
78     * @param false|string $default value to return if the name is not found
79     * @return mixed found value, otherwise $default
80     */
81    public function get($name, $default=false) {
82        return array_key_exists($name, $this->config) ? $this->config[$name] : $default;
83    }
84
85    /**
86     * Set the timezone
87     * @return void
88     */
89    public function set_tz() {
90        date_default_timezone_set($this->get('timezone_setting', 'UTC'));
91    }
92
93    /**
94     * Shuffle the config value order
95     * @return void
96     */
97    public function shuffle() {
98        $new_config = array();
99        $keys = array_keys($this->config);
100        shuffle($keys);
101        foreach ($keys as $key) {
102            $new_config[$key] = $this->config[$key];
103        }
104        $this->config = $new_config;
105    }
106
107    /**
108     * Decode user settings with json_decode or unserialize depending
109     * on the format
110     * @param string|false $data serialized or json encoded string
111     * @return mixed array, or false on failure
112     */
113    public function decode($data) {
114        if (!is_string($data) || !trim($data)) {
115            return false;
116        }
117        return Hm_Transform::convert($data);
118    }
119
120    /**
121     * Filter out default auth and SMTP servers so they don't get saved
122     * to the permanent user config. These are dynamically reloaded on
123     * login
124     * @return array of items removed
125     */
126    public function filter_servers() {
127        $removed = array();
128        $excluded = array('pop3_servers', 'imap_servers','smtp_servers');
129        $no_password = $this->get('no_password_save_setting', false);
130        foreach ($this->config as $key => $vals) {
131            if (in_array($key, $excluded, true)) {
132                foreach ($vals as $index => $server) {
133                    if (array_key_exists('default', $server) && $server['default']) {
134                        $removed[$key][$index] = $server;
135                        unset($this->config[$key][$index]);
136                    }
137                    elseif (!array_key_exists('server', $server)) {
138                        $removed[$key][$index] = $server;
139                        unset($this->config[$key][$index]);
140                    }
141                    else {
142                        $this->config[$key][$index]['object'] = false;
143                        if ($no_password) {
144                            if (!array_key_exists('auth', $server) || $server['auth'] != 'xoauth2') {
145                                $removed[$key][$index]['pass'] = $server['pass'];
146                                unset($this->config[$key][$index]['pass']);
147                            }
148                        }
149                    }
150                }
151            }
152        }
153        return $removed;
154    }
155
156    /**
157     * Restore server definitions removed before saving
158     * @param array $removed server info to restore
159     * @return void
160     */
161    public function restore_servers($removed) {
162        foreach ($removed as $key => $vals) {
163            foreach ($vals as $index => $server) {
164                if (is_array($server)) {
165                    $this->config[$key][$index] = $server;
166                }
167                else {
168                    $this->config[$key][$index]['pass'] = $server;
169                }
170            }
171        }
172    }
173}
174
175/**
176 * File based user settings
177 */
178class Hm_User_Config_File extends Hm_Config {
179
180    /* config values */
181    private $site_config;
182
183    /* encrption flag */
184    private $crypt;
185
186    /* username */
187    private $username;
188
189    /**
190     * Load site configuration
191     * @param object $config site config
192     */
193    public function __construct($config) {
194        $this->crypt = crypt_state($config);
195        $this->site_config = $config;
196        $this->config = array_merge($this->config, $config->user_defaults);
197    }
198
199    /**
200     * Get the filesystem path for a user settings file
201     * @param string $username username
202     * @return string filepath to the user config file
203     */
204    public function get_path($username) {
205        $path = $this->site_config->get('user_settings_dir', false);
206        return sprintf('%s/%s.txt', $path, $username);
207    }
208
209    /**
210     * Load the settings for a user
211     * @param string $username username
212     * @param string $key key to decrypt the user data
213     * @return void
214     */
215    public function load($username, $key) {
216        $this->username = $username;
217        $source = $this->get_path($username);
218        if (is_readable($source)) {
219            $str_data = file_get_contents($source);
220            if ($str_data) {
221                if (!$this->crypt) {
222                    $data = $this->decode($str_data);
223                }
224                else {
225                    $data = $this->decode(Hm_Crypt::plaintext($str_data, $key));
226                }
227                if (is_array($data)) {
228                    $this->config = array_merge($this->config, $data);
229                    $this->set_tz();
230                }
231                else {
232                    $this->decrypt_failed = true;
233                    $this->encrypted_str = $str_data;
234                }
235            }
236        }
237    }
238
239    /**
240     * Reload from outside input
241     * @param array $data new user data
242     * @param string $username
243     * @return void
244     */
245    public function reload($data, $username=false) {
246        $this->username = $username;
247        $this->config = $data;
248        $this->set_tz();
249    }
250
251    /**
252     * Save user settings to a file
253     * @param string $username username
254     * @param string $key encryption key
255     * @return void
256     */
257    public function save($username, $key) {
258        $this->shuffle();
259        $destination = $this->get_path($username);
260        $removed = $this->filter_servers();
261        if (!$this->crypt) {
262            $data = json_encode($this->config);
263        }
264        else {
265            $data = Hm_Crypt::ciphertext(json_encode($this->config), $key);
266        }
267        file_put_contents($destination, $data);
268        $this->restore_servers($removed);
269    }
270
271    /**
272     * Set a config value
273     * @param string $name config value name
274     * @param string $value config value
275     * @return void
276     */
277    public function set($name, $value) {
278        $this->config[$name] = $value;
279        if (!$this->crypt) {
280            $this->save($this->username, false);
281        }
282    }
283}
284
285/**
286 * DB based user settings
287 */
288class Hm_User_Config_DB extends Hm_Config {
289
290    /* site configuration */
291    private $site_config;
292
293    /* DB connection handle */
294    private $dbh;
295
296    /* encrption class */
297    private $crypt;
298
299    /* username */
300    private $username;
301
302    /**
303     * Load site config
304     * @param object $config site config
305     */
306    public function __construct($config) {
307        $this->crypt = crypt_state($config);
308        $this->site_config = $config;
309        $this->config = array_merge($this->config, $config->user_defaults);
310    }
311
312    /**
313     * @param string $username
314     * @return boolean
315     */
316    private function new_settings($username) {
317        $res = Hm_DB::execute($this->dbh, 'insert into hm_user_settings values(?,?)', array($username, ''));
318        Hm_Debug::add(sprintf("created new row in hm_user_settings for %s", $username));
319        $this->config = array();
320        return $res ? true : false;
321    }
322
323    /**
324     * @param array $data
325     * @param string $key
326     * @return boolean
327     */
328    private function decrypt_settings($data, $key) {
329        if (!$this->crypt) {
330            $data = $this->decode($data['settings']);
331        }
332        else {
333            $data = $this->decode(Hm_Crypt::plaintext($data['settings'], $key));
334        }
335        if (is_array($data)) {
336            $this->config = array_merge($this->config, $data);
337            $this->set_tz();
338            return true;
339        }
340        else {
341            $this->decrypt_failed = true;
342            return false;
343        }
344    }
345
346    /**
347     * Load the user settings from the DB
348     * @param string $username username
349     * @param string $key encryption key
350     * @return boolean
351     */
352    public function load($username, $key) {
353        $this->username = $username;
354        $this->connect();
355        $data = Hm_DB::execute($this->dbh, 'select * from hm_user_settings where username=?', array($username));
356        if (!$data || !array_key_exists('settings', $data)) {
357            return $this->new_settings($username);
358        }
359        return $this->decrypt_settings($data, $key);
360    }
361
362    /**
363     * Reload from outside input
364     * @param array $data new user data
365     * @param string $username
366     * @return void
367     */
368    public function reload($data, $username=false) {
369        $this->username = $username;
370        $this->config = $data;
371        $this->set_tz();
372    }
373
374    /**
375     * Connect to a configured DB
376     * @return bool true on success
377     */
378    public function connect() {
379        return ($this->dbh = Hm_DB::connect($this->site_config)) ? true : false;
380    }
381
382    /**
383     * Save user settings to the DB
384     * @param string $username username
385     * @param string $key encryption key
386     * @return integer|boolean|array
387     */
388    public function save($username, $key) {
389        $this->shuffle();
390        $removed = $this->filter_servers();
391        if (!$this->crypt) {
392            $config = json_encode($this->config);
393        }
394        else {
395            $config = Hm_Crypt::ciphertext(json_encode($this->config), $key);
396        }
397        $this->connect();
398        if (Hm_DB::execute($this->dbh, 'update hm_user_settings set settings=? where username=?', array($config, $username))) {
399            Hm_Debug::add(sprintf("Saved user data to DB for %s", $username));
400            $res = true;
401        }
402        else {
403            $res = Hm_DB::execute($this->dbh, 'insert into hm_user_settings values(?,?)', array($username, $config));
404        }
405        $this->restore_servers($removed);
406        return $res;
407    }
408
409    /**
410     * Set a config value
411     * @param string $name config value name
412     * @param string $value config value
413     * @return void
414     */
415    public function set($name, $value) {
416        $this->config[$name] = $value;
417        if (!$this->crypt) {
418            $this->save($this->username, false);
419        }
420    }
421}
422
423/**
424 * File based site configuration
425 */
426class Hm_Site_Config_File extends Hm_Config {
427
428    public $user_defaults = array();
429
430    /**
431     * Load data based on source
432     * @param string $source source location for site configuration
433     */
434    public function __construct($source) {
435        $this->load($source, false);
436    }
437
438    /**
439     * Load site data from a file
440     * @param string $source file path to the site configuration
441     * @param string $key encryption key (unsued in this class)
442     * @return void
443     */
444    public function load($source, $key) {
445        if (is_readable($source)) {
446            $data = $this->decode(file_get_contents($source));
447            if ($data) {
448                $this->config = array_merge($this->config, $data);
449                $this->get_user_defaults();
450            }
451        }
452    }
453
454    /*
455     * Determine default values for users without any settings
456     * @return void
457     */
458    private function get_user_defaults() {
459        foreach ($this->config as $name => $val) {
460            if (substr($name, 0, 15) == 'default_setting') {
461                $this->user_defaults[substr($name, 16).'_setting'] = $val;
462            }
463        }
464    }
465
466    /**
467     * Return a list of modules as an array
468     * @return array|false
469     */
470    public function get_modules() {
471        $mods = $this->get('modules');
472        if (is_string($mods)) {
473            return explode(',', $mods);
474        }
475        return $mods;
476    }
477}
478
479/**
480 * Load a user config object
481 * @param object $config site configuration
482 * @return object
483 */
484function load_user_config_object($config) {
485    $type = $config->get('user_config_type', 'file');
486    if (strstr($type, ':')) {
487        list($type, $class) = explode(':', $type);
488    }
489    switch ($type) {
490        case 'DB':
491            $user_config = new Hm_User_Config_DB($config);
492            Hm_Debug::add("Using DB user configuration");
493            break;
494        case 'custom':
495            if (class_exists($class)) {
496                $user_config = new $class($config);
497                Hm_Debug::add("Using custom user configuration: $class");
498                break;
499            } else {
500                Hm_Debug::add("User configuration class does not exist: $class");
501            }
502        default:
503            $user_config = new Hm_User_Config_File($config);
504            Hm_Debug::add("Using file based user configuration");
505            break;
506    }
507    return $user_config;
508}
509
510/**
511 * Determine encryption for user settings
512 * @param object $config site configuration
513 * @return boolean
514 */
515function crypt_state($config) {
516    if ($config->get('single_server_mode') &&
517        in_array($config->get('auth_type'), array('IMAP', 'POP3'), true)) {
518        return false;
519    }
520    return true;
521}
522