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