Aleksander Machniak
2015-02-27 8defdaf0b1864f8e420be65823c0bf72426254cc
commit | author | age
197601 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
e019f2 5  | This file is part of the Roundcube Webmail client                     |
fb5f52 6  | Copyright (C) 2008-2013, The Roundcube Dev Team                       |
7fe381 7  |                                                                       |
T 8  | Licensed under the GNU General Public License version 3 or            |
9  | any later version with exceptions for skins & plugins.                |
10  | See the README file for a full license statement.                     |
197601 11  |                                                                       |
T 12  | PURPOSE:                                                              |
13  |   Class to read configuration settings                                |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  +-----------------------------------------------------------------------+
17 */
18
19 /**
e019f2 20  * Configuration class for Roundcube
197601 21  *
9ab346 22  * @package    Framework
AM 23  * @subpackage Core
197601 24  */
T 25 class rcube_config
26 {
aff970 27     const DEFAULT_SKIN = 'larry';
TB 28
deb2b8 29     private $env = '';
fb5f52 30     private $paths = array();
2eb794 31     private $prop = array();
A 32     private $errors = array();
33     private $userprefs = array();
0ac416 34
A 35     /**
36      * Renamed options
37      *
38      * @var array
39      */
40     private $legacy_props = array(
41         // new name => old name
42         'default_folders'      => 'default_imap_folders',
43         'mail_pagesize'        => 'pagesize',
44         'addressbook_pagesize' => 'pagesize',
651c7b 45         'reply_mode'           => 'top_posting',
f22654 46         'refresh_interval'     => 'keep_alive',
AM 47         'min_refresh_interval' => 'min_keep_alive',
67ac6e 48         'messages_cache_ttl'   => 'message_cache_lifetime',
df9d00 49         'redundant_attachments_cache_ttl' => 'redundant_attachments_memcache_ttl',
0ac416 50     );
197601 51
T 52
2eb794 53     /**
A 54      * Object constructor
deb2b8 55      *
TB 56      * @param string Environment suffix for config files to load
2eb794 57      */
deb2b8 58     public function __construct($env = '')
2471d3 59     {
deb2b8 60         $this->env = $env;
fb5f52 61
TB 62         if ($paths = getenv('RCUBE_CONFIG_PATH')) {
63             $this->paths = explode(PATH_SEPARATOR, $paths);
64             // make all paths absolute
65             foreach ($this->paths as $i => $path) {
f130f9 66                 if (!rcube_utils::is_absolute_path($path)) {
fb5f52 67                     if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
TB 68                         $this->paths[$i] = unslashify($realpath) . '/';
69                     }
70                     else {
71                         unset($this->paths[$i]);
72                     }
73                 }
74                 else {
75                     $this->paths[$i] = unslashify($path) . '/';
76                 }
77             }
78         }
79
80         if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) {
81             $this->paths[] = RCUBE_CONFIG_DIR;
82         }
83
84         if (empty($this->paths)) {
85             $this->paths[] = RCUBE_INSTALL_PATH . 'config/';
86         }
deb2b8 87
2eb794 88         $this->load();
baecd8 89
TB 90         // Defaults, that we do not require you to configure,
91         // but contain information that is used in various
92         // locations in the code:
93         $this->set('contactlist_fields', array('name', 'firstname', 'surname', 'email'));
2471d3 94     }
2eb794 95
A 96
97     /**
98      * Load config from local config file
99      *
100      * @todo Remove global $CONFIG
101      */
102     private function load()
2471d3 103     {
461a30 104         // Load default settings
deb2b8 105         if (!$this->load_from_file('defaults.inc.php')) {
461a30 106             $this->errors[] = 'defaults.inc.php was not found.';
AM 107         }
2eb794 108
461a30 109         // load main config file
deb2b8 110         if (!$this->load_from_file('config.inc.php')) {
461a30 111             // Old configuration files
deb2b8 112             if (!$this->load_from_file('main.inc.php') ||
TB 113                 !$this->load_from_file('db.inc.php')) {
0f39b4 114                 $this->errors[] = 'config.inc.php was not found.';
TB 115             }
116             else if (rand(1,100) == 10) {  // log warning on every 100th request (average)
117                 trigger_error("config.inc.php was not found. Please migrate your config by running bin/update.sh", E_USER_WARNING);
118             }
461a30 119         }
f52c4f 120
2eb794 121         // load host-specific configuration
955a61 122         $this->load_host_config();
2eb794 123
A 124         // set skin (with fallback to old 'skin_path' property)
740875 125         if (empty($this->prop['skin'])) {
AM 126             if (!empty($this->prop['skin_path'])) {
127                 $this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
128             }
129             else {
aff970 130                 $this->prop['skin'] = self::DEFAULT_SKIN;
740875 131             }
AM 132         }
9f1652 133
TB 134         // larry is the new default skin :-)
135         if ($this->prop['skin'] == 'default')
aff970 136             $this->prop['skin'] = self::DEFAULT_SKIN;
2eb794 137
A 138         // fix paths
9be2f4 139         $this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : RCUBE_INSTALL_PATH . 'logs';
TB 140         $this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : RCUBE_INSTALL_PATH . 'temp';
f52c4f 141
2eb794 142         // fix default imap folders encoding
A 143         foreach (array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox') as $folder)
a92beb 144             $this->prop[$folder] = rcube_charset::convert($this->prop[$folder], RCUBE_CHARSET, 'UTF7-IMAP');
2eb794 145
c321a9 146         if (!empty($this->prop['default_folders']))
T 147             foreach ($this->prop['default_folders'] as $n => $folder)
a92beb 148                 $this->prop['default_folders'][$n] = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
2eb794 149
A 150         // set PHP error logging according to config
151         if ($this->prop['debug_level'] & 1) {
152             ini_set('log_errors', 1);
153
154             if ($this->prop['log_driver'] == 'syslog') {
155                 ini_set('error_log', 'syslog');
156             }
157             else {
158                 ini_set('error_log', $this->prop['log_dir'].'/errors');
159             }
160         }
23b495 161
A 162         // enable display_errors in 'show' level, but not for ajax requests
163         ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4)));
0829b7 164
5879c0 165         // remove deprecated properties
T 166         unset($this->prop['dst_active']);
2eb794 167
A 168         // export config data
169         $GLOBALS['CONFIG'] = &$this->prop;
2471d3 170     }
1854c4 171
2eb794 172     /**
A 173      * Load a host-specific config file if configured
174      * This will merge the host specific configuration with the given one
175      */
176     private function load_host_config()
177     {
955a61 178         if (empty($this->prop['include_host_config'])) {
AM 179             return;
2eb794 180         }
2471d3 181
955a61 182         foreach (array('HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR') as $key) {
AM 183             $fname = null;
184             $name  = $_SERVER[$key];
185
186             if (!$name) {
187                 continue;
188             }
189
190             if (is_array($this->prop['include_host_config'])) {
191                 $fname = $this->prop['include_host_config'][$name];
192             }
193             else {
194                 $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $name) . '.inc.php';
195             }
196
197             if ($fname && $this->load_from_file($fname)) {
198                 return;
199             }
2eb794 200         }
83a763 201     }
2eb794 202
A 203     /**
204      * Read configuration from a file
205      * and merge with the already stored config values
206      *
deb2b8 207      * @param string $file Name of the config file to be loaded
2eb794 208      * @return booelan True on success, false on failure
A 209      */
deb2b8 210     public function load_from_file($file)
2eb794 211     {
fb5f52 212         $success = false;
7c9850 213
fb5f52 214         foreach ($this->resolve_paths($file) as $fpath) {
TB 215             if ($fpath && is_file($fpath) && is_readable($fpath)) {
216                 // use output buffering, we don't need any output here 
217                 ob_start();
218                 include($fpath);
219                 ob_end_clean();
220
221                 if (is_array($config)) {
222                     $this->merge($config);
223                     $success = true;
224                 }
225                 // deprecated name of config variable
a315bf 226                 if (is_array($rcmail_config)) {
fb5f52 227                     $this->merge($rcmail_config);
TB 228                     $success = true;
229                 }
2eb794 230             }
A 231         }
232
fb5f52 233         return $success;
2eb794 234     }
A 235
deb2b8 236     /**
fb5f52 237      * Helper method to resolve absolute paths to the given config file.
deb2b8 238      * This also takes the 'env' property into account.
fb5f52 239      *
TB 240      * @param string  Filename or absolute file path
241      * @param boolean Return -$env file path if exists
242      * @return array  List of candidates in config dir path(s)
deb2b8 243      */
9e9d62 244     public function resolve_paths($file, $use_env = true)
deb2b8 245     {
f130f9 246         $files    = array();
AM 247         $abs_path = rcube_utils::is_absolute_path($file);
fb5f52 248
TB 249         foreach ($this->paths as $basepath) {
250             $realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
251
252             // check if <file>-env.ini exists
253             if ($realpath && $use_env && !empty($this->env)) {
254                 $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $realpath);
255                 if (is_file($envfile))
256                     $realpath = $envfile;
257             }
258
259             if ($realpath) {
260                 $files[] = $realpath;
261
262                 // no need to continue the loop if an absolute file path is given
263                 if ($abs_path) {
264                     break;
265                 }
266             }
deb2b8 267         }
TB 268
fb5f52 269         return $files;
TB 270     }
2eb794 271
A 272     /**
273      * Getter for a specific config parameter
274      *
5c461b 275      * @param  string $name Parameter name
A 276      * @param  mixed  $def  Default value if not set
2eb794 277      * @return mixed  The requested config value
A 278      */
279     public function get($name, $def = null)
280     {
0ac416 281         if (isset($this->prop[$name])) {
A 282             $result = $this->prop[$name];
283         }
284         else {
285             $result = $def;
286         }
287
be98df 288         $rcube = rcube::get_instance();
0ac416 289
7ebe06 290         if ($name == 'timezone') {
AM 291             if (empty($result) || $result == 'auto') {
292                 $result = $this->client_timezone();
293             }
8fb4f0 294         }
TB 295         else if ($name == 'client_mimetypes') {
296             if ($result == null && $def == null)
297                 $result = 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,image/bmp,image/tiff,application/x-javascript,application/pdf,application/x-shockwave-flash';
298             if ($result && is_string($result))
299                 $result = explode(',', $result);
300         }
183717 301
be98df 302         $plugin = $rcube->plugins->exec_hook('config_get', array(
A 303             'name' => $name, 'default' => $def, 'result' => $result));
183717 304
be98df 305         return $plugin['result'];
2eb794 306     }
A 307
308
309     /**
310      * Setter for a config parameter
311      *
5c461b 312      * @param string $name  Parameter name
A 313      * @param mixed  $value Parameter value
2eb794 314      */
A 315     public function set($name, $value)
316     {
317         $this->prop[$name] = $value;
318     }
319
320
321     /**
322      * Override config options with the given values (eg. user prefs)
323      *
5c461b 324      * @param array $prefs Hash array with config props to merge over
2eb794 325      */
A 326     public function merge($prefs)
327     {
993cb6 328         $prefs = $this->fix_legacy_props($prefs);
2eb794 329         $this->prop = array_merge($this->prop, $prefs, $this->userprefs);
A 330     }
331
332
333     /**
334      * Merge the given prefs over the current config
335      * and make sure that they survive further merging.
336      *
5c461b 337      * @param array $prefs Hash array with user prefs
2eb794 338      */
A 339     public function set_user_prefs($prefs)
340     {
993cb6 341         $prefs = $this->fix_legacy_props($prefs);
AM 342
bfb7d6 343         // Honor the dont_override setting for any existing user preferences
A 344         $dont_override = $this->get('dont_override');
345         if (is_array($dont_override) && !empty($dont_override)) {
9f1652 346             foreach ($dont_override as $key) {
TB 347                 unset($prefs[$key]);
bfb7d6 348             }
A 349         }
350
a9cbba 351         // larry is the new default skin :-)
TB 352         if ($prefs['skin'] == 'default') {
aff970 353             $prefs['skin'] = self::DEFAULT_SKIN;
a9cbba 354         }
TB 355
2eb794 356         $this->userprefs = $prefs;
bfb7d6 357         $this->prop      = array_merge($this->prop, $prefs);
2eb794 358     }
A 359
360
361     /**
362      * Getter for all config options
363      *
993cb6 364      * @return array  Hash array containing all config properties
2eb794 365      */
A 366     public function all()
367     {
38f1f5 368         $rcube  = rcube::get_instance();
AM 369         $plugin = $rcube->plugins->exec_hook('config_get', array(
370             'name' => '*', 'result' => $this->prop));
371
372         return $plugin['result'];
2eb794 373     }
A 374
da7178 375     /**
65082b 376      * Special getter for user's timezone offset including DST
T 377      *
378      * @return float  Timezone offset (in hours)
5879c0 379      * @deprecated
da7178 380      */
T 381     public function get_timezone()
382     {
e86a21 383       if ($tz = $this->get('timezone')) {
A 384         try {
385           $tz = new DateTimeZone($tz);
386           return $tz->getOffset(new DateTime('now')) / 3600;
387         }
388         catch (Exception $e) {
389         }
5879c0 390       }
T 391
392       return 0;
da7178 393     }
2eb794 394
A 395     /**
396      * Return requested DES crypto key.
397      *
5c461b 398      * @param string $key Crypto key name
2eb794 399      * @return string Crypto key
A 400      */
401     public function get_crypto_key($key)
402     {
403         // Bomb out if the requested key does not exist
404         if (!array_key_exists($key, $this->prop)) {
0c2596 405             rcube::raise_error(array(
2eb794 406                 'code' => 500, 'type' => 'php',
A 407                 'file' => __FILE__, 'line' => __LINE__,
408                 'message' => "Request for unconfigured crypto key \"$key\""
409             ), true, true);
410         }
411
412         $key = $this->prop[$key];
413
414         // Bomb out if the configured key is not exactly 24 bytes long
415         if (strlen($key) != 24) {
0c2596 416             rcube::raise_error(array(
2eb794 417                 'code' => 500, 'type' => 'php',
413df0 418                 'file' => __FILE__, 'line' => __LINE__,
2eb794 419                 'message' => "Configured crypto key '$key' is not exactly 24 bytes long"
A 420             ), true, true);
421         }
422
423         return $key;
424     }
425
426
427     /**
428      * Try to autodetect operating system and find the correct line endings
429      *
430      * @return string The appropriate mail header delimiter
431      */
432     public function header_delimiter()
433     {
434         // use the configured delimiter for headers
086767 435         if (!empty($this->prop['mail_header_delimiter'])) {
A 436             $delim = $this->prop['mail_header_delimiter'];
437             if ($delim == "\n" || $delim == "\r\n")
438                 return $delim;
439             else
0c2596 440                 rcube::raise_error(array(
086767 441                     'code' => 500, 'type' => 'php',
413df0 442                     'file' => __FILE__, 'line' => __LINE__,
086767 443                     'message' => "Invalid mail_header_delimiter setting"
A 444                 ), true, false);
445         }
2eb794 446
A 447         $php_os = strtolower(substr(PHP_OS, 0, 3));
448
449         if ($php_os == 'win')
450             return "\r\n";
451
452         if ($php_os == 'mac')
453             return "\r\n";
454
455         return "\n";
456     }
457
458
459     /**
460      * Return the mail domain configured for the given host
461      *
5c461b 462      * @param string  $host   IMAP host
A 463      * @param boolean $encode If true, domain name will be converted to IDN ASCII
2eb794 464      * @return string Resolved SMTP host
A 465      */
e99991 466     public function mail_domain($host, $encode=true)
2eb794 467     {
A 468         $domain = $host;
469
470         if (is_array($this->prop['mail_domain'])) {
471             if (isset($this->prop['mail_domain'][$host]))
472                 $domain = $this->prop['mail_domain'][$host];
473         }
1aceb9 474         else if (!empty($this->prop['mail_domain'])) {
A 475             $domain = rcube_utils::parse_host($this->prop['mail_domain']);
476         }
bb8721 477
1aceb9 478         if ($encode) {
A 479             $domain = rcube_utils::idn_to_ascii($domain);
480         }
e99991 481
2eb794 482         return $domain;
A 483     }
bb8721 484
A 485
2eb794 486     /**
A 487      * Getter for error state
488      *
489      * @return mixed Error message on error, False if no errors
490      */
491     public function get_error()
492     {
493         return empty($this->errors) ? false : join("\n", $this->errors);
494     }
83a763 495
5879c0 496
T 497     /**
498      * Internal getter for client's (browser) timezone identifier
499      */
500     private function client_timezone()
501     {
7ebe06 502         // @TODO: remove this legacy timezone handling in the future
AM 503         $props = $this->fix_legacy_props(array('timezone' => $_SESSION['timezone']));
504
505         if (!empty($props['timezone'])) {
086b15 506             try {
7ebe06 507                 $tz = new DateTimeZone($props['timezone']);
086b15 508                 return $tz->getName();
TB 509             }
510             catch (Exception $e) { /* gracefully ignore */ }
511         }
512
513         // fallback to server's timezone
514         return date_default_timezone_get();
5879c0 515     }
T 516
f65890 517     /**
AM 518      * Convert legacy options into new ones
993cb6 519      *
AM 520      * @param array $props Hash array with config props
521      *
522      * @return array Converted config props
f65890 523      */
993cb6 524     private function fix_legacy_props($props)
f65890 525     {
AM 526         foreach ($this->legacy_props as $new => $old) {
993cb6 527             if (isset($props[$old])) {
AM 528                 if (!isset($props[$new])) {
529                     $props[$new] = $props[$old];
f65890 530                 }
993cb6 531                 unset($props[$old]);
f65890 532             }
AM 533         }
993cb6 534
7ebe06 535         // convert deprecated numeric timezone value
AM 536         if (isset($props['timezone']) && is_numeric($props['timezone'])) {
8eb085 537             if ($tz = self::timezone_name_from_abbr($props['timezone'])) {
7ebe06 538                 $props['timezone'] = $tz;
AM 539             }
540             else {
541                 unset($props['timezone']);
542             }
543         }
544
993cb6 545         return $props;
f65890 546     }
8eb085 547
AM 548     /**
549      * timezone_name_from_abbr() replacement. Converts timezone offset
550      * into timezone name abbreviation.
551      *
552      * @param float $offset Timezone offset (in hours)
553      *
554      * @return string Timezone abbreviation
555      */
556     static public function timezone_name_from_abbr($offset)
557     {
558         // List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780
559         if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) {
560             return $tz;
561         }
562
563         // try with more complete list (#1489261)
564         $timezones = array(
565             '-660' => "Pacific/Apia",
566             '-600' => "Pacific/Honolulu",
567             '-570' => "Pacific/Marquesas",
568             '-540' => "America/Anchorage",
569             '-480' => "America/Los_Angeles",
570             '-420' => "America/Denver",
571             '-360' => "America/Chicago",
572             '-300' => "America/New_York",
573             '-270' => "America/Caracas",
574             '-240' => "America/Halifax",
575             '-210' => "Canada/Newfoundland",
576             '-180' => "America/Sao_Paulo",
577              '-60' => "Atlantic/Azores",
578                '0' => "Europe/London",
579               '60' => "Europe/Paris",
580              '120' => "Europe/Helsinki",
581              '180' => "Europe/Moscow",
582              '210' => "Asia/Tehran",
583              '240' => "Asia/Dubai",
584              '270' => "Asia/Kabul",
585              '300' => "Asia/Karachi",
586              '330' => "Asia/Kolkata",
587              '345' => "Asia/Katmandu",
588              '360' => "Asia/Yekaterinburg",
589              '390' => "Asia/Rangoon",
590              '420' => "Asia/Krasnoyarsk",
591              '480' => "Asia/Shanghai",
592              '525' => "Australia/Eucla",
593              '540' => "Asia/Tokyo",
594              '570' => "Australia/Adelaide",
595              '600' => "Australia/Melbourne",
596              '630' => "Australia/Lord_Howe",
597              '660' => "Asia/Vladivostok",
598              '690' => "Pacific/Norfolk",
599              '720' => "Pacific/Auckland",
600              '765' => "Pacific/Chatham",
601              '780' => "Pacific/Enderbury",
602              '840' => "Pacific/Kiritimati",
603         );
604
605         return $timezones[(string) intval($offset * 60)];
606     }
197601 607 }