Aleksander Machniak
2013-06-05 4fee776e51199b51853fa98c42d4d2af0b5330ca
commit | author | age
197601 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcmail.php                                            |
6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
0c2596 8  | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
A 9  | Copyright (C) 2011-2012, Kolab Systems AG                             |
7fe381 10  |                                                                       |
T 11  | Licensed under the GNU General Public License version 3 or            |
12  | any later version with exceptions for skins & plugins.                |
13  | See the README file for a full license statement.                     |
197601 14  |                                                                       |
T 15  | PURPOSE:                                                              |
16  |   Application class providing core functions and holding              |
17  |   instances of all 'global' objects like db- and imap-connections     |
18  +-----------------------------------------------------------------------+
19  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
1aceb9 20  | Author: Aleksander Machniak <alec@alec.pl>                            |
197601 21  +-----------------------------------------------------------------------+
T 22 */
23
24
25 /**
e019f2 26  * Application class of Roundcube Webmail
197601 27  * implemented as singleton
T 28  *
29  * @package Core
30  */
0c2596 31 class rcmail extends rcube
197601 32 {
5c461b 33   /**
A 34    * Main tasks.
35    *
36    * @var array
37    */
677e1f 38   static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
5c461b 39
A 40   /**
41    * Current task.
42    *
43    * @var string
44    */
9b94eb 45   public $task;
5c461b 46
A 47   /**
48    * Current action.
49    *
50    * @var string
51    */
197601 52   public $action = '';
T 53   public $comm_path = './';
677e1f 54
0501b6 55   private $address_books = array();
68d2d5 56   private $action_map = array();
677e1f 57
A 58
7c8fd8 59   const ERROR_STORAGE          = -2;
AM 60   const ERROR_INVALID_REQUEST  = 1;
61   const ERROR_INVALID_HOST     = 2;
62   const ERROR_COOKIES_DISABLED = 3;
63
64
197601 65   /**
T 66    * This implements the 'singleton' design pattern
67    *
5c461b 68    * @return rcmail The one and only instance
197601 69    */
T 70   static function get_instance()
71   {
0c2596 72     if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
197601 73       self::$instance = new rcmail();
T 74       self::$instance->startup();  // init AFTER object was linked with self::$instance
75     }
76
77     return self::$instance;
78   }
b62a0d 79
A 80
197601 81   /**
T 82    * Initial startup function
83    * to register session, create database and imap connections
84    */
0c2596 85   protected function startup()
197601 86   {
0c2596 87     $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
a90ad2 88
929a50 89     // start session
A 90     $this->session_init();
197601 91
T 92     // create user object
93     $this->set_user(new rcube_user($_SESSION['user_id']));
929a50 94
9b94eb 95     // set task and action properties
1aceb9 96     $this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC));
A 97     $this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC));
9b94eb 98
197601 99     // reset some session parameters when changing task
677e1f 100     if ($this->task != 'utils') {
7dcf24 101       // we reset list page when switching to another task
AM 102       // but only to the main task interface - empty action (#1489076)
103       // this will prevent from unintentional page reset on cross-task requests
104       if ($this->session && $_SESSION['task'] != $this->task && empty($this->action))
677e1f 105         $this->session->remove('page');
A 106       // set current task to session
107       $_SESSION['task'] = $this->task;
108     }
197601 109
48bc52 110     // init output class
A 111     if (!empty($_REQUEST['_remote']))
929a50 112       $GLOBALS['OUTPUT'] = $this->json_init();
48bc52 113     else
A 114       $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
115
0c2596 116     // load plugins
A 117     $this->plugins->init($this, $this->task);
118     $this->plugins->load_plugins((array)$this->config->get('plugins', array()), array('filesystem_attachments', 'jqueryui'));
197601 119   }
b62a0d 120
A 121
197601 122   /**
T 123    * Setter for application task
124    *
125    * @param string Task to set
126    */
127   public function set_task($task)
128   {
8b7716 129     $task = asciiwords($task, true);
9b94eb 130
A 131     if ($this->user && $this->user->ID)
c3be8e 132       $task = !$task ? 'mail' : $task;
9b94eb 133     else
A 134       $task = 'login';
135
136     $this->task = $task;
1c932d 137     $this->comm_path = $this->url(array('task' => $this->task));
b62a0d 138
197601 139     if ($this->output)
1c932d 140       $this->output->set_env('task', $this->task);
197601 141   }
b62a0d 142
A 143
197601 144   /**
T 145    * Setter for system user object
146    *
5c461b 147    * @param rcube_user Current user instance
197601 148    */
T 149   public function set_user($user)
150   {
151     if (is_object($user)) {
152       $this->user = $user;
b62a0d 153
197601 154       // overwrite config with user preferences
b545d3 155       $this->config->set_user_prefs((array)$this->user->get_prefs());
197601 156     }
b62a0d 157
553225 158     $lang = $this->language_prop($this->config->get('language', $_SESSION['language']));
AM 159     $_SESSION['language'] = $this->user->language = $lang;
531abb 160
197601 161     // set localization
553225 162     setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8');
14de18 163
b62a0d 164     // workaround for http://bugs.php.net/bug.php?id=18556
553225 165     if (in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
AM 166       setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8');
167     }
197601 168   }
b62a0d 169
A 170
197601 171   /**
ade8e1 172    * Return instance of the internal address book class
T 173    *
840b4d 174    * @param string  Address book identifier (-1 for default addressbook)
ade8e1 175    * @param boolean True if the address book needs to be writeable
7f7ed2 176    *
5c461b 177    * @return rcube_contacts Address book object
ade8e1 178    */
T 179   public function get_address_book($id, $writeable = false)
180   {
b896b1 181     $contacts    = null;
ade8e1 182     $ldap_config = (array)$this->config->get('ldap_public');
cc97ea 183
f03d89 184     // 'sql' is the alias for '0' used by autocomplete
A 185     if ($id == 'sql')
840b4d 186       $id = '0';
AM 187     else if ($id == -1) {
188       $id = $this->config->get('default_addressbook');
189       $default = true;
190     }
f03d89 191
0501b6 192     // use existing instance
840b4d 193     if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
0501b6 194       $contacts = $this->address_books[$id];
T 195     }
cc97ea 196     else if ($id && $ldap_config[$id]) {
c321a9 197       $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['storage_host']));
cc97ea 198     }
T 199     else if ($id === '0') {
0c2596 200       $contacts = new rcube_contacts($this->db, $this->get_user_id());
ade8e1 201     }
b896b1 202     else {
A 203       $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
204
205       // plugin returned instance of a rcube_addressbook
206       if ($plugin['instance'] instanceof rcube_addressbook) {
207         $contacts = $plugin['instance'];
208       }
840b4d 209     }
AM 210
65dff8 211     // when user requested default writeable addressbook
AM 212     // we need to check if default is writeable, if not we
213     // will return first writeable book (if any exist)
214     if ($contacts && $default && $contacts->readonly && $writeable) {
215       $contacts = null;
216     }
217
840b4d 218     // Get first addressbook from the list if configured default doesn't exist
AM 219     // This can happen when user deleted the addressbook (e.g. Kolab folder)
220     if (!$contacts && (!$id || $default)) {
65dff8 221       $source = reset($this->get_address_sources($writeable, !$default));
840b4d 222       if (!empty($source)) {
AM 223         $contacts = $this->get_address_book($source['id']);
65dff8 224         if ($contacts) {
840b4d 225           $id = $source['id'];
65dff8 226         }
ade8e1 227       }
5ed119 228     }
A 229
230     if (!$contacts) {
0c2596 231       self::raise_error(array(
782d85 232         'code' => 700, 'type' => 'php',
5ed119 233         'file' => __FILE__, 'line' => __LINE__,
A 234         'message' => "Addressbook source ($id) not found!"),
235         true, true);
ade8e1 236     }
457373 237
65dff8 238     // add to the 'books' array for shutdown function
AM 239     $this->address_books[$id] = $contacts;
240
840b4d 241     if ($writeable && $contacts->readonly) {
AM 242       return null;
243     }
244
438753 245     // set configured sort order
65dff8 246     if ($sort_col = $this->config->get('addressbook_sort_col')) {
438753 247         $contacts->set_sort_order($sort_col);
65dff8 248     }
b62a0d 249
ade8e1 250     return $contacts;
T 251   }
3704b7 252
A 253
254   /**
255    * Return address books list
256    *
257    * @param boolean True if the address book needs to be writeable
65dff8 258    * @param boolean True if the address book needs to be not hidden
5c9d1f 259    *
3704b7 260    * @return array  Address books array
A 261    */
65dff8 262   public function get_address_sources($writeable = false, $skip_hidden = false)
3704b7 263   {
d14a2f 264     $abook_type   = (string) $this->config->get('address_book_type');
AM 265     $ldap_config  = (array) $this->config->get('ldap_public');
7fdb9d 266     $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
d14a2f 267     $list         = array();
3704b7 268
5526f9 269     // We are using the DB address book or a plugin address book
d14a2f 270     if (!empty($abook_type) && strtolower($abook_type) != 'ldap') {
0501b6 271       if (!isset($this->address_books['0']))
0c2596 272         $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
3704b7 273       $list['0'] = array(
5c9d1f 274         'id'       => '0',
0c2596 275         'name'     => $this->gettext('personaladrbook'),
5c9d1f 276         'groups'   => $this->address_books['0']->groups,
8c263e 277         'readonly' => $this->address_books['0']->readonly,
d06e57 278         'autocomplete' => in_array('sql', $autocomplete),
T 279         'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
3704b7 280       );
A 281     }
282
d14a2f 283     if (!empty($ldap_config)) {
08b7b6 284       foreach ($ldap_config as $id => $prop) {
A 285         // handle misconfiguration
286         if (empty($prop) || !is_array($prop)) {
287           continue;
288         }
3704b7 289         $list[$id] = array(
5c9d1f 290           'id'       => $id,
1b9923 291           'name'     => html::quote($prop['name']),
5c9d1f 292           'groups'   => is_array($prop['groups']),
a61bbb 293           'readonly' => !$prop['writable'],
5c9d1f 294           'hidden'   => $prop['hidden'],
A 295           'autocomplete' => in_array($id, $autocomplete)
3704b7 296         );
08b7b6 297       }
3704b7 298     }
A 299
e6ce00 300     $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
3704b7 301     $list = $plugin['sources'];
A 302
0501b6 303     foreach ($list as $idx => $item) {
T 304       // register source for shutdown function
65dff8 305       if (!is_object($this->address_books[$item['id']])) {
0501b6 306         $this->address_books[$item['id']] = $item;
65dff8 307       }
0501b6 308       // remove from list if not writeable as requested
65dff8 309       if ($writeable && $item['readonly']) {
c0297f 310           unset($list[$idx]);
65dff8 311       }
AM 312       // remove from list if hidden as requested
313       else if ($skip_hidden && $item['hidden']) {
314           unset($list[$idx]);
315       }
3704b7 316     }
8c263e 317
3704b7 318     return $list;
A 319   }
b62a0d 320
A 321
ade8e1 322   /**
197601 323    * Init output object for GUI and add common scripts.
60226a 324    * This will instantiate a rcmail_output_html object and set
197601 325    * environment vars according to the current session and configuration
0ece58 326    *
T 327    * @param boolean True if this request is loaded in a (i)frame
60226a 328    * @return rcube_output Reference to HTML output object
197601 329    */
T 330   public function load_gui($framed = false)
331   {
332     // init output page
60226a 333     if (!($this->output instanceof rcmail_output_html))
TB 334       $this->output = new rcmail_output_html($this->task, $framed);
197601 335
f22654 336     // set refresh interval
AM 337     $this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0));
c442f8 338     $this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60);
197601 339
T 340     if ($framed) {
341       $this->comm_path .= '&_framed=1';
342       $this->output->set_env('framed', true);
343     }
344
345     $this->output->set_env('task', $this->task);
346     $this->output->set_env('action', $this->action);
347     $this->output->set_env('comm_path', $this->comm_path);
a92beb 348     $this->output->set_charset(RCUBE_CHARSET);
197601 349
7f5a84 350     // add some basic labels to client
77de23 351     $this->output->add_label('loading', 'servererror', 'requesttimedout', 'refreshing');
b62a0d 352
197601 353     return $this->output;
T 354   }
b62a0d 355
A 356
197601 357   /**
T 358    * Create an output object for JSON responses
0ece58 359    *
60226a 360    * @return rcube_output Reference to JSON output object
197601 361    */
929a50 362   public function json_init()
197601 363   {
60226a 364     if (!($this->output instanceof rcmail_output_json))
TB 365       $this->output = new rcmail_output_json($this->task);
b62a0d 366
197601 367     return $this->output;
929a50 368   }
A 369
370
371   /**
372    * Create session object and start the session.
373    */
374   public function session_init()
375   {
963a10 376     parent::session_init();
929a50 377
A 378     // set initial session vars
cf2da2 379     if (!$_SESSION['user_id'])
929a50 380       $_SESSION['temp'] = true;
40d246 381
T 382     // restore skin selection after logout
383     if ($_SESSION['temp'] && !empty($_SESSION['skin']))
384       $this->config->set('skin', $_SESSION['skin']);
197601 385   }
T 386
387
388   /**
c321a9 389    * Perfom login to the mail server and to the webmail service.
197601 390    * This will also create a new user entry if auto_create_user is configured.
T 391    *
c321a9 392    * @param string Mail storage (IMAP) user name
T 393    * @param string Mail storage (IMAP) password
394    * @param string Mail storage (IMAP) host
7c8fd8 395    * @param bool   Enables cookie check
fdff34 396    *
197601 397    * @return boolean True on success, False on failure
T 398    */
7c8fd8 399   function login($username, $pass, $host = null, $cookiecheck = false)
197601 400   {
7c8fd8 401     $this->login_error = null;
AM 402
fdff34 403     if (empty($username)) {
7c8fd8 404       return false;
AM 405     }
406
407     if ($cookiecheck && empty($_COOKIE)) {
408       $this->login_error = self::ERROR_COOKIES_DISABLED;
fdff34 409       return false;
A 410     }
411
197601 412     $config = $this->config->all();
T 413
414     if (!$host)
415       $host = $config['default_host'];
416
417     // Validate that selected host is in the list of configured hosts
418     if (is_array($config['default_host'])) {
419       $allowed = false;
420       foreach ($config['default_host'] as $key => $host_allowed) {
421         if (!is_numeric($key))
422           $host_allowed = $key;
423         if ($host == $host_allowed) {
424           $allowed = true;
425           break;
426         }
427       }
7c8fd8 428       if (!$allowed) {
AM 429         $host = null;
197601 430       }
7c8fd8 431     }
AM 432     else if (!empty($config['default_host']) && $host != rcube_utils::parse_host($config['default_host'])) {
433       $host = null;
434     }
435
436     if (!$host) {
437       $this->login_error = self::ERROR_INVALID_HOST;
197601 438       return false;
7c8fd8 439     }
197601 440
T 441     // parse $host URL
442     $a_host = parse_url($host);
443     if ($a_host['host']) {
444       $host = $a_host['host'];
c321a9 445       $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
e99991 446       if (!empty($a_host['port']))
c321a9 447         $port = $a_host['port'];
T 448       else if ($ssl && $ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
449         $port = 993;
197601 450     }
b62a0d 451
c321a9 452     if (!$port) {
T 453         $port = $config['default_port'];
454     }
197601 455
b62a0d 456     /* Modify username with domain if required
197601 457        Inspired by Marco <P0L0_notspam_binware.org>
T 458     */
459     // Check if we need to add domain
c16fab 460     if (!empty($config['username_domain']) && strpos($username, '@') === false) {
197601 461       if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
1aceb9 462         $username .= '@'.rcube_utils::parse_host($config['username_domain'][$host], $host);
197601 463       else if (is_string($config['username_domain']))
1aceb9 464         $username .= '@'.rcube_utils::parse_host($config['username_domain'], $host);
197601 465     }
T 466
df95e7 467     if (!isset($config['login_lc'])) {
AM 468       $config['login_lc'] = 2; // default
469     }
470
c321a9 471     // Convert username to lowercase. If storage backend
e17553 472     // is case-insensitive we need to store always the same username (#1487113)
A 473     if ($config['login_lc']) {
c72325 474       if ($config['login_lc'] == 2 || $config['login_lc'] === true) {
AM 475         $username = mb_strtolower($username);
476       }
477       else if (strpos($username, '@')) {
478         // lowercase domain name
479         list($local, $domain) = explode('@', $username);
480         $username = $local . '@' . mb_strtolower($domain);
481       }
e17553 482     }
A 483
942069 484     // try to resolve email address from virtuser table
e17553 485     if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
A 486       $username = $virtuser;
487     }
197601 488
f1adbf 489     // Here we need IDNA ASCII
A 490     // Only rcube_contacts class is using domain names in Unicode
c72325 491     $host     = rcube_utils::idn_to_ascii($host);
AM 492     $username = rcube_utils::idn_to_ascii($username);
f1adbf 493
197601 494     // user already registered -> overwrite username
c72325 495     if ($user = rcube_user::query($username, $host)) {
197601 496       $username = $user->data['username'];
c72325 497     }
197601 498
1aceb9 499     $storage = $this->get_storage();
48bc52 500
c321a9 501     // try to log in
df95e7 502     if (!$storage->connect($host, $username, $pass, $port, $ssl)) {
197601 503       return false;
c321a9 504     }
197601 505
T 506     // user already registered -> update user's record
507     if (is_object($user)) {
d08333 508       // update last login timestamp
197601 509       $user->touch();
T 510     }
511     // create new system user
512     else if ($config['auto_create_user']) {
513       if ($created = rcube_user::create($username, $host)) {
514         $user = $created;
515       }
f879f4 516       else {
0c2596 517         self::raise_error(array(
782d85 518           'code' => 620, 'type' => 'php',
6d94ab 519           'file' => __FILE__, 'line' => __LINE__,
f879f4 520           'message' => "Failed to create a user record. Maybe aborted by a plugin?"
10eedb 521           ), true, false);
f879f4 522       }
197601 523     }
T 524     else {
0c2596 525       self::raise_error(array(
782d85 526         'code' => 621, 'type' => 'php',
10eedb 527         'file' => __FILE__, 'line' => __LINE__,
782d85 528         'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
197601 529         ), true, false);
T 530     }
531
532     // login succeeded
533     if (is_object($user) && $user->ID) {
f53750 534       // Configure environment
197601 535       $this->set_user($user);
c321a9 536       $this->set_storage_prop();
f53750 537
A 538       // fix some old settings according to namespace prefix
539       $this->fix_namespace_settings($user);
540
541       // create default folders on first login
542       if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
1aceb9 543         $storage->create_default_folders();
f53750 544       }
197601 545
T 546       // set session vars
c321a9 547       $_SESSION['user_id']      = $user->ID;
T 548       $_SESSION['username']     = $user->data['username'];
549       $_SESSION['storage_host'] = $host;
550       $_SESSION['storage_port'] = $port;
551       $_SESSION['storage_ssl']  = $ssl;
552       $_SESSION['password']     = $this->encrypt($pass);
6a8b4c 553       $_SESSION['login_time']   = time();
f53750 554
b62a0d 555       if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
086b15 556         $_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC);
197601 557
T 558       // force reloading complete list of subscribed mailboxes
1aceb9 559       $storage->clear_cache('mailboxes', true);
197601 560
T 561       return true;
562     }
563
564     return false;
565   }
566
567
7c8fd8 568     /**
AM 569      * Returns error code of last login operation
570      *
571      * @return int Error code
572      */
573     public function login_error()
574     {
575         if ($this->login_error) {
576             return $this->login_error;
577         }
578
579         if ($this->storage && $this->storage->get_error_code() < -1) {
580             return self::ERROR_STORAGE;
581         }
582     }
583
584
197601 585   /**
1854c4 586    * Auto-select IMAP host based on the posted login information
T 587    *
588    * @return string Selected IMAP host
589    */
590   public function autoselect_host()
591   {
592     $default_host = $this->config->get('default_host');
257f88 593     $host = null;
b62a0d 594
257f88 595     if (is_array($default_host)) {
1aceb9 596       $post_host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
45dd7c 597       $post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST);
AM 598
3725cf 599       list(, $domain) = explode('@', $post_user);
b62a0d 600
257f88 601       // direct match in default_host array
T 602       if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
603         $host = $post_host;
604       }
605       // try to select host by mail domain
45dd7c 606       else if (!empty($domain)) {
c321a9 607         foreach ($default_host as $storage_host => $mail_domains) {
48f04d 608           if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
c321a9 609             $host = $storage_host;
48f04d 610             break;
T 611           }
612           else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
613             $host = is_numeric($storage_host) ? $mail_domains : $storage_host;
1854c4 614             break;
T 615           }
616         }
617       }
618
48f04d 619       // take the first entry if $host is still not set
257f88 620       if (empty($host)) {
48f04d 621         list($key, $val) = each($default_host);
T 622         $host = is_numeric($key) ? $val : $key;
257f88 623       }
T 624     }
625     else if (empty($default_host)) {
1aceb9 626       $host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
1854c4 627     }
eec34e 628     else
1aceb9 629       $host = rcube_utils::parse_host($default_host);
1854c4 630
T 631     return $host;
632   }
633
634
635   /**
636    * Destroy session data and remove cookie
637    */
638   public function kill_session()
639   {
e6ce00 640     $this->plugins->exec_hook('session_destroy');
b62a0d 641
cf2da2 642     $this->session->kill();
40d246 643     $_SESSION = array('language' => $this->user->language, 'temp' => true, 'skin' => $this->config->get('skin'));
1854c4 644     $this->user->reset();
T 645   }
646
647
648   /**
649    * Do server side actions on logout
650    */
651   public function logout_actions()
652   {
1aceb9 653     $config  = $this->config->all();
A 654     $storage = $this->get_storage();
1854c4 655
T 656     if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
1aceb9 657       $storage->clear_folder($config['trash_mbox']);
1854c4 658     }
T 659
660     if ($config['logout_expunge']) {
1aceb9 661       $storage->expunge_folder('INBOX');
1854c4 662     }
40a186 663
A 664     // Try to save unsaved user preferences
665     if (!empty($_SESSION['preferences'])) {
666       $this->user->save_prefs(unserialize($_SESSION['preferences']));
667     }
fec2d8 668   }
T 669
670
671   /**
57f0c8 672    * Generate a unique token to be used in a form request
T 673    *
674    * @return string The request token
675    */
549933 676   public function get_request_token()
57f0c8 677   {
ec045b 678     $sess_id = $_COOKIE[ini_get('session.name')];
c9f2c4 679     if (!$sess_id) $sess_id = session_id();
1aceb9 680
A 681     $plugin = $this->plugins->exec_hook('request_token', array(
682         'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
683
ef27a6 684     return $plugin['value'];
57f0c8 685   }
b62a0d 686
A 687
57f0c8 688   /**
T 689    * Check if the current request contains a valid token
690    *
549933 691    * @param int Request method
57f0c8 692    * @return boolean True if request token is valid false if not
T 693    */
1aceb9 694   public function check_request($mode = rcube_utils::INPUT_POST)
57f0c8 695   {
1aceb9 696     $token = rcube_utils::get_input_value('_token', $mode);
ec045b 697     $sess_id = $_COOKIE[ini_get('session.name')];
T 698     return !empty($sess_id) && $token == $this->get_request_token();
1854c4 699   }
T 700
564741 701
A 702   /**
e019f2 703    * Build a valid URL to this instance of Roundcube
c719f3 704    *
T 705    * @param mixed Either a string with the action or url parameters as key-value pairs
1aceb9 706    *
c719f3 707    * @return string Valid application URL
T 708    */
709   public function url($p)
710   {
88fb56 711     if (!is_array($p)) {
TB 712       if (strpos($p, 'http') === 0)
713         return $p;
714
fde466 715       $p = array('_action' => @func_get_arg(0));
88fb56 716     }
b62a0d 717
1c932d 718     $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
cc97ea 719     $p['_task'] = $task;
1038a6 720     unset($p['task']);
A 721
cf1777 722     $url = './';
T 723     $delm = '?';
77406b 724     foreach (array_reverse($p) as $key => $val) {
a7321e 725       if ($val !== '' && $val !== null) {
cc97ea 726         $par = $key[0] == '_' ? $key : '_'.$key;
cf1777 727         $url .= $delm.urlencode($par).'='.urlencode($val);
T 728         $delm = '&';
729       }
730     }
c719f3 731     return $url;
T 732   }
cefd1d 733
T 734
735   /**
0c2596 736    * Function to be executed in script shutdown
0501b6 737    */
0c2596 738   public function shutdown()
0501b6 739   {
0c2596 740     parent::shutdown();
0501b6 741
0c2596 742     foreach ($this->address_books as $book) {
A 743       if (is_object($book) && is_a($book, 'rcube_addressbook'))
744         $book->close();
0501b6 745     }
T 746
0c2596 747     // before closing the database connection, write session data
A 748     if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
42de33 749       $this->session->write_close();
0c2596 750     }
f53750 751
0c2596 752     // write performance stats to logs/console
A 753     if ($this->config->get('devel_mode')) {
754       if (function_exists('memory_get_usage'))
1aceb9 755         $mem = $this->show_bytes(memory_get_usage());
0c2596 756       if (function_exists('memory_get_peak_usage'))
1aceb9 757         $mem .= '/'.$this->show_bytes(memory_get_peak_usage());
0c2596 758
A 759       $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
760       if (defined('RCMAIL_START'))
761         self::print_timer(RCMAIL_START, $log);
762       else
763         self::console($log);
764     }
cefd1d 765   }
68d2d5 766
77de23 767
68d2d5 768   /**
A 769    * Registers action aliases for current task
770    *
771    * @param array $map Alias-to-filename hash array
772    */
773   public function register_action_map($map)
774   {
775     if (is_array($map)) {
776       foreach ($map as $idx => $val) {
777         $this->action_map[$idx] = $val;
778       }
779     }
780   }
f53750 781
77de23 782
68d2d5 783   /**
A 784    * Returns current action filename
785    *
786    * @param array $map Alias-to-filename hash array
787    */
788   public function get_action_file()
789   {
790     if (!empty($this->action_map[$this->action])) {
791       return $this->action_map[$this->action];
792     }
793
794     return strtr($this->action, '-', '_') . '.inc';
795   }
796
77de23 797
d08333 798   /**
A 799    * Fixes some user preferences according to namespace handling change.
800    * Old Roundcube versions were using folder names with removed namespace prefix.
801    * Now we need to add the prefix on servers where personal namespace has prefix.
802    *
803    * @param rcube_user $user User object
804    */
805   private function fix_namespace_settings($user)
806   {
c321a9 807     $prefix     = $this->storage->get_namespace('prefix');
d08333 808     $prefix_len = strlen($prefix);
A 809
810     if (!$prefix_len)
811       return;
812
f53750 813     $prefs = $this->config->all();
A 814     if (!empty($prefs['namespace_fixed']))
d08333 815       return;
A 816
817     // Build namespace prefix regexp
c321a9 818     $ns     = $this->storage->get_namespace();
d08333 819     $regexp = array();
A 820
821     foreach ($ns as $entry) {
822       if (!empty($entry)) {
823         foreach ($entry as $item) {
824           if (strlen($item[0])) {
825             $regexp[] = preg_quote($item[0], '/');
826           }
827         }
828       }
829     }
830     $regexp = '/^('. implode('|', $regexp).')/';
831
832     // Fix preferences
833     $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
834     foreach ($opts as $opt) {
835       if ($value = $prefs[$opt]) {
836         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
837           $prefs[$opt] = $prefix.$value;
838         }
839       }
840     }
841
c321a9 842     if (!empty($prefs['default_folders'])) {
T 843       foreach ($prefs['default_folders'] as $idx => $name) {
d08333 844         if ($name != 'INBOX' && !preg_match($regexp, $name)) {
c321a9 845           $prefs['default_folders'][$idx] = $prefix.$name;
d08333 846         }
A 847       }
848     }
849
850     if (!empty($prefs['search_mods'])) {
851       $folders = array();
852       foreach ($prefs['search_mods'] as $idx => $value) {
853         if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
854           $idx = $prefix.$idx;
855         }
856         $folders[$idx] = $value;
857       }
858       $prefs['search_mods'] = $folders;
859     }
860
861     if (!empty($prefs['message_threading'])) {
862       $folders = array();
863       foreach ($prefs['message_threading'] as $idx => $value) {
864         if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
865           $idx = $prefix.$idx;
866         }
867         $folders[$prefix.$idx] = $value;
868       }
869       $prefs['message_threading'] = $folders;
870     }
871
872     if (!empty($prefs['collapsed_folders'])) {
873       $folders     = explode('&&', $prefs['collapsed_folders']);
874       $count       = count($folders);
875       $folders_str = '';
876
877       if ($count) {
878           $folders[0]        = substr($folders[0], 1);
879           $folders[$count-1] = substr($folders[$count-1], 0, -1);
880       }
881
882       foreach ($folders as $value) {
883         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
884           $value = $prefix.$value;
885         }
886         $folders_str .= '&'.$value.'&';
887       }
888       $prefs['collapsed_folders'] = $folders_str;
889     }
890
891     $prefs['namespace_fixed'] = true;
892
893     // save updated preferences and reset imap settings (default folders)
894     $user->save_prefs($prefs);
c321a9 895     $this->set_storage_prop();
d08333 896   }
A 897
0c2596 898
A 899     /**
900      * Overwrite action variable
901      *
902      * @param string New action value
903      */
904     public function overwrite_action($action)
905     {
906         $this->action = $action;
907         $this->output->set_env('action', $action);
908     }
909
910
911     /**
912      * Returns RFC2822 formatted current date in user's timezone
913      *
914      * @return string Date
915      */
916     public function user_date()
917     {
918         // get user's timezone
919         try {
920             $tz   = new DateTimeZone($this->config->get('timezone'));
921             $date = new DateTime('now', $tz);
922         }
923         catch (Exception $e) {
924             $date = new DateTime();
925         }
926
927         return $date->format('r');
928     }
929
930
931     /**
932      * Write login data (name, ID, IP address) to the 'userlogins' log file.
933      */
934     public function log_login()
935     {
936         if (!$this->config->get('log_logins')) {
937             return;
938         }
939
940         $user_name = $this->get_user_name();
941         $user_id   = $this->get_user_id();
942
943         if (!$user_id) {
944             return;
945         }
946
947         self::write_log('userlogins',
948             sprintf('Successful login for %s (ID: %d) from %s in session %s',
1aceb9 949                 $user_name, $user_id, rcube_utils::remote_ip(), session_id()));
0c2596 950     }
A 951
1aceb9 952
A 953     /**
954      * Create a HTML table based on the given data
955      *
956      * @param  array  Named table attributes
957      * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
958      * @param  array  List of cols to show
959      * @param  string Name of the identifier col
960      *
961      * @return string HTML table code
962      */
963     public function table_output($attrib, $table_data, $a_show_cols, $id_col)
964     {
517dae 965         $table = new html_table($attrib);
1aceb9 966
A 967         // add table header
968         if (!$attrib['noheader']) {
969             foreach ($a_show_cols as $col) {
970                 $table->add_header($col, $this->Q($this->gettext($col)));
971             }
972         }
973
974         if (!is_array($table_data)) {
975             $db = $this->get_dbh();
976             while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
977                 $table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
978
979                 // format each col
980                 foreach ($a_show_cols as $col) {
981                     $table->add($col, $this->Q($sql_arr[$col]));
982                 }
983             }
984         }
985         else {
986             foreach ($table_data as $row_data) {
987                 $class = !empty($row_data['class']) ? $row_data['class'] : '';
988                 $rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
989
990                 $table->add_row(array('id' => $rowid, 'class' => $class));
991
992                 // format each col
993                 foreach ($a_show_cols as $col) {
994                     $table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
995                 }
996             }
997         }
998
999         return $table->show($attrib);
1000     }
1001
1002
1003     /**
1004      * Convert the given date to a human readable form
1005      * This uses the date formatting properties from config
1006      *
1007      * @param mixed  Date representation (string, timestamp or DateTime object)
1008      * @param string Date format to use
1009      * @param bool   Enables date convertion according to user timezone
1010      *
1011      * @return string Formatted date string
1012      */
1013     public function format_date($date, $format = null, $convert = true)
1014     {
1015         if (is_object($date) && is_a($date, 'DateTime')) {
1016             $timestamp = $date->format('U');
1017         }
1018         else {
1019             if (!empty($date)) {
6075f0 1020                 $timestamp = rcube_utils::strtotime($date);
1aceb9 1021             }
A 1022
1023             if (empty($timestamp)) {
1024                 return '';
1025             }
1026
1027             try {
1028                 $date = new DateTime("@".$timestamp);
1029             }
1030             catch (Exception $e) {
1031                 return '';
1032             }
1033         }
1034
1035         if ($convert) {
1036             try {
1037                 // convert to the right timezone
1038                 $stz = date_default_timezone_get();
1039                 $tz = new DateTimeZone($this->config->get('timezone'));
1040                 $date->setTimezone($tz);
1041                 date_default_timezone_set($tz->getName());
1042
1043                 $timestamp = $date->format('U');
1044             }
1045             catch (Exception $e) {
1046             }
1047         }
1048
1049         // define date format depending on current time
1050         if (!$format) {
1051             $now         = time();
1052             $now_date    = getdate($now);
1053             $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1054             $week_limit  = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1055             $pretty_date = $this->config->get('prettydate');
1056
1057             if ($pretty_date && $timestamp > $today_limit && $timestamp < $now) {
1058                 $format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
1059                 $today  = true;
1060             }
1061             else if ($pretty_date && $timestamp > $week_limit && $timestamp < $now) {
1062                 $format = $this->config->get('date_short', 'D H:i');
1063             }
1064             else {
1065                 $format = $this->config->get('date_long', 'Y-m-d H:i');
1066             }
1067         }
1068
1069         // strftime() format
1070         if (preg_match('/%[a-z]+/i', $format)) {
1071             $format = strftime($format, $timestamp);
1072             if ($stz) {
1073                 date_default_timezone_set($stz);
1074             }
1075             return $today ? ($this->gettext('today') . ' ' . $format) : $format;
1076         }
1077
1078         // parse format string manually in order to provide localized weekday and month names
1079         // an alternative would be to convert the date() format string to fit with strftime()
1080         $out = '';
1081         for ($i=0; $i<strlen($format); $i++) {
1082             if ($format[$i] == "\\") {  // skip escape chars
1083                 continue;
1084             }
1085
1086             // write char "as-is"
1087             if ($format[$i] == ' ' || $format[$i-1] == "\\") {
1088                 $out .= $format[$i];
1089             }
1090             // weekday (short)
1091             else if ($format[$i] == 'D') {
1092                 $out .= $this->gettext(strtolower(date('D', $timestamp)));
1093             }
1094             // weekday long
1095             else if ($format[$i] == 'l') {
1096                 $out .= $this->gettext(strtolower(date('l', $timestamp)));
1097             }
1098             // month name (short)
1099             else if ($format[$i] == 'M') {
1100                 $out .= $this->gettext(strtolower(date('M', $timestamp)));
1101             }
1102             // month name (long)
1103             else if ($format[$i] == 'F') {
1104                 $out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
1105             }
1106             else if ($format[$i] == 'x') {
1107                 $out .= strftime('%x %X', $timestamp);
1108             }
1109             else {
1110                 $out .= date($format[$i], $timestamp);
1111             }
1112         }
1113
1114         if ($today) {
1115             $label = $this->gettext('today');
1116             // replcae $ character with "Today" label (#1486120)
1117             if (strpos($out, '$') !== false) {
1118                 $out = preg_replace('/\$/', $label, $out, 1);
1119             }
1120             else {
1121                 $out = $label . ' ' . $out;
1122             }
1123         }
1124
1125         if ($stz) {
1126             date_default_timezone_set($stz);
1127         }
1128
1129         return $out;
1130     }
1131
1132
1133     /**
1134      * Return folders list in HTML
1135      *
1136      * @param array $attrib Named parameters
1137      *
1138      * @return string HTML code for the gui object
1139      */
1140     public function folder_list($attrib)
1141     {
1142         static $a_mailboxes;
1143
1144         $attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
1145
1146         $rcmail  = rcmail::get_instance();
1147         $storage = $rcmail->get_storage();
1148
1149         // add some labels to client
1150         $rcmail->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1151
1152         $type = $attrib['type'] ? $attrib['type'] : 'ul';
1153         unset($attrib['type']);
1154
1155         if ($type == 'ul' && !$attrib['id']) {
1156             $attrib['id'] = 'rcmboxlist';
1157         }
1158
1159         if (empty($attrib['folder_name'])) {
1160             $attrib['folder_name'] = '*';
1161         }
1162
1163         // get current folder
1164         $mbox_name = $storage->get_folder();
1165
1166         // build the folders tree
1167         if (empty($a_mailboxes)) {
1168             // get mailbox list
1169             $a_folders = $storage->list_folders_subscribed(
1170                 '', $attrib['folder_name'], $attrib['folder_filter']);
1171             $delimiter = $storage->get_hierarchy_delimiter();
1172             $a_mailboxes = array();
1173
1174             foreach ($a_folders as $folder) {
1175                 $rcmail->build_folder_tree($a_mailboxes, $folder, $delimiter);
1176             }
1177         }
1178
1179         // allow plugins to alter the folder tree or to localize folder names
1180         $hook = $rcmail->plugins->exec_hook('render_mailboxlist', array(
1181             'list'      => $a_mailboxes,
1182             'delimiter' => $delimiter,
1183             'type'      => $type,
1184             'attribs'   => $attrib,
1185         ));
1186
1187         $a_mailboxes = $hook['list'];
1188         $attrib      = $hook['attribs'];
1189
1190         if ($type == 'select') {
0a1dd5 1191             $attrib['is_escaped'] = true;
1aceb9 1192             $select = new html_select($attrib);
A 1193
1194             // add no-selection option
1195             if ($attrib['noselection']) {
0a1dd5 1196                 $select->add(html::quote($rcmail->gettext($attrib['noselection'])), '');
1aceb9 1197             }
A 1198
1199             $rcmail->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1200             $out = $select->show($attrib['default']);
1201         }
1202         else {
1203             $js_mailboxlist = array();
1204             $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1205
3c309a 1206             $rcmail->output->include_script('treelist.js');
1aceb9 1207             $rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
A 1208             $rcmail->output->set_env('mailboxes', $js_mailboxlist);
1209             $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
1210             $rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders'));
1211         }
1212
1213         return $out;
1214     }
1215
1216
1217     /**
1218      * Return folders list as html_select object
1219      *
1220      * @param array $p  Named parameters
1221      *
1222      * @return html_select HTML drop-down object
1223      */
1224     public function folder_selector($p = array())
1225     {
0a1dd5 1226         $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true);
1aceb9 1227         $a_mailboxes = array();
A 1228         $storage = $this->get_storage();
1229
1230         if (empty($p['folder_name'])) {
1231             $p['folder_name'] = '*';
1232         }
1233
1234         if ($p['unsubscribed']) {
1235             $list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1236         }
1237         else {
1238             $list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1239         }
1240
1241         $delimiter = $storage->get_hierarchy_delimiter();
1242
1243         foreach ($list as $folder) {
1244             if (empty($p['exceptions']) || !in_array($folder, $p['exceptions'])) {
1245                 $this->build_folder_tree($a_mailboxes, $folder, $delimiter);
1246             }
1247         }
1248
1249         $select = new html_select($p);
1250
1251         if ($p['noselection']) {
0a1dd5 1252             $select->add(html::quote($p['noselection']), '');
1aceb9 1253         }
A 1254
1255         $this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
1256
1257         return $select;
1258     }
1259
1260
1261     /**
1262      * Create a hierarchical array of the mailbox list
1263      */
1264     public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
1265     {
1266         // Handle namespace prefix
1267         $prefix = '';
1268         if (!$path) {
1269             $n_folder = $folder;
1270             $folder = $this->storage->mod_folder($folder);
1271
1272             if ($n_folder != $folder) {
1273                 $prefix = substr($n_folder, 0, -strlen($folder));
1274             }
1275         }
1276
1277         $pos = strpos($folder, $delm);
1278
1279         if ($pos !== false) {
1280             $subFolders    = substr($folder, $pos+1);
1281             $currentFolder = substr($folder, 0, $pos);
1282
1283             // sometimes folder has a delimiter as the last character
1284             if (!strlen($subFolders)) {
1285                 $virtual = false;
1286             }
1287             else if (!isset($arrFolders[$currentFolder])) {
1288                 $virtual = true;
1289             }
1290             else {
1291                 $virtual = $arrFolders[$currentFolder]['virtual'];
1292             }
1293         }
1294         else {
1295             $subFolders    = false;
1296             $currentFolder = $folder;
1297             $virtual       = false;
1298         }
1299
1300         $path .= $prefix . $currentFolder;
1301
1302         if (!isset($arrFolders[$currentFolder])) {
1303             $arrFolders[$currentFolder] = array(
1304                 'id' => $path,
1305                 'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
1306                 'virtual' => $virtual,
1307                 'folders' => array());
1308         }
1309         else {
1310             $arrFolders[$currentFolder]['virtual'] = $virtual;
1311         }
1312
1313         if (strlen($subFolders)) {
1314             $this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1315         }
1316     }
1317
1318
1319     /**
1320      * Return html for a structured list &lt;ul&gt; for the mailbox tree
1321      */
1322     public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
1323     {
1324         $maxlength = intval($attrib['maxlength']);
1325         $realnames = (bool)$attrib['realnames'];
1326         $msgcounts = $this->storage->get_cache('messagecount');
1327         $collapsed = $this->config->get('collapsed_folders');
1328
1329         $out = '';
3725cf 1330         foreach ($arrFolders as $folder) {
1aceb9 1331             $title        = null;
A 1332             $folder_class = $this->folder_classname($folder['id']);
1333             $is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
1334             $unread       = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1335
1336             if ($folder_class && !$realnames) {
1337                 $foldername = $this->gettext($folder_class);
1338             }
1339             else {
1340                 $foldername = $folder['name'];
1341
1342                 // shorten the folder name to a given length
1343                 if ($maxlength && $maxlength > 1) {
1344                     $fname = abbreviate_string($foldername, $maxlength);
1345                     if ($fname != $foldername) {
1346                         $title = $foldername;
1347                     }
1348                     $foldername = $fname;
1349                 }
1350             }
1351
1352             // make folder name safe for ids and class names
1353             $folder_id = rcube_utils::html_identifier($folder['id'], true);
1354             $classes   = array('mailbox');
1355
1356             // set special class for Sent, Drafts, Trash and Junk
1357             if ($folder_class) {
1358                 $classes[] = $folder_class;
1359             }
1360
1361             if ($folder['id'] == $mbox_name) {
1362                 $classes[] = 'selected';
1363             }
1364
1365             if ($folder['virtual']) {
1366                 $classes[] = 'virtual';
1367             }
1368             else if ($unread) {
1369                 $classes[] = 'unread';
1370             }
1371
1372             $js_name = $this->JQ($folder['id']);
1373             $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
1374             $link_attrib = $folder['virtual'] ? array() : array(
1375                 'href' => $this->url(array('_mbox' => $folder['id'])),
60226a 1376                 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::JS_OBJECT_NAME, $js_name),
1aceb9 1377                 'rel' => $folder['id'],
A 1378                 'title' => $title,
1379             );
1380
1381             $out .= html::tag('li', array(
1382                 'id' => "rcmli".$folder_id,
1383                 'class' => join(' ', $classes),
1384                 'noclose' => true),
3c309a 1385                 html::a($link_attrib, $html_name));
1aceb9 1386
3c309a 1387             if (!empty($folder['folders'])) {
TB 1388                 $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
1389             }
1390
1391             $jslist[$folder['id']] = array(
1aceb9 1392                 'id'      => $folder['id'],
A 1393                 'name'    => $foldername,
1394                 'virtual' => $folder['virtual']
1395             );
1396
1397             if (!empty($folder['folders'])) {
1398                 $out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
1399                     $this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1400             }
1401
1402             $out .= "</li>\n";
1403         }
1404
1405         return $out;
1406     }
1407
1408
1409     /**
1410      * Return html for a flat list <select> for the mailbox tree
1411      */
1412     public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
1413     {
1414         $out = '';
1415
3725cf 1416         foreach ($arrFolders as $folder) {
1aceb9 1417             // skip exceptions (and its subfolders)
A 1418             if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
1419                 continue;
1420             }
1421
1422             // skip folders in which it isn't possible to create subfolders
1423             if (!empty($opts['skip_noinferiors'])) {
1424                 $attrs = $this->storage->folder_attributes($folder['id']);
1425                 if ($attrs && in_array('\\Noinferiors', $attrs)) {
1426                     continue;
1427                 }
1428             }
1429
1430             if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
1431                 $foldername = $this->gettext($folder_class);
1432             }
1433             else {
1434                 $foldername = $folder['name'];
1435
1436                 // shorten the folder name to a given length
1437                 if ($maxlength && $maxlength > 1) {
1438                     $foldername = abbreviate_string($foldername, $maxlength);
1439                 }
e7ca04 1440             }
1aceb9 1441
0a1dd5 1442             $select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
1aceb9 1443
e7ca04 1444             if (!empty($folder['folders'])) {
A 1445                 $out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
1446                     $select, $realnames, $nestLevel+1, $opts);
1aceb9 1447             }
A 1448         }
1449
1450         return $out;
1451     }
1452
1453
1454     /**
1455      * Return internal name for the given folder if it matches the configured special folders
1456      */
1457     public function folder_classname($folder_id)
1458     {
1459         if ($folder_id == 'INBOX') {
1460             return 'inbox';
1461         }
1462
1463         // for these mailboxes we have localized labels and css classes
1464         foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1465         {
1466             if ($folder_id === $this->config->get($smbx.'_mbox')) {
1467                 return $smbx;
1468             }
1469         }
1470     }
1471
1472
1473     /**
1474      * Try to localize the given IMAP folder name.
1475      * UTF-7 decode it in case no localized text was found
1476      *
4d7964 1477      * @param string $name      Folder name
AM 1478      * @param bool   $with_path Enable path localization
1aceb9 1479      *
A 1480      * @return string Localized folder name in UTF-8 encoding
1481      */
4d7964 1482     public function localize_foldername($name, $with_path = true)
1aceb9 1483     {
4d7964 1484         // try to localize path of the folder
AM 1485         if ($with_path) {
1486             $storage   = $this->get_storage();
1487             $delimiter = $storage->get_hierarchy_delimiter();
1488             $path      = explode($delimiter, $name);
1489             $count     = count($path);
1490
1491             if ($count > 1) {
61be82 1492                 for ($i = 0; $i < $count; $i++) {
4d7964 1493                     $folder = implode($delimiter, array_slice($path, 0, -$i));
AM 1494                     if ($folder_class = $this->folder_classname($folder)) {
1495                         $name = implode($delimiter, array_slice($path, $count - $i));
1496                         return $this->gettext($folder_class) . $delimiter . rcube_charset::convert($name, 'UTF7-IMAP');
1497                     }
1498                 }
1499             }
1500         }
1501
1aceb9 1502         if ($folder_class = $this->folder_classname($name)) {
A 1503             return $this->gettext($folder_class);
1504         }
1505         else {
1506             return rcube_charset::convert($name, 'UTF7-IMAP');
1507         }
1508     }
1509
1510
1511     public function localize_folderpath($path)
1512     {
1513         $protect_folders = $this->config->get('protect_default_folders');
1514         $default_folders = (array) $this->config->get('default_folders');
1515         $delimiter       = $this->storage->get_hierarchy_delimiter();
1516         $path            = explode($delimiter, $path);
1517         $result          = array();
1518
1519         foreach ($path as $idx => $dir) {
1520             $directory = implode($delimiter, array_slice($path, 0, $idx+1));
1521             if ($protect_folders && in_array($directory, $default_folders)) {
1522                 unset($result);
1523                 $result[] = $this->localize_foldername($directory);
1524             }
1525             else {
1526                 $result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
1527             }
1528         }
1529
1530         return implode($delimiter, $result);
1531     }
1532
1533
1534     public static function quota_display($attrib)
1535     {
1536         $rcmail = rcmail::get_instance();
1537
1538         if (!$attrib['id']) {
1539             $attrib['id'] = 'rcmquotadisplay';
1540         }
1541
1542         $_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
1543
1544         $rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
1545
1546         $quota = $rcmail->quota_content($attrib);
1547
1548         $rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
1549
1550         return html::span($attrib, '');
1551     }
1552
1553
1554     public function quota_content($attrib = null)
1555     {
1556         $quota = $this->storage->get_quota();
1557         $quota = $this->plugins->exec_hook('quota', $quota);
1558
1559         $quota_result = (array) $quota;
1560         $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1561
4fee77 1562         if ($quota['total'] > 0) {
1aceb9 1563             if (!isset($quota['percent'])) {
A 1564                 $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1565             }
1566
1567             $title = sprintf('%s / %s (%.0f%%)',
1568                 $this->show_bytes($quota['used'] * 1024), $this->show_bytes($quota['total'] * 1024),
1569                 $quota_result['percent']);
1570
1571             $quota_result['title'] = $title;
1572
1573             if ($attrib['width']) {
1574                 $quota_result['width'] = $attrib['width'];
1575             }
1576             if ($attrib['height']) {
1577                 $quota_result['height']    = $attrib['height'];
1578             }
1579         }
1580         else {
4fee77 1581             $unlimited               = $this->config->get('quota_zero_as_unlimited');
AM 1582             $quota_result['title']   = $this->gettext($unlimited ? 'unlimited' : 'unknown');
1aceb9 1583             $quota_result['percent'] = 0;
A 1584         }
1585
1586         return $quota_result;
1587     }
1588
1589
1590     /**
1591      * Outputs error message according to server error/response codes
1592      *
1593      * @param string $fallback       Fallback message label
1594      * @param array  $fallback_args  Fallback message label arguments
e7c1aa 1595      * @param string $suffix         Message label suffix
1aceb9 1596      */
e7c1aa 1597     public function display_server_error($fallback = null, $fallback_args = null, $suffix = '')
1aceb9 1598     {
A 1599         $err_code = $this->storage->get_error_code();
1600         $res_code = $this->storage->get_response_code();
e7c1aa 1601         $args     = array();
1aceb9 1602
524e48 1603         if ($res_code == rcube_storage::NOPERM) {
e7c1aa 1604             $error = 'errornoperm';
1aceb9 1605         }
A 1606         else if ($res_code == rcube_storage::READONLY) {
e7c1aa 1607             $error = 'errorreadonly';
1aceb9 1608         }
0bf724 1609         else if ($res_code == rcube_storage::OVERQUOTA) {
e7c1aa 1610             $error = 'errorroverquota';
0bf724 1611         }
1aceb9 1612         else if ($err_code && ($err_str = $this->storage->get_error_str())) {
A 1613             // try to detect access rights problem and display appropriate message
1614             if (stripos($err_str, 'Permission denied') !== false) {
e7c1aa 1615                 $error = 'errornoperm';
1aceb9 1616             }
0bf724 1617             // try to detect full mailbox problem and display appropriate message
8c93c2 1618             // there can be e.g. "Quota exceeded" or "quotum would exceed"
AM 1619             else if (stripos($err_str, 'quot') !== false && stripos($err_str, 'exceed') !== false) {
e7c1aa 1620                 $error = 'erroroverquota';
0bf724 1621             }
1aceb9 1622             else {
e7c1aa 1623                 $error = 'servererrormsg';
AM 1624                 $args  = array('msg' => $err_str);
1aceb9 1625             }
A 1626         }
524e48 1627         else if ($err_code < 0) {
e7c1aa 1628             $error = 'storageerror';
524e48 1629         }
1aceb9 1630         else if ($fallback) {
e7c1aa 1631             $error = $fallback;
AM 1632             $args  = $fallback_args;
1633         }
1634
1635         if ($error) {
1636             if ($suffix && $this->text_exists($error . $suffix)) {
1637                 $error .= $suffix;
1638             }
1639             $this->output->show_message($error, 'error', $args);
1aceb9 1640         }
A 1641     }
1642
1643
1644     /**
1645      * Output HTML editor scripts
1646      *
1647      * @param string $mode  Editor mode
1648      */
1649     public function html_editor($mode = '')
1650     {
1651         $hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
1652
1653         if ($hook['abort']) {
1654             return;
1655         }
1656
1657         $lang = strtolower($_SESSION['language']);
1658
1659         // TinyMCE uses two-letter lang codes, with exception of Chinese
1660         if (strpos($lang, 'zh_') === 0) {
1661             $lang = str_replace('_', '-', $lang);
1662         }
1663         else {
1664             $lang = substr($lang, 0, 2);
1665         }
1666
1667         if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
1668             $lang = 'en';
1669         }
1670
1671         $script = json_encode(array(
1672             'mode'       => $mode,
1673             'lang'       => $lang,
1674             'skin_path'  => $this->output->get_skin_path(),
1675             'spellcheck' => intval($this->config->get('enable_spellcheck')),
1676             'spelldict'  => intval($this->config->get('spellcheck_dictionary'))
1677         ));
1678
1679         $this->output->include_script('tiny_mce/tiny_mce.js');
1680         $this->output->include_script('editor.js');
1681         $this->output->add_script("rcmail_editor_init($script)", 'docready');
1682     }
1683
1684
1685     /**
1686      * Replaces TinyMCE's emoticon images with plain-text representation
1687      *
1688      * @param string $html  HTML content
1689      *
1690      * @return string HTML content
1691      */
1692     public static function replace_emoticons($html)
1693     {
1694         $emoticons = array(
1695             '8-)' => 'smiley-cool',
1696             ':-#' => 'smiley-foot-in-mouth',
1697             ':-*' => 'smiley-kiss',
1698             ':-X' => 'smiley-sealed',
1699             ':-P' => 'smiley-tongue-out',
1700             ':-@' => 'smiley-yell',
1701             ":'(" => 'smiley-cry',
1702             ':-(' => 'smiley-frown',
1703             ':-D' => 'smiley-laughing',
1704             ':-)' => 'smiley-smile',
1705             ':-S' => 'smiley-undecided',
1706             ':-$' => 'smiley-embarassed',
1707             'O:-)' => 'smiley-innocent',
1708             ':-|' => 'smiley-money-mouth',
1709             ':-O' => 'smiley-surprised',
1710             ';-)' => 'smiley-wink',
1711         );
1712
1713         foreach ($emoticons as $idx => $file) {
1714             // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1715             $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1716             $replace[] = $idx;
1717         }
1718
1719         return preg_replace($search, $replace, $html);
1720     }
1721
1722
1723     /**
1724      * File upload progress handler.
1725      */
1726     public function upload_progress()
1727     {
1728         $prefix = ini_get('apc.rfc1867_prefix');
1729         $params = array(
1730             'action' => $this->action,
1731             'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET),
1732         );
1733
1734         if (function_exists('apc_fetch')) {
1735             $status = apc_fetch($prefix . $params['name']);
1736
1737             if (!empty($status)) {
1738                 $status['percent'] = round($status['current']/$status['total']*100);
1739                 $params = array_merge($status, $params);
1740             }
1741         }
1742
1743         if (isset($params['percent']))
1744             $params['text'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array(
1745                 'percent' => $params['percent'] . '%',
1746                 'current' => $this->show_bytes($params['current']),
1747                 'total'   => $this->show_bytes($params['total'])
1748         )));
1749
1750         $this->output->command('upload_progress_update', $params);
1751         $this->output->send();
1752     }
1753
1754
1755     /**
1756      * Initializes file uploading interface.
1757      */
1758     public function upload_init()
1759     {
1760         // Enable upload progress bar
1761         if (($seconds = $this->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
1762             if ($field_name = ini_get('apc.rfc1867_name')) {
1763                 $this->output->set_env('upload_progress_name', $field_name);
1764                 $this->output->set_env('upload_progress_time', (int) $seconds);
1765             }
1766         }
1767
1768         // find max filesize value
1769         $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
1770         $max_postsize = parse_bytes(ini_get('post_max_size'));
1771         if ($max_postsize && $max_postsize < $max_filesize) {
1772             $max_filesize = $max_postsize;
1773         }
1774
1775         $this->output->set_env('max_filesize', $max_filesize);
1776         $max_filesize = self::show_bytes($max_filesize);
1777         $this->output->set_env('filesizeerror', $this->gettext(array(
1778             'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
1779
1780         return $max_filesize;
1781     }
1782
1783
1784     /**
1785      * Initializes client-side autocompletion.
1786      */
1787     public function autocomplete_init()
1788     {
1789         static $init;
1790
1791         if ($init) {
1792             return;
1793         }
1794
1795         $init = 1;
1796
1797         if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
1798             $book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
1799             if (count($book_types) > 1) {
1800                 $this->output->set_env('autocomplete_threads', $threads);
1801                 $this->output->set_env('autocomplete_sources', $book_types);
1802             }
1803         }
1804
1805         $this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
1806         $this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
1807         $this->output->add_label('autocompletechars', 'autocompletemore');
1808     }
1809
1810
1811     /**
1812      * Returns supported font-family specifications
1813      *
1814      * @param string $font  Font name
1815      *
1816      * @param string|array Font-family specification array or string (if $font is used)
1817      */
1818     public static function font_defs($font = null)
1819     {
1820         $fonts = array(
1821             'Andale Mono'   => '"Andale Mono",Times,monospace',
1822             'Arial'         => 'Arial,Helvetica,sans-serif',
1823             'Arial Black'   => '"Arial Black","Avant Garde",sans-serif',
1824             'Book Antiqua'  => '"Book Antiqua",Palatino,serif',
1825             'Courier New'   => '"Courier New",Courier,monospace',
1826             'Georgia'       => 'Georgia,Palatino,serif',
1827             'Helvetica'     => 'Helvetica,Arial,sans-serif',
1828             'Impact'        => 'Impact,Chicago,sans-serif',
1829             'Tahoma'        => 'Tahoma,Arial,Helvetica,sans-serif',
1830             'Terminal'      => 'Terminal,Monaco,monospace',
1831             'Times New Roman' => '"Times New Roman",Times,serif',
1832             'Trebuchet MS'  => '"Trebuchet MS",Geneva,sans-serif',
1833             'Verdana'       => 'Verdana,Geneva,sans-serif',
1834         );
1835
1836         if ($font) {
1837             return $fonts[$font];
1838         }
1839
1840         return $fonts;
1841     }
1842
1843
1844     /**
1845      * Create a human readable string for a number of bytes
1846      *
1847      * @param int Number of bytes
1848      *
1849      * @return string Byte string
1850      */
1851     public function show_bytes($bytes)
1852     {
1853         if ($bytes >= 1073741824) {
1854             $gb  = $bytes/1073741824;
1855             $str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->gettext('GB');
1856         }
1857         else if ($bytes >= 1048576) {
1858             $mb  = $bytes/1048576;
1859             $str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->gettext('MB');
1860         }
1861         else if ($bytes >= 1024) {
1862             $str = sprintf("%d ",  round($bytes/1024)) . $this->gettext('KB');
1863         }
1864         else {
1865             $str = sprintf('%d ', $bytes) . $this->gettext('B');
1866         }
1867
1868         return $str;
1869     }
1870
1871
1872     /**
8749e9 1873      * Returns real size (calculated) of the message part
AM 1874      *
1875      * @param rcube_message_part  Message part
1876      *
1877      * @return string Part size (and unit)
1878      */
1879     public function message_part_size($part)
1880     {
1881         if (isset($part->d_parameters['size'])) {
1882             $size = $this->show_bytes((int)$part->d_parameters['size']);
1883         }
1884         else {
1885           $size = $part->size;
1886           if ($part->encoding == 'base64') {
1887             $size = $size / 1.33;
1888           }
1889
1890           $size = '~' . $this->show_bytes($size);
1891         }
1892
1893         return $size;
1894     }
1895
1896
1aceb9 1897     /************************************************************************
A 1898      *********          Deprecated methods (to be removed)          *********
1899      ***********************************************************************/
1900
1901     public static function setcookie($name, $value, $exp = 0)
1902     {
1903         rcube_utils::setcookie($name, $value, $exp);
1904     }
38a08c 1905
AM 1906     public function imap_connect()
1907     {
1908         return $this->storage_connect();
1909     }
5a575b 1910
b97f21 1911     public function imap_init()
TB 1912     {
1913         return $this->storage_init();
1914     }
1915
5a575b 1916     /**
AM 1917      * Connect to the mail storage server with stored session data
1918      *
1919      * @return bool True on success, False on error
1920      */
1921     public function storage_connect()
1922     {
1923         $storage = $this->get_storage();
1924
1925         if ($_SESSION['storage_host'] && !$storage->is_connected()) {
1926             $host = $_SESSION['storage_host'];
1927             $user = $_SESSION['username'];
1928             $port = $_SESSION['storage_port'];
1929             $ssl  = $_SESSION['storage_ssl'];
1930             $pass = $this->decrypt($_SESSION['password']);
1931
1932             if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
1933                 if (is_object($this->output)) {
7ea292 1934                     $this->output->show_message('storageerror', 'error');
5a575b 1935                 }
AM 1936             }
1937             else {
1938                 $this->set_storage_prop();
1939             }
1940         }
1941
5f6c71 1942         return $storage->is_connected();
5a575b 1943     }
197601 1944 }