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