source: subversion/branches/devel-framework/roundcubemail/program/include/rcmail.php @ 5866

Last change on this file since 5866 was 5866, checked in by alec, 16 months ago
  • Unified output classes
  • Property svn:keywords set to Id
File size: 70.6 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcmail.php                                            |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
9 | Copyright (C) 2011, Kolab Systems AG                                  |
10 | Licensed under the GNU GPL                                            |
11 |                                                                       |
12 | PURPOSE:                                                              |
13 |   Application class providing core functions and holding              |
14 |   instances of all 'global' objects like db- and imap-connections     |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Application class of Roundcube Webmail
26 * implemented as singleton
27 *
28 * @package Core
29 */
30class rcmail
31{
32  /**
33   * Main tasks.
34   *
35   * @var array
36   */
37  static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
38
39  /**
40   * Singleton instace of rcmail
41   *
42   * @var rcmail
43   */
44  static private $instance;
45
46  /**
47   * Stores instance of rcube_config.
48   *
49   * @var rcube_config
50   */
51  public $config;
52
53  /**
54   * Stores rcube_user instance.
55   *
56   * @var rcube_user
57   */
58  public $user;
59
60  /**
61   * Instace of database class.
62   *
63   * @var rcube_mdb2
64   */
65  public $db;
66
67  /**
68   * Instace of Memcache class.
69   *
70   * @var rcube_mdb2
71   */
72  public $memcache;
73
74  /**
75   * Instace of rcube_session class.
76   *
77   * @var rcube_session
78   */
79  public $session;
80
81  /**
82   * Instance of rcube_smtp class.
83   *
84   * @var rcube_smtp
85   */
86  public $smtp;
87
88  /**
89   * Instance of rcube_storage class.
90   *
91   * @var rcube_storage
92   */
93  public $storage;
94
95  /**
96   * Instance of rcube_output class.
97   *
98   * @var rcube_output
99   */
100  public $output;
101
102  /**
103   * Instance of rcube_plugin_api.
104   *
105   * @var rcube_plugin_api
106   */
107  public $plugins;
108
109  /**
110   * Current task.
111   *
112   * @var string
113   */
114  public $task;
115
116  /**
117   * Current action.
118   *
119   * @var string
120   */
121  public $action = '';
122  public $comm_path = './';
123
124  private $texts;
125  private $address_books = array();
126  private $caches = array();
127  private $action_map = array();
128  private $shutdown_functions = array();
129
130
131  /**
132   * This implements the 'singleton' design pattern
133   *
134   * @return rcmail The one and only instance
135   */
136  static function get_instance()
137  {
138    if (!self::$instance) {
139      self::$instance = new rcmail();
140      self::$instance->startup();  // init AFTER object was linked with self::$instance
141    }
142
143    return self::$instance;
144  }
145
146
147  /**
148   * Private constructor
149   */
150  private function __construct()
151  {
152    // load configuration
153    $this->config = new rcube_config();
154
155    register_shutdown_function(array($this, 'shutdown'));
156  }
157
158
159  /**
160   * Initial startup function
161   * to register session, create database and imap connections
162   */
163  private function startup()
164  {
165    // initialize syslog
166    if ($this->config->get('log_driver') == 'syslog') {
167      $syslog_id = $this->config->get('syslog_id', 'roundcube');
168      $syslog_facility = $this->config->get('syslog_facility', LOG_USER);
169      openlog($syslog_id, LOG_ODELAY, $syslog_facility);
170    }
171
172    // connect to database
173    $this->get_dbh();
174
175    // start session
176    $this->session_init();
177
178    // create user object
179    $this->set_user(new rcube_user($_SESSION['user_id']));
180
181    // configure session (after user config merge!)
182    $this->session_configure();
183
184    // set task and action properties
185    $this->set_task(rcube_ui::get_input_value('_task', rcube_ui::INPUT_GPC));
186    $this->action = asciiwords(rcube_ui::get_input_value('_action', rcube_ui::INPUT_GPC));
187
188    // reset some session parameters when changing task
189    if ($this->task != 'utils') {
190      if ($this->session && $_SESSION['task'] != $this->task)
191        $this->session->remove('page');
192      // set current task to session
193      $_SESSION['task'] = $this->task;
194    }
195
196    // init output class
197    if (!empty($_REQUEST['_remote']))
198      $GLOBALS['OUTPUT'] = $this->json_init();
199    else
200      $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
201
202    // create plugin API and load plugins
203    $this->plugins = rcube_plugin_api::get_instance();
204
205    // init plugins
206    $this->plugins->init();
207  }
208
209
210  /**
211   * Setter for application task
212   *
213   * @param string Task to set
214   */
215  public function set_task($task)
216  {
217    $task = asciiwords($task);
218
219    if ($this->user && $this->user->ID)
220      $task = !$task ? 'mail' : $task;
221    else
222      $task = 'login';
223
224    $this->task = $task;
225    $this->comm_path = $this->url(array('task' => $this->task));
226
227    if ($this->output)
228      $this->output->set_env('task', $this->task);
229  }
230
231
232  /**
233   * Setter for system user object
234   *
235   * @param rcube_user Current user instance
236   */
237  public function set_user($user)
238  {
239    if (is_object($user)) {
240      $this->user = $user;
241
242      // overwrite config with user preferences
243      $this->config->set_user_prefs((array)$this->user->get_prefs());
244    }
245
246    $_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language']));
247
248    // set localization
249    setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8');
250
251    // workaround for http://bugs.php.net/bug.php?id=18556
252    if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ')))
253      setlocale(LC_CTYPE, 'en_US' . '.utf8');
254  }
255
256
257  /**
258   * Check the given string and return a valid language code
259   *
260   * @param string Language code
261   * @return string Valid language code
262   */
263  private function language_prop($lang)
264  {
265    static $rcube_languages, $rcube_language_aliases;
266
267    // user HTTP_ACCEPT_LANGUAGE if no language is specified
268    if (empty($lang) || $lang == 'auto') {
269       $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
270       $lang = str_replace('-', '_', $accept_langs[0]);
271     }
272
273    if (empty($rcube_languages)) {
274      @include(INSTALL_PATH . 'program/localization/index.inc');
275    }
276
277    // check if we have an alias for that language
278    if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
279      $lang = $rcube_language_aliases[$lang];
280    }
281    // try the first two chars
282    else if (!isset($rcube_languages[$lang])) {
283      $short = substr($lang, 0, 2);
284
285      // check if we have an alias for the short language code
286      if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
287        $lang = $rcube_language_aliases[$short];
288      }
289      // expand 'nn' to 'nn_NN'
290      else if (!isset($rcube_languages[$short])) {
291        $lang = $short.'_'.strtoupper($short);
292      }
293    }
294
295    if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
296      $lang = 'en_US';
297    }
298
299    return $lang;
300  }
301
302
303  /**
304   * Get the current database connection
305   *
306   * @return rcube_mdb2  Database connection object
307   */
308  public function get_dbh()
309  {
310    if (!$this->db) {
311      $config_all = $this->config->all();
312
313      $this->db = new rcube_mdb2($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
314      $this->db->sqlite_initials = INSTALL_PATH . 'SQL/sqlite.initial.sql';
315      $this->db->set_debug((bool)$config_all['sql_debug']);
316    }
317
318    return $this->db;
319  }
320
321
322  /**
323   * Get global handle for memcache access
324   *
325   * @return object Memcache
326   */
327  public function get_memcache()
328  {
329    if (!isset($this->memcache)) {
330      // no memcache support in PHP
331      if (!class_exists('Memcache')) {
332        $this->memcache = false;
333        return false;
334      }
335
336      $this->memcache = new Memcache;
337      $this->mc_available = 0;
338
339      // add alll configured hosts to pool
340      $pconnect = $this->config->get('memcache_pconnect', true);
341      foreach ($this->config->get('memcache_hosts', array()) as $host) {
342        list($host, $port) = explode(':', $host);
343        if (!$port) $port = 11211;
344        $this->mc_available += intval($this->memcache->addServer($host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure')));
345      }
346
347      // test connection and failover (will result in $this->mc_available == 0 on complete failure)
348      $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist
349
350      if (!$this->mc_available)
351        $this->memcache = false;
352    }
353
354    return $this->memcache;
355  }
356
357
358  /**
359   * Callback for memcache failure
360   */
361  public function memcache_failure($host, $port)
362  {
363    static $seen = array();
364
365    // only report once
366    if (!$seen["$host:$port"]++) {
367      $this->mc_available--;
368      self::raise_error(array('code' => 604, 'type' => 'db',
369        'line' => __LINE__, 'file' => __FILE__,
370        'message' => "Memcache failure on host $host:$port"),
371        true, false);
372    }
373  }
374
375
376  /**
377   * Initialize and get cache object
378   *
379   * @param string $name   Cache identifier
380   * @param string $type   Cache type ('db', 'apc' or 'memcache')
381   * @param int    $ttl    Expiration time for cache items in seconds
382   * @param bool   $packed Enables/disables data serialization
383   *
384   * @return rcube_cache Cache object
385   */
386  public function get_cache($name, $type='db', $ttl=0, $packed=true)
387  {
388    if (!isset($this->caches[$name])) {
389      $this->caches[$name] = new rcube_cache($type, $_SESSION['user_id'], $name, $ttl, $packed);
390    }
391
392    return $this->caches[$name];
393  }
394
395
396  /**
397   * Return instance of the internal address book class
398   *
399   * @param string  Address book identifier
400   * @param boolean True if the address book needs to be writeable
401   *
402   * @return rcube_contacts Address book object
403   */
404  public function get_address_book($id, $writeable = false)
405  {
406    $contacts    = null;
407    $ldap_config = (array)$this->config->get('ldap_public');
408    $abook_type  = strtolower($this->config->get('address_book_type'));
409
410    // 'sql' is the alias for '0' used by autocomplete
411    if ($id == 'sql')
412        $id = '0';
413
414    // use existing instance
415    if (isset($this->address_books[$id]) && is_object($this->address_books[$id])
416      && is_a($this->address_books[$id], 'rcube_addressbook')
417      && (!$writeable || !$this->address_books[$id]->readonly)
418    ) {
419      $contacts = $this->address_books[$id];
420    }
421    else if ($id && $ldap_config[$id]) {
422      $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['storage_host']));
423    }
424    else if ($id === '0') {
425      $contacts = new rcube_contacts($this->db, $this->get_user_id());
426    }
427    else {
428      $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
429
430      // plugin returned instance of a rcube_addressbook
431      if ($plugin['instance'] instanceof rcube_addressbook) {
432        $contacts = $plugin['instance'];
433      }
434      // get first source from the list
435      else if (!$id) {
436        $source = reset($this->get_address_sources($writeable));
437        if (!empty($source)) {
438          $contacts = $this->get_address_book($source['id']);
439          if ($contacts)
440            $id = $source['id'];
441        }
442      }
443    }
444
445    if (!$contacts) {
446      self::raise_error(array(
447        'code' => 700, 'type' => 'php',
448        'file' => __FILE__, 'line' => __LINE__,
449        'message' => "Addressbook source ($id) not found!"),
450        true, true);
451    }
452
453    // add to the 'books' array for shutdown function
454    $this->address_books[$id] = $contacts;
455
456    return $contacts;
457  }
458
459
460  /**
461   * Return address books list
462   *
463   * @param boolean True if the address book needs to be writeable
464   *
465   * @return array  Address books array
466   */
467  public function get_address_sources($writeable = false)
468  {
469    $abook_type = strtolower($this->config->get('address_book_type'));
470    $ldap_config = $this->config->get('ldap_public');
471    $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
472    $list = array();
473
474    // We are using the DB address book
475    if ($abook_type != 'ldap') {
476      if (!isset($this->address_books['0']))
477        $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
478      $list['0'] = array(
479        'id'       => '0',
480        'name'     => $this->gettext('personaladrbook'),
481        'groups'   => $this->address_books['0']->groups,
482        'readonly' => $this->address_books['0']->readonly,
483        'autocomplete' => in_array('sql', $autocomplete),
484        'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
485      );
486    }
487
488    if ($ldap_config) {
489      $ldap_config = (array) $ldap_config;
490      foreach ($ldap_config as $id => $prop)
491        $list[$id] = array(
492          'id'       => $id,
493          'name'     => $prop['name'],
494          'groups'   => is_array($prop['groups']),
495          'readonly' => !$prop['writable'],
496          'hidden'   => $prop['hidden'],
497          'autocomplete' => in_array($id, $autocomplete)
498        );
499    }
500
501    $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
502    $list = $plugin['sources'];
503
504    foreach ($list as $idx => $item) {
505      // register source for shutdown function
506      if (!is_object($this->address_books[$item['id']]))
507        $this->address_books[$item['id']] = $item;
508      // remove from list if not writeable as requested
509      if ($writeable && $item['readonly'])
510          unset($list[$idx]);
511    }
512
513    return $list;
514  }
515
516
517  /**
518   * Init output object for GUI and add common scripts.
519   * This will instantiate a rcmail_template object and set
520   * environment vars according to the current session and configuration
521   *
522   * @param boolean True if this request is loaded in a (i)frame
523   * @return rcube_output_html Reference to HTML output object
524   */
525  public function load_gui($framed = false)
526  {
527    // init output page
528    if (!($this->output instanceof rcube_output_html))
529      $this->output = new rcube_output_html($this->task, $framed);
530
531    // set keep-alive/check-recent interval
532    if ($this->session && ($keep_alive = $this->session->get_keep_alive())) {
533      $this->output->set_env('keep_alive', $keep_alive);
534    }
535
536    if ($framed) {
537      $this->comm_path .= '&_framed=1';
538      $this->output->set_env('framed', true);
539    }
540
541    $this->output->set_env('task', $this->task);
542    $this->output->set_env('action', $this->action);
543    $this->output->set_env('comm_path', $this->comm_path);
544    $this->output->set_charset(RCMAIL_CHARSET);
545
546    // add some basic labels to client
547    $this->output->add_label('loading', 'servererror');
548
549    return $this->output;
550  }
551
552
553  /**
554   * Create an output object for JSON responses
555   *
556   * @return rcube_output_json Reference to JSON output object
557   */
558  public function json_init()
559  {
560    if (!($this->output instanceof rcube_output_json))
561      $this->output = new rcube_output_json($this->task);
562
563    return $this->output;
564  }
565
566
567  /**
568   * Create SMTP object and connect to server
569   *
570   * @param boolean True if connection should be established
571   */
572  public function smtp_init($connect = false)
573  {
574    $this->smtp = new rcube_smtp();
575
576    if ($connect)
577      $this->smtp->connect();
578  }
579
580
581  /**
582   * Initialize and get storage object
583   *
584   * @return rcube_storage Storage object
585   */
586  public function get_storage()
587  {
588    // already initialized
589    if (!is_object($this->storage)) {
590      $this->storage_init();
591    }
592
593    return $this->storage;
594  }
595
596
597  /**
598   * Connect to the IMAP server with stored session data.
599   *
600   * @return bool True on success, False on error
601   * @deprecated
602   */
603  public function imap_connect()
604  {
605    return $this->storage_connect();
606  }
607
608
609  /**
610   * Initialize IMAP object.
611   *
612   * @deprecated
613   */
614  public function imap_init()
615  {
616    $this->storage_init();
617  }
618
619
620  /**
621   * Initialize storage object
622   */
623  public function storage_init()
624  {
625    // already initialized
626    if (is_object($this->storage)) {
627      return;
628    }
629
630    $driver = $this->config->get('storage_driver', 'imap');
631    $driver_class = "rcube_{$driver}";
632
633    if (!class_exists($driver_class)) {
634      self::raise_error(array(
635        'code' => 700, 'type' => 'php',
636        'file' => __FILE__, 'line' => __LINE__,
637        'message' => "Storage driver class ($driver) not found!"),
638        true, true);
639    }
640
641    // Initialize storage object
642    $this->storage = new $driver_class;
643
644    // for backward compat. (deprecated, will be removed)
645    $this->imap = $this->storage;
646
647    // enable caching of mail data
648    $storage_cache  = $this->config->get("{$driver}_cache");
649    $messages_cache = $this->config->get('messages_cache');
650    // for backward compatybility
651    if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
652        $storage_cache  = 'db';
653        $messages_cache = true;
654    }
655
656    if ($storage_cache)
657        $this->storage->set_caching($storage_cache);
658    if ($messages_cache)
659        $this->storage->set_messages_caching(true);
660
661    // set pagesize from config
662    $pagesize = $this->config->get('mail_pagesize');
663    if (!$pagesize) {
664        $pagesize = $this->config->get('pagesize', 50);
665    }
666    $this->storage->set_pagesize($pagesize);
667
668    // set class options
669    $options = array(
670      'auth_type'   => $this->config->get("{$driver}_auth_type", 'check'),
671      'auth_cid'    => $this->config->get("{$driver}_auth_cid"),
672      'auth_pw'     => $this->config->get("{$driver}_auth_pw"),
673      'debug'       => (bool) $this->config->get("{$driver}_debug"),
674      'force_caps'  => (bool) $this->config->get("{$driver}_force_caps"),
675      'timeout'     => (int) $this->config->get("{$driver}_timeout"),
676      'skip_deleted' => (bool) $this->config->get('skip_deleted'),
677      'driver'      => $driver,
678    );
679
680    if (!empty($_SESSION['storage_host'])) {
681      $options['host']     = $_SESSION['storage_host'];
682      $options['user']     = $_SESSION['username'];
683      $options['port']     = $_SESSION['storage_port'];
684      $options['ssl']      = $_SESSION['storage_ssl'];
685      $options['password'] = $this->decrypt($_SESSION['password']);
686    }
687
688    $options = $this->plugins->exec_hook("storage_init", $options);
689
690    // for backward compat. (deprecated, to be removed)
691    $options = $this->plugins->exec_hook("imap_init", $options);
692
693    $this->storage->set_options($options);
694    $this->set_storage_prop();
695  }
696
697
698  /**
699   * Connect to the mail storage server with stored session data
700   *
701   * @return bool True on success, False on error
702   */
703  public function storage_connect()
704  {
705    $storage = $this->get_storage();
706
707    if ($_SESSION['storage_host'] && !$storage->is_connected()) {
708      $host = $_SESSION['storage_host'];
709      $user = $_SESSION['username'];
710      $port = $_SESSION['storage_port'];
711      $ssl  = $_SESSION['storage_ssl'];
712      $pass = $this->decrypt($_SESSION['password']);
713
714      if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
715        if ($this->output)
716          $this->output->show_message($storage->get_error_code() == -1 ? 'storageerror' : 'sessionerror', 'error');
717      }
718      else {
719        $this->set_storage_prop();
720        return $storage->is_connected();
721      }
722    }
723
724    return false;
725  }
726
727
728  /**
729   * Create session object and start the session.
730   */
731  public function session_init()
732  {
733    // session started (Installer?)
734    if (session_id())
735      return;
736
737    $sess_name   = $this->config->get('session_name');
738    $sess_domain = $this->config->get('session_domain');
739    $lifetime    = $this->config->get('session_lifetime', 0) * 60;
740
741    // set session domain
742    if ($sess_domain) {
743      ini_set('session.cookie_domain', $sess_domain);
744    }
745    // set session garbage collecting time according to session_lifetime
746    if ($lifetime) {
747      ini_set('session.gc_maxlifetime', $lifetime * 2);
748    }
749
750    ini_set('session.cookie_secure', rcube_ui::https_check());
751    ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
752    ini_set('session.use_cookies', 1);
753    ini_set('session.use_only_cookies', 1);
754    ini_set('session.serialize_handler', 'php');
755
756    // use database for storing session data
757    $this->session = new rcube_session($this->get_dbh(), $this->config);
758
759    $this->session->register_gc_handler(array($this, 'temp_gc'));
760    if ($this->config->get('enable_caching'))
761      $this->session->register_gc_handler(array($this, 'cache_gc'));
762
763    // start PHP session (if not in CLI mode)
764    if ($_SERVER['REMOTE_ADDR'])
765      session_start();
766
767    // set initial session vars
768    if (!$_SESSION['user_id'])
769      $_SESSION['temp'] = true;
770  }
771
772
773  /**
774   * Configure session object internals
775   */
776  public function session_configure()
777  {
778    if (!$this->session)
779      return;
780
781    $lifetime = $this->config->get('session_lifetime', 0) * 60;
782
783    // set keep-alive/check-recent interval
784    if ($keep_alive = $this->config->get('keep_alive')) {
785      // be sure that it's less than session lifetime
786      if ($lifetime)
787        $keep_alive = min($keep_alive, $lifetime - 30);
788      $keep_alive = max(60, $keep_alive);
789      $this->session->set_keep_alive($keep_alive);
790    }
791
792    $this->session->set_secret($this->config->get('des_key') . $_SERVER['HTTP_USER_AGENT']);
793    $this->session->set_ip_check($this->config->get('ip_check'));
794  }
795
796
797  /**
798   * Perfom login to the mail server and to the webmail service.
799   * This will also create a new user entry if auto_create_user is configured.
800   *
801   * @param string Mail storage (IMAP) user name
802   * @param string Mail storage (IMAP) password
803   * @param string Mail storage (IMAP) host
804   *
805   * @return boolean True on success, False on failure
806   */
807  function login($username, $pass, $host=NULL)
808  {
809    if (empty($username)) {
810      return false;
811    }
812
813    $config = $this->config->all();
814
815    if (!$host)
816      $host = $config['default_host'];
817
818    // Validate that selected host is in the list of configured hosts
819    if (is_array($config['default_host'])) {
820      $allowed = false;
821      foreach ($config['default_host'] as $key => $host_allowed) {
822        if (!is_numeric($key))
823          $host_allowed = $key;
824        if ($host == $host_allowed) {
825          $allowed = true;
826          break;
827        }
828      }
829      if (!$allowed)
830        return false;
831      }
832    else if (!empty($config['default_host']) && $host != self::parse_host($config['default_host']))
833      return false;
834
835    // parse $host URL
836    $a_host = parse_url($host);
837    if ($a_host['host']) {
838      $host = $a_host['host'];
839      $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
840      if (!empty($a_host['port']))
841        $port = $a_host['port'];
842      else if ($ssl && $ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
843        $port = 993;
844    }
845
846    if (!$port) {
847        $port = $config['default_port'];
848    }
849
850    /* Modify username with domain if required
851       Inspired by Marco <P0L0_notspam_binware.org>
852    */
853    // Check if we need to add domain
854    if (!empty($config['username_domain']) && strpos($username, '@') === false) {
855      if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
856        $username .= '@'.self::parse_host($config['username_domain'][$host], $host);
857      else if (is_string($config['username_domain']))
858        $username .= '@'.self::parse_host($config['username_domain'], $host);
859    }
860
861    // Convert username to lowercase. If storage backend
862    // is case-insensitive we need to store always the same username (#1487113)
863    if ($config['login_lc']) {
864      $username = mb_strtolower($username);
865    }
866
867    // try to resolve email address from virtuser table
868    if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
869      $username = $virtuser;
870    }
871
872    // Here we need IDNA ASCII
873    // Only rcube_contacts class is using domain names in Unicode
874    $host = rcube_idn_to_ascii($host);
875    if (strpos($username, '@')) {
876      // lowercase domain name
877      list($local, $domain) = explode('@', $username);
878      $username = $local . '@' . mb_strtolower($domain);
879      $username = rcube_idn_to_ascii($username);
880    }
881
882    // user already registered -> overwrite username
883    if ($user = rcube_user::query($username, $host))
884      $username = $user->data['username'];
885
886    if (!$this->storage)
887      $this->storage_init();
888
889    // try to log in
890    if (!($login = $this->storage->connect($host, $username, $pass, $port, $ssl))) {
891      // try with lowercase
892      $username_lc = mb_strtolower($username);
893      if ($username_lc != $username) {
894        // try to find user record again -> overwrite username
895        if (!$user && ($user = rcube_user::query($username_lc, $host)))
896          $username_lc = $user->data['username'];
897
898        if ($login = $this->storage->connect($host, $username_lc, $pass, $port, $ssl))
899          $username = $username_lc;
900      }
901    }
902
903    // exit if login failed
904    if (!$login) {
905      return false;
906    }
907
908    // user already registered -> update user's record
909    if (is_object($user)) {
910      // update last login timestamp
911      $user->touch();
912    }
913    // create new system user
914    else if ($config['auto_create_user']) {
915      if ($created = rcube_user::create($username, $host)) {
916        $user = $created;
917      }
918      else {
919        self::raise_error(array(
920          'code' => 620, 'type' => 'php',
921          'file' => __FILE__, 'line' => __LINE__,
922          'message' => "Failed to create a user record. Maybe aborted by a plugin?"
923          ), true, false);
924      }
925    }
926    else {
927      self::raise_error(array(
928        'code' => 621, 'type' => 'php',
929        'file' => __FILE__, 'line' => __LINE__,
930        'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
931        ), true, false);
932    }
933
934    // login succeeded
935    if (is_object($user) && $user->ID) {
936      // Configure environment
937      $this->set_user($user);
938      $this->set_storage_prop();
939      $this->session_configure();
940
941      // fix some old settings according to namespace prefix
942      $this->fix_namespace_settings($user);
943
944      // create default folders on first login
945      if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
946        $this->storage->create_default_folders();
947      }
948
949      // set session vars
950      $_SESSION['user_id']      = $user->ID;
951      $_SESSION['username']     = $user->data['username'];
952      $_SESSION['storage_host'] = $host;
953      $_SESSION['storage_port'] = $port;
954      $_SESSION['storage_ssl']  = $ssl;
955      $_SESSION['password']     = $this->encrypt($pass);
956      $_SESSION['login_time']   = mktime();
957
958      if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
959        $_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
960      if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_')
961        $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']);
962
963      // force reloading complete list of subscribed mailboxes
964      $this->storage->clear_cache('mailboxes', true);
965
966      return true;
967    }
968
969    return false;
970  }
971
972
973  /**
974   * Set storage parameters.
975   * This must be done AFTER connecting to the server!
976   */
977  private function set_storage_prop()
978  {
979    $storage = $this->get_storage();
980
981    $storage->set_charset($this->config->get('default_charset', RCMAIL_CHARSET));
982
983    if ($default_folders = $this->config->get('default_folders')) {
984      $storage->set_default_folders($default_folders);
985    }
986    if (isset($_SESSION['mbox'])) {
987      $storage->set_folder($_SESSION['mbox']);
988    }
989    if (isset($_SESSION['page'])) {
990      $storage->set_page($_SESSION['page']);
991    }
992  }
993
994
995  /**
996   * Auto-select IMAP host based on the posted login information
997   *
998   * @return string Selected IMAP host
999   */
1000  public function autoselect_host()
1001  {
1002    $default_host = $this->config->get('default_host');
1003    $host = null;
1004
1005    if (is_array($default_host)) {
1006      $post_host = rcube_ui::get_input_value('_host', rcube_ui::INPUT_POST);
1007
1008      // direct match in default_host array
1009      if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
1010        $host = $post_host;
1011      }
1012
1013      // try to select host by mail domain
1014      list($user, $domain) = explode('@', rcube_ui::get_input_value('_user', rcube_ui::INPUT_POST));
1015      if (!empty($domain)) {
1016        foreach ($default_host as $storage_host => $mail_domains) {
1017          if (is_array($mail_domains) && in_array($domain, $mail_domains)) {
1018            $host = $storage_host;
1019            break;
1020          }
1021        }
1022      }
1023
1024      // take the first entry if $host is still an array
1025      if (empty($host)) {
1026        $host = array_shift($default_host);
1027      }
1028    }
1029    else if (empty($default_host)) {
1030      $host = rcube_ui::get_input_value('_host', rcube_ui::INPUT_POST);
1031    }
1032    else
1033      $host = self::parse_host($default_host);
1034
1035    return $host;
1036  }
1037
1038
1039  /**
1040   * Get localized text in the desired language
1041   *
1042   * @param mixed   $attrib  Named parameters array or label name
1043   * @param string  $domain  Label domain (plugin) name
1044   *
1045   * @return string Localized text
1046   */
1047  public function gettext($attrib, $domain=null)
1048  {
1049    // load localization files if not done yet
1050    if (empty($this->texts))
1051      $this->load_language();
1052
1053    // extract attributes
1054    if (is_string($attrib))
1055      $attrib = array('name' => $attrib);
1056
1057    $name = $attrib['name'] ? $attrib['name'] : '';
1058
1059    // attrib contain text values: use them from now
1060    if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us']))
1061        $this->texts[$name] = $setval;
1062
1063    // check for text with domain
1064    if ($domain && ($text = $this->texts[$domain.'.'.$name]))
1065      ;
1066    // text does not exist
1067    else if (!($text = $this->texts[$name])) {
1068      return "[$name]";
1069    }
1070
1071    // replace vars in text
1072    if (is_array($attrib['vars'])) {
1073      foreach ($attrib['vars'] as $var_key => $var_value)
1074        $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
1075    }
1076
1077    // format output
1078    if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst'])
1079      return ucfirst($text);
1080    else if ($attrib['uppercase'])
1081      return mb_strtoupper($text);
1082    else if ($attrib['lowercase'])
1083      return mb_strtolower($text);
1084
1085    return strtr($text, array('\n' => "\n"));
1086  }
1087
1088
1089  /**
1090   * Check if the given text label exists
1091   *
1092   * @param string  $name       Label name
1093   * @param string  $domain     Label domain (plugin) name or '*' for all domains
1094   * @param string  $ref_domain Sets domain name if label is found
1095   *
1096   * @return boolean True if text exists (either in the current language or in en_US)
1097   */
1098  public function text_exists($name, $domain = null, &$ref_domain = null)
1099  {
1100    // load localization files if not done yet
1101    if (empty($this->texts))
1102      $this->load_language();
1103
1104    if (isset($this->texts[$name])) {
1105        $ref_domain = '';
1106        return true;
1107    }
1108
1109    // any of loaded domains (plugins)
1110    if ($domain == '*') {
1111      foreach ($this->plugins->loaded_plugins() as $domain)
1112        if (isset($this->texts[$domain.'.'.$name])) {
1113          $ref_domain = $domain;
1114          return true;
1115        }
1116    }
1117    // specified domain
1118    else if ($domain) {
1119      $ref_domain = $domain;
1120      return isset($this->texts[$domain.'.'.$name]);
1121    }
1122
1123    return false;
1124  }
1125
1126  /**
1127   * Load a localization package
1128   *
1129   * @param string Language ID
1130   */
1131  public function load_language($lang = null, $add = array())
1132  {
1133    $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
1134
1135    // load localized texts
1136    if (empty($this->texts) || $lang != $_SESSION['language']) {
1137      $this->texts = array();
1138
1139      // handle empty lines after closing PHP tag in localization files
1140      ob_start();
1141
1142      // get english labels (these should be complete)
1143      @include(INSTALL_PATH . 'program/localization/en_US/labels.inc');
1144      @include(INSTALL_PATH . 'program/localization/en_US/messages.inc');
1145
1146      if (is_array($labels))
1147        $this->texts = $labels;
1148      if (is_array($messages))
1149        $this->texts = array_merge($this->texts, $messages);
1150
1151      // include user language files
1152      if ($lang != 'en' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
1153        include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc');
1154        include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc');
1155
1156        if (is_array($labels))
1157          $this->texts = array_merge($this->texts, $labels);
1158        if (is_array($messages))
1159          $this->texts = array_merge($this->texts, $messages);
1160      }
1161
1162      ob_end_clean();
1163
1164      $_SESSION['language'] = $lang;
1165    }
1166
1167    // append additional texts (from plugin)
1168    if (is_array($add) && !empty($add))
1169      $this->texts += $add;
1170  }
1171
1172
1173  /**
1174   * Read directory program/localization and return a list of available languages
1175   *
1176   * @return array List of available localizations
1177   */
1178  public function list_languages()
1179  {
1180    static $sa_languages = array();
1181
1182    if (!sizeof($sa_languages)) {
1183      @include(INSTALL_PATH . 'program/localization/index.inc');
1184
1185      if ($dh = @opendir(INSTALL_PATH . 'program/localization')) {
1186        while (($name = readdir($dh)) !== false) {
1187          if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name))
1188            continue;
1189
1190          if ($label = $rcube_languages[$name])
1191            $sa_languages[$name] = $label;
1192        }
1193        closedir($dh);
1194      }
1195    }
1196
1197    return $sa_languages;
1198  }
1199
1200
1201  /**
1202   * Destroy session data and remove cookie
1203   */
1204  public function kill_session()
1205  {
1206    $this->plugins->exec_hook('session_destroy');
1207
1208    $this->session->kill();
1209    $_SESSION = array('language' => $this->user->language, 'temp' => true);
1210    $this->user->reset();
1211  }
1212
1213
1214  /**
1215   * Do server side actions on logout
1216   */
1217  public function logout_actions()
1218  {
1219    $config = $this->config->all();
1220
1221    // on logout action we're not connected to imap server
1222    if (($config['logout_purge'] && !empty($config['trash_mbox'])) || $config['logout_expunge']) {
1223      if (!$this->session->check_auth())
1224        return;
1225
1226      $this->storage_connect();
1227    }
1228
1229    if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
1230      $this->storage->clear_folder($config['trash_mbox']);
1231    }
1232
1233    if ($config['logout_expunge']) {
1234      $this->storage->expunge_folder('INBOX');
1235    }
1236
1237    // Try to save unsaved user preferences
1238    if (!empty($_SESSION['preferences'])) {
1239      $this->user->save_prefs(unserialize($_SESSION['preferences']));
1240    }
1241  }
1242
1243
1244  /**
1245   * Function to be executed in script shutdown
1246   * Registered with register_shutdown_function()
1247   */
1248  public function shutdown()
1249  {
1250    foreach ($this->shutdown_functions as $function)
1251      call_user_func($function);
1252
1253    if (is_object($this->smtp))
1254      $this->smtp->disconnect();
1255
1256    foreach ($this->address_books as $book) {
1257      if (is_object($book) && is_a($book, 'rcube_addressbook'))
1258        $book->close();
1259    }
1260
1261    foreach ($this->caches as $cache) {
1262        if (is_object($cache))
1263            $cache->close();
1264    }
1265
1266    if (is_object($this->storage))
1267      $this->storage->close();
1268
1269    // before closing the database connection, write session data
1270    if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
1271      session_write_close();
1272    }
1273
1274    // write performance stats to logs/console
1275    if ($this->config->get('devel_mode')) {
1276      if (function_exists('memory_get_usage'))
1277        $mem = rcube_ui::show_bytes(memory_get_usage());
1278      if (function_exists('memory_get_peak_usage'))
1279        $mem .= '/'.rcube_ui::show_bytes(memory_get_peak_usage());
1280
1281      $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
1282      if (defined('RCMAIL_START'))
1283        self::print_timer(RCMAIL_START, $log);
1284      else
1285        self::console($log);
1286    }
1287  }
1288
1289
1290  /**
1291   * Registers shutdown function to be executed on shutdown.
1292   * The functions will be executed before destroying any
1293   * objects like smtp, imap, session, etc.
1294   *
1295   * @param callback Function callback
1296   */
1297  public function add_shutdown_function($function)
1298  {
1299    $this->shutdown_functions[] = $function;
1300  }
1301
1302
1303  /**
1304   * Generate a unique token to be used in a form request
1305   *
1306   * @return string The request token
1307   */
1308  public function get_request_token()
1309  {
1310    $sess_id = $_COOKIE[ini_get('session.name')];
1311    if (!$sess_id) $sess_id = session_id();
1312    $plugin = $this->plugins->exec_hook('request_token', array('value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
1313    return $plugin['value'];
1314  }
1315
1316
1317  /**
1318   * Check if the current request contains a valid token
1319   *
1320   * @param int Request method
1321   * @return boolean True if request token is valid false if not
1322   */
1323  public function check_request($mode = rcube_ui::INPUT_POST)
1324  {
1325    $token = rcube_ui::get_input_value('_token', $mode);
1326    $sess_id = $_COOKIE[ini_get('session.name')];
1327    return !empty($sess_id) && $token == $this->get_request_token();
1328  }
1329
1330
1331  /**
1332   * Create unique authorization hash
1333   *
1334   * @param string Session ID
1335   * @param int Timestamp
1336   * @return string The generated auth hash
1337   */
1338  private function get_auth_hash($sess_id, $ts)
1339  {
1340    $auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
1341      $sess_id,
1342      $ts,
1343      $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
1344      $_SERVER['HTTP_USER_AGENT']);
1345
1346    if (function_exists('sha1'))
1347      return sha1($auth_string);
1348    else
1349      return md5($auth_string);
1350  }
1351
1352
1353  /**
1354   * Encrypt using 3DES
1355   *
1356   * @param string $clear clear text input
1357   * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
1358   * @param boolean $base64 whether or not to base64_encode() the result before returning
1359   *
1360   * @return string encrypted text
1361   */
1362  public function encrypt($clear, $key = 'des_key', $base64 = true)
1363  {
1364    if (!$clear)
1365      return '';
1366    /*-
1367     * Add a single canary byte to the end of the clear text, which
1368     * will help find out how much of padding will need to be removed
1369     * upon decryption; see http://php.net/mcrypt_generic#68082
1370     */
1371    $clear = pack("a*H2", $clear, "80");
1372
1373    if (function_exists('mcrypt_module_open') &&
1374        ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
1375    {
1376      $iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
1377      mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
1378      $cipher = $iv . mcrypt_generic($td, $clear);
1379      mcrypt_generic_deinit($td);
1380      mcrypt_module_close($td);
1381    }
1382    else {
1383      @include_once 'des.inc';
1384
1385      if (function_exists('des')) {
1386        $des_iv_size = 8;
1387        $iv = $this->create_iv($des_iv_size);
1388        $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
1389      }
1390      else {
1391        self::raise_error(array(
1392          'code' => 500, 'type' => 'php',
1393          'file' => __FILE__, 'line' => __LINE__,
1394          'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
1395        ), true, true);
1396      }
1397    }
1398
1399    return $base64 ? base64_encode($cipher) : $cipher;
1400  }
1401
1402  /**
1403   * Decrypt 3DES-encrypted string
1404   *
1405   * @param string $cipher encrypted text
1406   * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
1407   * @param boolean $base64 whether or not input is base64-encoded
1408   *
1409   * @return string decrypted text
1410   */
1411  public function decrypt($cipher, $key = 'des_key', $base64 = true)
1412  {
1413    if (!$cipher)
1414      return '';
1415
1416    $cipher = $base64 ? base64_decode($cipher) : $cipher;
1417
1418    if (function_exists('mcrypt_module_open') &&
1419        ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
1420    {
1421      $iv_size = mcrypt_enc_get_iv_size($td);
1422      $iv = substr($cipher, 0, $iv_size);
1423
1424      // session corruption? (#1485970)
1425      if (strlen($iv) < $iv_size)
1426        return '';
1427
1428      $cipher = substr($cipher, $iv_size);
1429      mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
1430      $clear = mdecrypt_generic($td, $cipher);
1431      mcrypt_generic_deinit($td);
1432      mcrypt_module_close($td);
1433    }
1434    else {
1435      @include_once 'des.inc';
1436
1437      if (function_exists('des')) {
1438        $des_iv_size = 8;
1439        $iv = substr($cipher, 0, $des_iv_size);
1440        $cipher = substr($cipher, $des_iv_size);
1441        $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
1442      }
1443      else {
1444        self::raise_error(array(
1445          'code' => 500, 'type' => 'php',
1446          'file' => __FILE__, 'line' => __LINE__,
1447          'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
1448        ), true, true);
1449      }
1450    }
1451
1452    /*-
1453     * Trim PHP's padding and the canary byte; see note in
1454     * rcmail::encrypt() and http://php.net/mcrypt_generic#68082
1455     */
1456    $clear = substr(rtrim($clear, "\0"), 0, -1);
1457
1458    return $clear;
1459  }
1460
1461  /**
1462   * Generates encryption initialization vector (IV)
1463   *
1464   * @param int Vector size
1465   * @return string Vector string
1466   */
1467  private function create_iv($size)
1468  {
1469    // mcrypt_create_iv() can be slow when system lacks entrophy
1470    // we'll generate IV vector manually
1471    $iv = '';
1472    for ($i = 0; $i < $size; $i++)
1473        $iv .= chr(mt_rand(0, 255));
1474    return $iv;
1475  }
1476
1477  /**
1478   * Build a valid URL to this instance of Roundcube
1479   *
1480   * @param mixed Either a string with the action or url parameters as key-value pairs
1481   * @return string Valid application URL
1482   */
1483  public function url($p)
1484  {
1485    if (!is_array($p))
1486      $p = array('_action' => @func_get_arg(0));
1487
1488    $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
1489    $p['_task'] = $task;
1490    unset($p['task']);
1491
1492    $url = './';
1493    $delm = '?';
1494    foreach (array_reverse($p) as $key => $val) {
1495      if ($val !== '') {
1496        $par = $key[0] == '_' ? $key : '_'.$key;
1497        $url .= $delm.urlencode($par).'='.urlencode($val);
1498        $delm = '&';
1499      }
1500    }
1501    return $url;
1502  }
1503
1504
1505  /**
1506   * Use imagemagick or GD lib to read image properties
1507   *
1508   * @param string Absolute file path
1509   * @return mixed Hash array with image props like type, width, height or False on error
1510   */
1511  public static function imageprops($filepath)
1512  {
1513    $rcmail = rcmail::get_instance();
1514    if ($cmd = $rcmail->config->get('im_identify_path', false)) {
1515      list(, $type, $size) = explode(' ', strtolower(rcmail::exec($cmd. ' 2>/dev/null {in}', array('in' => $filepath))));
1516      if ($size)
1517        list($width, $height) = explode('x', $size);
1518    }
1519    else if (function_exists('getimagesize')) {
1520      $imsize = @getimagesize($filepath);
1521      $width = $imsize[0];
1522      $height = $imsize[1];
1523      $type = preg_replace('!image/!', '', $imsize['mime']);
1524    }
1525
1526    return $type ? array('type' => $type, 'width' => $width, 'height' => $height) : false;
1527  }
1528
1529
1530  /**
1531   * Convert an image to a given size and type using imagemagick (ensures input is an image)
1532   *
1533   * @param $p['in']  Input filename (mandatory)
1534   * @param $p['out'] Output filename (mandatory)
1535   * @param $p['size']  Width x height of resulting image, e.g. "160x60"
1536   * @param $p['type']  Output file type, e.g. "jpg"
1537   * @param $p['-opts'] Custom command line options to ImageMagick convert
1538   * @return Success of convert as true/false
1539   */
1540  public static function imageconvert($p)
1541  {
1542    $result = false;
1543    $rcmail = rcmail::get_instance();
1544    $convert  = $rcmail->config->get('im_convert_path', false);
1545    $identify = $rcmail->config->get('im_identify_path', false);
1546
1547    // imagemagick is required for this
1548    if (!$convert)
1549        return false;
1550
1551    if (!(($imagetype = @exif_imagetype($p['in'])) && ($type = image_type_to_extension($imagetype, false))))
1552      list(, $type) = explode(' ', strtolower(rcmail::exec($identify . ' 2>/dev/null {in}', $p))); # for things like eps
1553
1554    $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
1555    $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75);
1556    $p['-opts'] = array('-resize' => $p['size'].'>') + (array)$p['-opts'];
1557
1558    if (in_array($type, explode(',', $p['types']))) # Valid type?
1559      $result = rcmail::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace RGB -quality {quality} {-opts} {in} {type}:{out}', $p) === "";
1560
1561    return $result;
1562  }
1563
1564
1565  /**
1566   * Construct shell command, execute it and return output as string.
1567   * Keywords {keyword} are replaced with arguments
1568   *
1569   * @param $cmd Format string with {keywords} to be replaced
1570   * @param $values (zero, one or more arrays can be passed)
1571   * @return output of command. shell errors not detectable
1572   */
1573  public static function exec(/* $cmd, $values1 = array(), ... */)
1574  {
1575    $args = func_get_args();
1576    $cmd = array_shift($args);
1577    $values = $replacements = array();
1578
1579    // merge values into one array
1580    foreach ($args as $arg)
1581      $values += (array)$arg;
1582
1583    preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
1584    foreach ($matches as $tags) {
1585      list(, $tag, $option, $key) = $tags;
1586      $parts = array();
1587
1588      if ($option) {
1589        foreach ((array)$values["-$key"] as $key => $value) {
1590          if ($value === true || $value === false || $value === null)
1591            $parts[] = $value ? $key : "";
1592          else foreach ((array)$value as $val)
1593            $parts[] = "$key " . escapeshellarg($val);
1594        }
1595      }
1596      else {
1597        foreach ((array)$values[$key] as $value)
1598          $parts[] = escapeshellarg($value);
1599      }
1600
1601      $replacements[$tag] = join(" ", $parts);
1602    }
1603
1604    // use strtr behaviour of going through source string once
1605    $cmd = strtr($cmd, $replacements);
1606
1607    return (string)shell_exec($cmd);
1608  }
1609
1610
1611  /**
1612   * Helper method to set a cookie with the current path and host settings
1613   *
1614   * @param string Cookie name
1615   * @param string Cookie value
1616   * @param string Expiration time
1617   */
1618  public static function setcookie($name, $value, $exp = 0)
1619  {
1620    if (headers_sent())
1621      return;
1622
1623    $cookie = session_get_cookie_params();
1624
1625    setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'],
1626      rcube_ui::https_check(), true);
1627  }
1628
1629  /**
1630   * Registers action aliases for current task
1631   *
1632   * @param array $map Alias-to-filename hash array
1633   */
1634  public function register_action_map($map)
1635  {
1636    if (is_array($map)) {
1637      foreach ($map as $idx => $val) {
1638        $this->action_map[$idx] = $val;
1639      }
1640    }
1641  }
1642
1643  /**
1644   * Returns current action filename
1645   *
1646   * @param array $map Alias-to-filename hash array
1647   */
1648  public function get_action_file()
1649  {
1650    if (!empty($this->action_map[$this->action])) {
1651      return $this->action_map[$this->action];
1652    }
1653
1654    return strtr($this->action, '-', '_') . '.inc';
1655  }
1656
1657  /**
1658   * Fixes some user preferences according to namespace handling change.
1659   * Old Roundcube versions were using folder names with removed namespace prefix.
1660   * Now we need to add the prefix on servers where personal namespace has prefix.
1661   *
1662   * @param rcube_user $user User object
1663   */
1664  private function fix_namespace_settings($user)
1665  {
1666    $prefix     = $this->storage->get_namespace('prefix');
1667    $prefix_len = strlen($prefix);
1668
1669    if (!$prefix_len)
1670      return;
1671
1672    $prefs = $this->config->all();
1673    if (!empty($prefs['namespace_fixed']))
1674      return;
1675
1676    // Build namespace prefix regexp
1677    $ns     = $this->storage->get_namespace();
1678    $regexp = array();
1679
1680    foreach ($ns as $entry) {
1681      if (!empty($entry)) {
1682        foreach ($entry as $item) {
1683          if (strlen($item[0])) {
1684            $regexp[] = preg_quote($item[0], '/');
1685          }
1686        }
1687      }
1688    }
1689    $regexp = '/^('. implode('|', $regexp).')/';
1690
1691    // Fix preferences
1692    $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
1693    foreach ($opts as $opt) {
1694      if ($value = $prefs[$opt]) {
1695        if ($value != 'INBOX' && !preg_match($regexp, $value)) {
1696          $prefs[$opt] = $prefix.$value;
1697        }
1698      }
1699    }
1700
1701    if (!empty($prefs['default_folders'])) {
1702      foreach ($prefs['default_folders'] as $idx => $name) {
1703        if ($name != 'INBOX' && !preg_match($regexp, $name)) {
1704          $prefs['default_folders'][$idx] = $prefix.$name;
1705        }
1706      }
1707    }
1708
1709    if (!empty($prefs['search_mods'])) {
1710      $folders = array();
1711      foreach ($prefs['search_mods'] as $idx => $value) {
1712        if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
1713          $idx = $prefix.$idx;
1714        }
1715        $folders[$idx] = $value;
1716      }
1717      $prefs['search_mods'] = $folders;
1718    }
1719
1720    if (!empty($prefs['message_threading'])) {
1721      $folders = array();
1722      foreach ($prefs['message_threading'] as $idx => $value) {
1723        if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
1724          $idx = $prefix.$idx;
1725        }
1726        $folders[$prefix.$idx] = $value;
1727      }
1728      $prefs['message_threading'] = $folders;
1729    }
1730
1731    if (!empty($prefs['collapsed_folders'])) {
1732      $folders     = explode('&&', $prefs['collapsed_folders']);
1733      $count       = count($folders);
1734      $folders_str = '';
1735
1736      if ($count) {
1737          $folders[0]        = substr($folders[0], 1);
1738          $folders[$count-1] = substr($folders[$count-1], 0, -1);
1739      }
1740
1741      foreach ($folders as $value) {
1742        if ($value != 'INBOX' && !preg_match($regexp, $value)) {
1743          $value = $prefix.$value;
1744        }
1745        $folders_str .= '&'.$value.'&';
1746      }
1747      $prefs['collapsed_folders'] = $folders_str;
1748    }
1749
1750    $prefs['namespace_fixed'] = true;
1751
1752    // save updated preferences and reset imap settings (default folders)
1753    $user->save_prefs($prefs);
1754    $this->set_storage_prop();
1755  }
1756
1757
1758    /**
1759     * Overwrite action variable
1760     *
1761     * @param string New action value
1762     */
1763    public function overwrite_action($action)
1764    {
1765        $this->action = $action;
1766        $this->output->set_env('action', $action);
1767    }
1768
1769
1770    /**
1771     * Send the given message using the configured method.
1772     *
1773     * @param object $message    Reference to Mail_MIME object
1774     * @param string $from       Sender address string
1775     * @param array  $mailto     Array of recipient address strings
1776     * @param array  $smtp_error SMTP error array (reference)
1777     * @param string $body_file  Location of file with saved message body (reference),
1778     *                           used when delay_file_io is enabled
1779     * @param array  $smtp_opts  SMTP options (e.g. DSN request)
1780     *
1781     * @return boolean Send status.
1782     */
1783    public function deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file = null, $smtp_opts = null)
1784    {
1785        $headers = $message->headers();
1786
1787        // send thru SMTP server using custom SMTP library
1788        if ($this->config->get('smtp_server')) {
1789            // generate list of recipients
1790            $a_recipients = array($mailto);
1791
1792            if (strlen($headers['Cc']))
1793                $a_recipients[] = $headers['Cc'];
1794            if (strlen($headers['Bcc']))
1795                $a_recipients[] = $headers['Bcc'];
1796
1797            // clean Bcc from header for recipients
1798            $send_headers = $headers;
1799            unset($send_headers['Bcc']);
1800            // here too, it because txtHeaders() below use $message->_headers not only $send_headers
1801            unset($message->_headers['Bcc']);
1802
1803            $smtp_headers = $message->txtHeaders($send_headers, true);
1804
1805            if ($message->getParam('delay_file_io')) {
1806                // use common temp dir
1807                $temp_dir = $this->config->get('temp_dir');
1808                $body_file = tempnam($temp_dir, 'rcmMsg');
1809                if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
1810                    self::raise_error(array('code' => 650, 'type' => 'php',
1811                        'file' => __FILE__, 'line' => __LINE__,
1812                        'message' => "Could not create message: ".$mime_result->getMessage()),
1813                        TRUE, FALSE);
1814                    return false;
1815                }
1816                $msg_body = fopen($body_file, 'r');
1817            }
1818            else {
1819                $msg_body = $message->get();
1820            }
1821
1822            // send message
1823            if (!is_object($this->smtp)) {
1824                $this->smtp_init(true);
1825            }
1826
1827            $sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
1828            $smtp_response = $this->smtp->get_response();
1829            $smtp_error = $this->smtp->get_error();
1830
1831            // log error
1832            if (!$sent) {
1833                self::raise_error(array('code' => 800, 'type' => 'smtp',
1834                    'line' => __LINE__, 'file' => __FILE__,
1835                    'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
1836            }
1837        }
1838        // send mail using PHP's mail() function
1839        else {
1840            // unset some headers because they will be added by the mail() function
1841            $headers_enc = $message->headers($headers);
1842            $headers_php = $message->_headers;
1843            unset($headers_php['To'], $headers_php['Subject']);
1844
1845            // reset stored headers and overwrite
1846            $message->_headers = array();
1847            $header_str = $message->txtHeaders($headers_php);
1848
1849            // #1485779
1850            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1851                if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
1852                    $headers_enc['To'] = implode(', ', $m[1]);
1853                }
1854            }
1855
1856            $msg_body = $message->get();
1857
1858            if (PEAR::isError($msg_body)) {
1859                self::raise_error(array('code' => 650, 'type' => 'php',
1860                    'file' => __FILE__, 'line' => __LINE__,
1861                    'message' => "Could not create message: ".$msg_body->getMessage()),
1862                    TRUE, FALSE);
1863            }
1864            else {
1865                $delim   = $this->config->header_delimiter();
1866                $to      = $headers_enc['To'];
1867                $subject = $headers_enc['Subject'];
1868                $header_str = rtrim($header_str);
1869
1870                if ($delim != "\r\n") {
1871                    $header_str = str_replace("\r\n", $delim, $header_str);
1872                    $msg_body   = str_replace("\r\n", $delim, $msg_body);
1873                    $to         = str_replace("\r\n", $delim, $to);
1874                    $subject    = str_replace("\r\n", $delim, $subject);
1875                }
1876
1877                if (ini_get('safe_mode'))
1878                    $sent = mail($to, $subject, $msg_body, $header_str);
1879                else
1880                    $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1881            }
1882        }
1883
1884        if ($sent) {
1885            $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1886
1887            // remove MDN headers after sending
1888            unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1889
1890            // get all recipients
1891            if ($headers['Cc'])
1892                $mailto .= $headers['Cc'];
1893            if ($headers['Bcc'])
1894                $mailto .= $headers['Bcc'];
1895            if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1896                $mailto = implode(', ', array_unique($m[1]));
1897
1898            if ($this->config->get('smtp_log')) {
1899                self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1900                    $this->user->get_username(),
1901                    $_SERVER['REMOTE_ADDR'],
1902                    $mailto,
1903                    !empty($smtp_response) ? join('; ', $smtp_response) : ''));
1904            }
1905        }
1906
1907        if (is_resource($msg_body)) {
1908            fclose($msg_body);
1909        }
1910
1911        $message->_headers = array();
1912        $message->headers($headers);
1913
1914        return $sent;
1915    }
1916
1917
1918    /**
1919     * Unique Message-ID generator.
1920     *
1921     * @return string Message-ID
1922     */
1923    public function gen_message_id()
1924    {
1925        $local_part  = md5(uniqid('rcmail'.mt_rand(),true));
1926        $domain_part = $this->user->get_username('domain');
1927
1928        // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1929        if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1930            foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
1931                $host = preg_replace('/:[0-9]+$/', '', $host);
1932                if ($host && preg_match('/\.[a-z]+$/i', $host)) {
1933                    $domain_part = $host;
1934                }
1935            }
1936        }
1937
1938        return sprintf('<%s@%s>', $local_part, $domain_part);
1939    }
1940
1941
1942    /**
1943     * Returns RFC2822 formatted current date in user's timezone
1944     *
1945     * @return string Date
1946     */
1947    public function user_date()
1948    {
1949        // get user's timezone
1950        try {
1951            $tz   = new DateTimeZone($this->config->get('timezone'));
1952            $date = new DateTime('now', $tz);
1953        }
1954        catch (Exception $e) {
1955            $date = new DateTime();
1956        }
1957
1958        return $date->format('r');
1959    }
1960
1961
1962    /**
1963     * Replaces hostname variables.
1964     *
1965     * @param string $name Hostname
1966     * @param string $host Optional IMAP hostname
1967     *
1968     * @return string Hostname
1969     */
1970    public static function parse_host($name, $host = '')
1971    {
1972        // %n - host
1973        $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1974        // %d - domain name without first part, e.g. %n=mail.domain.tld, %d=domain.tld
1975        $d = preg_replace('/^[^\.]+\./', '', $n);
1976        // %h - IMAP host
1977        $h = $_SESSION['storage_host'] ? $_SESSION['storage_host'] : $host;
1978        // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1979        $z = preg_replace('/^[^\.]+\./', '', $h);
1980        // %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided
1981        if (strpos($name, '%s') !== false) {
1982            $user_email = rcube_ui::get_input_value('_user', rcube_ui::INPUT_POST);
1983            $user_email = rcube_idn_convert($user_email, true);
1984            $matches    = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s);
1985            if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) {
1986                return false;
1987            }
1988        }
1989
1990        $name = str_replace(array('%n', '%d', '%h', '%z', '%s'), array($n, $d, $h, $z, $s[2]), $name);
1991        return $name;
1992    }
1993
1994
1995    /**
1996     * E-mail address validation.
1997     *
1998     * @param string $email Email address
1999     * @param boolean $dns_check True to check dns
2000     *
2001     * @return boolean True on success, False if address is invalid
2002     */
2003    public function check_email($email, $dns_check=true)
2004    {
2005        // Check for invalid characters
2006        if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email)) {
2007            return false;
2008        }
2009
2010        // Check for length limit specified by RFC 5321 (#1486453)
2011        if (strlen($email) > 254) {
2012            return false;
2013        }
2014
2015        $email_array = explode('@', $email);
2016
2017        // Check that there's one @ symbol
2018        if (count($email_array) < 2) {
2019            return false;
2020        }
2021
2022        $domain_part = array_pop($email_array);
2023        $local_part  = implode('@', $email_array);
2024
2025        // from PEAR::Validate
2026        $regexp = '&^(?:
2027                ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                                         #1 quoted name
2028                ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*))  #2 OR dot-atom (RFC5322)
2029                $&xi';
2030
2031        if (!preg_match($regexp, $local_part)) {
2032            return false;
2033        }
2034
2035        // Check domain part
2036        if (preg_match('/^\[*(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]*$/', $domain_part)) {
2037            return true; // IP address
2038        }
2039        else {
2040            // If not an IP address
2041            $domain_array = explode('.', $domain_part);
2042            // Not enough parts to be a valid domain
2043            if (sizeof($domain_array) < 2) {
2044                return false;
2045            }
2046
2047            foreach ($domain_array as $part) {
2048                if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
2049                    return false;
2050                }
2051            }
2052
2053            if (!$dns_check || !$this->config->get('email_dns_check')) {
2054                return true;
2055            }
2056
2057            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
2058                $lookup = array();
2059                @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
2060                foreach ($lookup as $line) {
2061                    if (strpos($line, 'MX preference')) {
2062                        return true;
2063                    }
2064                }
2065                return false;
2066            }
2067
2068            // find MX record(s)
2069            if (getmxrr($domain_part, $mx_records)) {
2070                return true;
2071            }
2072
2073            // find any DNS record
2074            if (checkdnsrr($domain_part, 'ANY')) {
2075                return true;
2076            }
2077        }
2078
2079        return false;
2080    }
2081
2082
2083    /**
2084     * Print or write debug messages
2085     *
2086     * @param mixed Debug message or data
2087     */
2088    public static function console()
2089    {
2090        $args = func_get_args();
2091
2092        if (class_exists('rcmail', false)) {
2093            $rcmail = rcmail::get_instance();
2094            if (is_object($rcmail->plugins)) {
2095                $plugin = $rcmail->plugins->exec_hook('console', array('args' => $args));
2096                if ($plugin['abort']) {
2097                    return;
2098                }
2099               $args = $plugin['args'];
2100            }
2101        }
2102
2103        $msg = array();
2104        foreach ($args as $arg) {
2105            $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
2106        }
2107
2108        self::write_log('console', join(";\n", $msg));
2109    }
2110
2111
2112    /**
2113     * Append a line to a logfile in the logs directory.
2114     * Date will be added automatically to the line.
2115     *
2116     * @param $name name of log file
2117     * @param line Line to append
2118     */
2119    public static function write_log($name, $line)
2120    {
2121        global $RCMAIL;
2122
2123        if (!is_string($line)) {
2124            $line = var_export($line, true);
2125        }
2126
2127        $date_format = $RCMAIL ? $RCMAIL->config->get('log_date_format') : null;
2128        $log_driver  = $RCMAIL ? $RCMAIL->config->get('log_driver') : null;
2129
2130        if (empty($date_format)) {
2131            $date_format = 'd-M-Y H:i:s O';
2132        }
2133
2134        $date = date($date_format);
2135
2136        // trigger logging hook
2137        if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
2138            $log  = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
2139            $name = $log['name'];
2140            $line = $log['line'];
2141            $date = $log['date'];
2142            if ($log['abort'])
2143                return true;
2144        }
2145
2146        if ($log_driver == 'syslog') {
2147            $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
2148            syslog($prio, $line);
2149            return true;
2150        }
2151
2152        // log_driver == 'file' is assumed here
2153
2154        $line = sprintf("[%s]: %s\n", $date, $line);
2155        $log_dir  = $RCMAIL ? $RCMAIL->config->get('log_dir') : null;
2156
2157        if (empty($log_dir)) {
2158            $log_dir = INSTALL_PATH . 'logs';
2159        }
2160
2161        // try to open specific log file for writing
2162        $logfile = $log_dir.'/'.$name;
2163
2164        if ($fp = @fopen($logfile, 'a')) {
2165            fwrite($fp, $line);
2166            fflush($fp);
2167            fclose($fp);
2168            return true;
2169        }
2170
2171        trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
2172        return false;
2173    }
2174
2175
2176    /**
2177     * Throw system error (and show error page).
2178     *
2179     * @param array Named parameters
2180     *      - code:    Error code (required)
2181     *      - type:    Error type [php|db|imap|javascript] (required)
2182     *      - message: Error message
2183     *      - file:    File where error occured
2184     *      - line:    Line where error occured
2185     * @param boolean True to log the error
2186     * @param boolean Terminate script execution
2187     */
2188    public static function raise_error($arg = array(), $log = false, $terminate = false)
2189    {
2190        // installer
2191        if (class_exists('rcube_install', false)) {
2192            $rci = rcube_install::get_instance();
2193            $rci->raise_error($arg);
2194            return;
2195        }
2196
2197        if ($log && $arg['type'] && $arg['message']) {
2198            self::log_bug($arg);
2199        }
2200
2201        // display error page and terminate script
2202        if ($terminate) {
2203            rcube_ui::raise_error($arg['code'], $arg['message']);
2204        }
2205    }
2206
2207
2208    /**
2209     * Report error according to configured debug_level
2210     *
2211     * @param array Named parameters
2212     * @see self::raise_error()
2213     */
2214    public static function log_bug($arg_arr)
2215    {
2216        $rcmail  = rcmail::get_instance();
2217        $program = strtoupper($arg_arr['type']);
2218        $level   = $rcmail->config->get('debug_level');
2219
2220        // disable errors for ajax requests, write to log instead (#1487831)
2221        if (($level & 4) && !empty($_REQUEST['_remote'])) {
2222            $level = ($level ^ 4) | 1;
2223        }
2224
2225        // write error to local log file
2226        if ($level & 1) {
2227            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
2228                $post_query = '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']);
2229            }
2230            else {
2231                $post_query = '';
2232            }
2233
2234            $log_entry = sprintf("%s Error: %s%s (%s %s)",
2235                $program,
2236                $arg_arr['message'],
2237                $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
2238                $_SERVER['REQUEST_METHOD'],
2239                $_SERVER['REQUEST_URI'] . $post_query);
2240
2241            if (!self::write_log('errors', $log_entry)) {
2242                // send error to PHPs error handler if write_log didn't succeed
2243                trigger_error($arg_arr['message']);
2244            }
2245        }
2246
2247        // report the bug to the global bug reporting system
2248        if ($level & 2) {
2249            // TODO: Send error via HTTP
2250        }
2251
2252        // show error if debug_mode is on
2253        if ($level & 4) {
2254            print "<b>$program Error";
2255
2256            if (!empty($arg_arr['file']) && !empty($arg_arr['line'])) {
2257                print " in $arg_arr[file] ($arg_arr[line])";
2258            }
2259
2260            print ':</b>&nbsp;';
2261            print nl2br($arg_arr['message']);
2262            print '<br />';
2263            flush();
2264        }
2265    }
2266
2267
2268    /**
2269     * Write login data (name, ID, IP address) to the 'userlogins' log file.
2270     */
2271    public function log_login()
2272    {
2273        if (!$this->config->get('log_logins')) {
2274            return;
2275        }
2276
2277        $user_name = $this->get_user_name();
2278        $user_id   = $this->get_user_id();
2279
2280        if (!$user_id) {
2281            return;
2282        }
2283
2284        self::write_log('userlogins',
2285            sprintf('Successful login for %s (ID: %d) from %s in session %s',
2286                $user_name, $user_id, self::remote_ip(), session_id()));
2287    }
2288
2289
2290    /**
2291     * Returns remote IP address and forwarded addresses if found
2292     *
2293     * @return string Remote IP address(es)
2294     */
2295    public static function remote_ip()
2296    {
2297        $address = $_SERVER['REMOTE_ADDR'];
2298
2299        // append the NGINX X-Real-IP header, if set
2300        if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
2301            $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
2302        }
2303        // append the X-Forwarded-For header, if set
2304        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
2305            $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
2306        }
2307
2308        if (!empty($remote_ip)) {
2309            $address .= '(' . implode(',', $remote_ip) . ')';
2310        }
2311
2312        return $address;
2313    }
2314
2315
2316    /**
2317     * Check whether the HTTP referer matches the current request
2318     *
2319     * @return boolean True if referer is the same host+path, false if not
2320     */
2321    public static function check_referer()
2322    {
2323        $uri = parse_url($_SERVER['REQUEST_URI']);
2324        $referer = parse_url(rcube_request_header('Referer'));
2325        return $referer['host'] == rcube_request_header('Host') && $referer['path'] == $uri['path'];
2326    }
2327
2328
2329    /**
2330     * Returns current time (with microseconds).
2331     *
2332     * @return float Current time in seconds since the Unix
2333     */
2334    public static function timer()
2335    {
2336        return microtime(true);
2337    }
2338
2339
2340    /**
2341     * Logs time difference according to provided timer
2342     *
2343     * @param float  $timer  Timer (self::timer() result)
2344     * @param string $label  Log line prefix
2345     * @param string $dest   Log file name
2346     *
2347     * @see self::timer()
2348     */
2349    public static function print_timer($timer, $label = 'Timer', $dest = 'console')
2350    {
2351        static $print_count = 0;
2352
2353        $print_count++;
2354        $now  = self::timer();
2355        $diff = $now - $timer;
2356
2357        if (empty($label)) {
2358            $label = 'Timer '.$print_count;
2359        }
2360
2361        self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
2362    }
2363
2364
2365    /**
2366     * Garbage collector function for temp files.
2367     * Remove temp files older than two days
2368     */
2369    public function temp_gc()
2370    {
2371        $tmp = unslashify($this->config->get('temp_dir'));
2372        $expire = mktime() - 172800;  // expire in 48 hours
2373
2374        if ($dir = opendir($tmp)) {
2375            while (($fname = readdir($dir)) !== false) {
2376                if ($fname{0} == '.') {
2377                    continue;
2378                }
2379
2380                if (filemtime($tmp.'/'.$fname) < $expire) {
2381                    @unlink($tmp.'/'.$fname);
2382                }
2383            }
2384
2385            closedir($dir);
2386        }
2387    }
2388
2389
2390    /**
2391     * Garbage collector for cache entries.
2392     * Remove all expired message cache records
2393     */
2394    public function cache_gc()
2395    {
2396        $db = $this->get_dbh();
2397
2398        // get target timestamp
2399        $ts = get_offset_time($this->config->get('message_cache_lifetime', '30d'), -1);
2400
2401        $db->query("DELETE FROM " . $db->table_name('cache_messages')
2402            ." WHERE changed < " . $db->fromunixtime($ts));
2403
2404        $db->query("DELETE FROM " . $db->table_name('cache_index')
2405            ." WHERE changed < " . $db->fromunixtime($ts));
2406
2407        $db->query("DELETE FROM " . $db->table_name('cache_thread')
2408            ." WHERE changed < " . $db->fromunixtime($ts));
2409
2410        $db->query("DELETE FROM " . $db->table_name('cache')
2411            ." WHERE created < " . $db->fromunixtime($ts));
2412    }
2413
2414
2415    /**
2416     * Getter for logged user ID.
2417     *
2418     * @return mixed User identifier
2419     */
2420    public function get_user_id()
2421    {
2422        if (is_object($this->user)) {
2423            return $this->user->ID;
2424        }
2425    }
2426
2427
2428    /**
2429     * Getter for logged user name.
2430     *
2431     * @return string User name
2432     */
2433    public function get_user_name()
2434    {
2435        if (is_object($this->user)) {
2436            return $this->user->get_username();
2437        }
2438    }
2439}
Note: See TracBrowser for help on using the repository browser.