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