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