Aleksander Machniak
2014-04-04 adaddf0b188eca228d626973f193b2a51de2dbe7
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')) {
842             if (function_exists('memory_get_usage'))
843                 $mem = $this->show_bytes(memory_get_usage());
844             if (function_exists('memory_get_peak_usage'))
845                 $mem .= '/'.$this->show_bytes(memory_get_peak_usage());
846
847             $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
848
849             if (defined('RCMAIL_START'))
850                 self::print_timer(RCMAIL_START, $log);
851             else
852                 self::console($log);
853         }
854     }
855
856     /**
857      * Registers action aliases for current task
858      *
859      * @param array $map Alias-to-filename hash array
860      */
861     public function register_action_map($map)
862     {
863         if (is_array($map)) {
864             foreach ($map as $idx => $val) {
865                 $this->action_map[$idx] = $val;
866             }
867         }
868     }
869
870     /**
871      * Returns current action filename
872      *
873      * @param array $map Alias-to-filename hash array
874      */
875     public function get_action_file()
876     {
877         if (!empty($this->action_map[$this->action])) {
878             return $this->action_map[$this->action];
879         }
880
881         return strtr($this->action, '-', '_') . '.inc';
882     }
883
884     /**
885      * Fixes some user preferences according to namespace handling change.
886      * Old Roundcube versions were using folder names with removed namespace prefix.
887      * Now we need to add the prefix on servers where personal namespace has prefix.
888      *
889      * @param rcube_user $user User object
890      */
891     private function fix_namespace_settings($user)
892     {
893         $prefix     = $this->storage->get_namespace('prefix');
894         $prefix_len = strlen($prefix);
895
896         if (!$prefix_len)
897             return;
898
899         $prefs = $this->config->all();
900         if (!empty($prefs['namespace_fixed']))
901             return;
902
903         // Build namespace prefix regexp
904         $ns     = $this->storage->get_namespace();
905         $regexp = array();
906
907         foreach ($ns as $entry) {
908             if (!empty($entry)) {
909                 foreach ($entry as $item) {
910                     if (strlen($item[0])) {
911                         $regexp[] = preg_quote($item[0], '/');
912                     }
913                 }
914             }
915         }
916         $regexp = '/^('. implode('|', $regexp).')/';
917
918         // Fix preferences
919         $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
920         foreach ($opts as $opt) {
921             if ($value = $prefs[$opt]) {
922                 if ($value != 'INBOX' && !preg_match($regexp, $value)) {
923                     $prefs[$opt] = $prefix.$value;
924                 }
925             }
926         }
927
928         if (!empty($prefs['search_mods'])) {
929             $folders = array();
930             foreach ($prefs['search_mods'] as $idx => $value) {
931                 if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
932                     $idx = $prefix.$idx;
933                 }
934                 $folders[$idx] = $value;
935             }
936
937             $prefs['search_mods'] = $folders;
938         }
939
940         if (!empty($prefs['message_threading'])) {
941             $folders = array();
942             foreach ($prefs['message_threading'] as $idx => $value) {
943                 if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
944                     $idx = $prefix.$idx;
945                 }
946                 $folders[$prefix.$idx] = $value;
947             }
948
949             $prefs['message_threading'] = $folders;
950         }
951
952         if (!empty($prefs['collapsed_folders'])) {
953             $folders     = explode('&&', $prefs['collapsed_folders']);
954             $count       = count($folders);
955             $folders_str = '';
956
957             if ($count) {
958                 $folders[0]        = substr($folders[0], 1);
959                 $folders[$count-1] = substr($folders[$count-1], 0, -1);
960             }
961
962             foreach ($folders as $value) {
963                 if ($value != 'INBOX' && !preg_match($regexp, $value)) {
964                     $value = $prefix.$value;
965                 }
966                 $folders_str .= '&'.$value.'&';
967             }
968
969             $prefs['collapsed_folders'] = $folders_str;
970         }
971
972         $prefs['namespace_fixed'] = true;
973
974         // save updated preferences and reset imap settings (default folders)
975         $user->save_prefs($prefs);
976         $this->set_storage_prop();
977     }
0c2596 978
A 979     /**
980      * Overwrite action variable
981      *
982      * @param string New action value
983      */
984     public function overwrite_action($action)
985     {
986         $this->action = $action;
987         $this->output->set_env('action', $action);
988     }
989
990     /**
f5d2ee 991      * Set environment variables for specified config options
AM 992      */
993     public function set_env_config($options)
994     {
995         foreach ((array) $options as $option) {
996             if ($this->config->get($option)) {
997                 $this->output->set_env($option, true);
998             }
999         }
1000     }
1001
1002     /**
0c2596 1003      * Returns RFC2822 formatted current date in user's timezone
A 1004      *
1005      * @return string Date
1006      */
1007     public function user_date()
1008     {
1009         // get user's timezone
1010         try {
1011             $tz   = new DateTimeZone($this->config->get('timezone'));
1012             $date = new DateTime('now', $tz);
1013         }
1014         catch (Exception $e) {
1015             $date = new DateTime();
1016         }
1017
1018         return $date->format('r');
1019     }
1020
1021     /**
1022      * Write login data (name, ID, IP address) to the 'userlogins' log file.
1023      */
0f5574 1024     public function log_login($user = null, $failed_login = false, $error_code = 0)
0c2596 1025     {
A 1026         if (!$this->config->get('log_logins')) {
1027             return;
1028         }
1029
060467 1030         // failed login
AM 1031         if ($failed_login) {
1032             $message = sprintf('Failed login for %s from %s in session %s (error: %d)',
1033                 $user, rcube_utils::remote_ip(), session_id(), $error_code);
1034         }
1035         // successful login
1036         else {
1037             $user_name = $this->get_user_name();
1038             $user_id   = $this->get_user_id();
0c2596 1039
060467 1040             if (!$user_id) {
AM 1041                 return;
1042             }
1043
1044             $message = sprintf('Successful login for %s (ID: %d) from %s in session %s',
0301d9 1045                 $user_name, $user_id, rcube_utils::remote_ip(), session_id());
0c2596 1046         }
A 1047
060467 1048         // log login
AM 1049         self::write_log('userlogins', $message);
0c2596 1050     }
1aceb9 1051
A 1052     /**
1053      * Create a HTML table based on the given data
1054      *
1055      * @param  array  Named table attributes
1056      * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
1057      * @param  array  List of cols to show
1058      * @param  string Name of the identifier col
1059      *
1060      * @return string HTML table code
1061      */
1062     public function table_output($attrib, $table_data, $a_show_cols, $id_col)
1063     {
517dae 1064         $table = new html_table($attrib);
1aceb9 1065
A 1066         // add table header
1067         if (!$attrib['noheader']) {
1068             foreach ($a_show_cols as $col) {
1069                 $table->add_header($col, $this->Q($this->gettext($col)));
1070             }
1071         }
1072
1073         if (!is_array($table_data)) {
1074             $db = $this->get_dbh();
1075             while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
1076                 $table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
1077
1078                 // format each col
1079                 foreach ($a_show_cols as $col) {
1080                     $table->add($col, $this->Q($sql_arr[$col]));
1081                 }
1082             }
1083         }
1084         else {
1085             foreach ($table_data as $row_data) {
1086                 $class = !empty($row_data['class']) ? $row_data['class'] : '';
1087                 $rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
1088
1089                 $table->add_row(array('id' => $rowid, 'class' => $class));
1090
1091                 // format each col
1092                 foreach ($a_show_cols as $col) {
1093                     $table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
1094                 }
1095             }
1096         }
1097
1098         return $table->show($attrib);
1099     }
1100
1101     /**
1102      * Convert the given date to a human readable form
1103      * This uses the date formatting properties from config
1104      *
1105      * @param mixed  Date representation (string, timestamp or DateTime object)
1106      * @param string Date format to use
1107      * @param bool   Enables date convertion according to user timezone
1108      *
1109      * @return string Formatted date string
1110      */
1111     public function format_date($date, $format = null, $convert = true)
1112     {
1113         if (is_object($date) && is_a($date, 'DateTime')) {
1114             $timestamp = $date->format('U');
1115         }
1116         else {
1117             if (!empty($date)) {
6075f0 1118                 $timestamp = rcube_utils::strtotime($date);
1aceb9 1119             }
A 1120
1121             if (empty($timestamp)) {
1122                 return '';
1123             }
1124
1125             try {
1126                 $date = new DateTime("@".$timestamp);
1127             }
1128             catch (Exception $e) {
1129                 return '';
1130             }
1131         }
1132
1133         if ($convert) {
1134             try {
1135                 // convert to the right timezone
1136                 $stz = date_default_timezone_get();
1137                 $tz = new DateTimeZone($this->config->get('timezone'));
1138                 $date->setTimezone($tz);
1139                 date_default_timezone_set($tz->getName());
1140
1141                 $timestamp = $date->format('U');
1142             }
1143             catch (Exception $e) {
1144             }
1145         }
1146
1147         // define date format depending on current time
1148         if (!$format) {
1149             $now         = time();
1150             $now_date    = getdate($now);
1151             $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1152             $week_limit  = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1153             $pretty_date = $this->config->get('prettydate');
1154
2b05c5 1155             if ($pretty_date && $timestamp > $today_limit && $timestamp <= $now) {
1aceb9 1156                 $format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
A 1157                 $today  = true;
1158             }
2b05c5 1159             else if ($pretty_date && $timestamp > $week_limit && $timestamp <= $now) {
1aceb9 1160                 $format = $this->config->get('date_short', 'D H:i');
A 1161             }
1162             else {
1163                 $format = $this->config->get('date_long', 'Y-m-d H:i');
1164             }
1165         }
1166
1167         // strftime() format
1168         if (preg_match('/%[a-z]+/i', $format)) {
1169             $format = strftime($format, $timestamp);
1170             if ($stz) {
1171                 date_default_timezone_set($stz);
1172             }
1173             return $today ? ($this->gettext('today') . ' ' . $format) : $format;
1174         }
1175
1176         // parse format string manually in order to provide localized weekday and month names
1177         // an alternative would be to convert the date() format string to fit with strftime()
1178         $out = '';
1179         for ($i=0; $i<strlen($format); $i++) {
1180             if ($format[$i] == "\\") {  // skip escape chars
1181                 continue;
1182             }
1183
1184             // write char "as-is"
1185             if ($format[$i] == ' ' || $format[$i-1] == "\\") {
1186                 $out .= $format[$i];
1187             }
1188             // weekday (short)
1189             else if ($format[$i] == 'D') {
1190                 $out .= $this->gettext(strtolower(date('D', $timestamp)));
1191             }
1192             // weekday long
1193             else if ($format[$i] == 'l') {
1194                 $out .= $this->gettext(strtolower(date('l', $timestamp)));
1195             }
1196             // month name (short)
1197             else if ($format[$i] == 'M') {
1198                 $out .= $this->gettext(strtolower(date('M', $timestamp)));
1199             }
1200             // month name (long)
1201             else if ($format[$i] == 'F') {
1202                 $out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
1203             }
1204             else if ($format[$i] == 'x') {
1205                 $out .= strftime('%x %X', $timestamp);
1206             }
1207             else {
1208                 $out .= date($format[$i], $timestamp);
1209             }
1210         }
1211
1212         if ($today) {
1213             $label = $this->gettext('today');
1214             // replcae $ character with "Today" label (#1486120)
1215             if (strpos($out, '$') !== false) {
1216                 $out = preg_replace('/\$/', $label, $out, 1);
1217             }
1218             else {
1219                 $out = $label . ' ' . $out;
1220             }
1221         }
1222
1223         if ($stz) {
1224             date_default_timezone_set($stz);
1225         }
1226
1227         return $out;
1228     }
1229
1230     /**
1231      * Return folders list in HTML
1232      *
1233      * @param array $attrib Named parameters
1234      *
1235      * @return string HTML code for the gui object
1236      */
1237     public function folder_list($attrib)
1238     {
1239         static $a_mailboxes;
1240
1241         $attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
1242
1243         $rcmail  = rcmail::get_instance();
1244         $storage = $rcmail->get_storage();
1245
1246         // add some labels to client
1247         $rcmail->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1248
1249         $type = $attrib['type'] ? $attrib['type'] : 'ul';
1250         unset($attrib['type']);
1251
1252         if ($type == 'ul' && !$attrib['id']) {
1253             $attrib['id'] = 'rcmboxlist';
1254         }
1255
1256         if (empty($attrib['folder_name'])) {
1257             $attrib['folder_name'] = '*';
1258         }
1259
1260         // get current folder
1261         $mbox_name = $storage->get_folder();
1262
1263         // build the folders tree
1264         if (empty($a_mailboxes)) {
1265             // get mailbox list
1266             $a_folders = $storage->list_folders_subscribed(
1267                 '', $attrib['folder_name'], $attrib['folder_filter']);
1268             $delimiter = $storage->get_hierarchy_delimiter();
1269             $a_mailboxes = array();
1270
1271             foreach ($a_folders as $folder) {
1272                 $rcmail->build_folder_tree($a_mailboxes, $folder, $delimiter);
1273             }
1274         }
1275
1276         // allow plugins to alter the folder tree or to localize folder names
1277         $hook = $rcmail->plugins->exec_hook('render_mailboxlist', array(
1278             'list'      => $a_mailboxes,
1279             'delimiter' => $delimiter,
1280             'type'      => $type,
1281             'attribs'   => $attrib,
1282         ));
1283
1284         $a_mailboxes = $hook['list'];
1285         $attrib      = $hook['attribs'];
1286
1287         if ($type == 'select') {
0a1dd5 1288             $attrib['is_escaped'] = true;
1aceb9 1289             $select = new html_select($attrib);
A 1290
1291             // add no-selection option
1292             if ($attrib['noselection']) {
0a1dd5 1293                 $select->add(html::quote($rcmail->gettext($attrib['noselection'])), '');
1aceb9 1294             }
A 1295
1296             $rcmail->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1297             $out = $select->show($attrib['default']);
1298         }
1299         else {
1300             $js_mailboxlist = array();
9a0153 1301             $tree = $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
1aceb9 1302
9a0153 1303             if ($type != 'js') {
AM 1304                 $out = html::tag('ul', $attrib, $tree, html::$common_attrib);
1305
1306                 $rcmail->output->include_script('treelist.js');
1307                 $rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
1308                 $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
1309                 $rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders'));
1310             }
1311
1aceb9 1312             $rcmail->output->set_env('mailboxes', $js_mailboxlist);
9a0153 1313
AM 1314             // we can't use object keys in javascript because they are unordered
1315             // we need sorted folders list for folder-selector widget
1316             $rcmail->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
1aceb9 1317         }
A 1318
1319         return $out;
1320     }
1321
1322     /**
1323      * Return folders list as html_select object
1324      *
1325      * @param array $p  Named parameters
1326      *
1327      * @return html_select HTML drop-down object
1328      */
1329     public function folder_selector($p = array())
1330     {
0a1dd5 1331         $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true);
1aceb9 1332         $a_mailboxes = array();
A 1333         $storage = $this->get_storage();
1334
1335         if (empty($p['folder_name'])) {
1336             $p['folder_name'] = '*';
1337         }
1338
1339         if ($p['unsubscribed']) {
1340             $list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1341         }
1342         else {
1343             $list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1344         }
1345
1346         $delimiter = $storage->get_hierarchy_delimiter();
1347
1597c8 1348         if (!empty($p['exceptions'])) {
AM 1349             $list = array_diff($list, (array) $p['exceptions']);
1350         }
1351
1352         if (!empty($p['additional'])) {
1353             foreach ($p['additional'] as $add_folder) {
1354                 $add_items = explode($delimiter, $add_folder);
1355                 $folder    = '';
1356                 while (count($add_items)) {
1357                     $folder .= array_shift($add_items);
1358
1359                     // @TODO: sorting
1360                     if (!in_array($folder, $list)) {
1361                         $list[] = $folder;
1362                     }
1363
1364                     $folder .= $delimiter;
1365                 }
1aceb9 1366             }
A 1367         }
1368
1597c8 1369         foreach ($list as $folder) {
AM 1370             $this->build_folder_tree($a_mailboxes, $folder, $delimiter);
1371         }
1372
1aceb9 1373         $select = new html_select($p);
A 1374
1375         if ($p['noselection']) {
0a1dd5 1376             $select->add(html::quote($p['noselection']), '');
1aceb9 1377         }
A 1378
1379         $this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
1380
1381         return $select;
1382     }
1383
1384     /**
1385      * Create a hierarchical array of the mailbox list
1386      */
1387     public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
1388     {
1389         // Handle namespace prefix
1390         $prefix = '';
1391         if (!$path) {
1392             $n_folder = $folder;
1393             $folder = $this->storage->mod_folder($folder);
1394
1395             if ($n_folder != $folder) {
1396                 $prefix = substr($n_folder, 0, -strlen($folder));
1397             }
1398         }
1399
1400         $pos = strpos($folder, $delm);
1401
1402         if ($pos !== false) {
1403             $subFolders    = substr($folder, $pos+1);
1404             $currentFolder = substr($folder, 0, $pos);
1405
1406             // sometimes folder has a delimiter as the last character
1407             if (!strlen($subFolders)) {
1408                 $virtual = false;
1409             }
1410             else if (!isset($arrFolders[$currentFolder])) {
1411                 $virtual = true;
1412             }
1413             else {
1414                 $virtual = $arrFolders[$currentFolder]['virtual'];
1415             }
1416         }
1417         else {
1418             $subFolders    = false;
1419             $currentFolder = $folder;
1420             $virtual       = false;
1421         }
1422
1423         $path .= $prefix . $currentFolder;
1424
1425         if (!isset($arrFolders[$currentFolder])) {
1426             $arrFolders[$currentFolder] = array(
1427                 'id' => $path,
1428                 'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
1429                 'virtual' => $virtual,
1430                 'folders' => array());
1431         }
1432         else {
1433             $arrFolders[$currentFolder]['virtual'] = $virtual;
1434         }
1435
1436         if (strlen($subFolders)) {
1437             $this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1438         }
1439     }
1440
1441     /**
1442      * Return html for a structured list &lt;ul&gt; for the mailbox tree
1443      */
1444     public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
1445     {
1446         $maxlength = intval($attrib['maxlength']);
1447         $realnames = (bool)$attrib['realnames'];
1448         $msgcounts = $this->storage->get_cache('messagecount');
1449         $collapsed = $this->config->get('collapsed_folders');
85e65c 1450         $realnames = $this->config->get('show_real_foldernames');
52deb1 1451
1aceb9 1452         $out = '';
3725cf 1453         foreach ($arrFolders as $folder) {
1aceb9 1454             $title        = null;
A 1455             $folder_class = $this->folder_classname($folder['id']);
1456             $is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
1457             $unread       = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1458
1459             if ($folder_class && !$realnames) {
1460                 $foldername = $this->gettext($folder_class);
1461             }
1462             else {
1463                 $foldername = $folder['name'];
1464
1465                 // shorten the folder name to a given length
1466                 if ($maxlength && $maxlength > 1) {
1467                     $fname = abbreviate_string($foldername, $maxlength);
1468                     if ($fname != $foldername) {
1469                         $title = $foldername;
1470                     }
1471                     $foldername = $fname;
1472                 }
1473             }
1474
1475             // make folder name safe for ids and class names
1476             $folder_id = rcube_utils::html_identifier($folder['id'], true);
1477             $classes   = array('mailbox');
1478
1479             // set special class for Sent, Drafts, Trash and Junk
1480             if ($folder_class) {
1481                 $classes[] = $folder_class;
1482             }
1483
1484             if ($folder['id'] == $mbox_name) {
1485                 $classes[] = 'selected';
1486             }
1487
1488             if ($folder['virtual']) {
1489                 $classes[] = 'virtual';
1490             }
1491             else if ($unread) {
1492                 $classes[] = 'unread';
1493             }
1494
1495             $js_name = $this->JQ($folder['id']);
1496             $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
1497             $link_attrib = $folder['virtual'] ? array() : array(
1498                 'href' => $this->url(array('_mbox' => $folder['id'])),
60226a 1499                 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::JS_OBJECT_NAME, $js_name),
1aceb9 1500                 'rel' => $folder['id'],
A 1501                 'title' => $title,
1502             );
1503
1504             $out .= html::tag('li', array(
1505                 'id' => "rcmli".$folder_id,
1506                 'class' => join(' ', $classes),
1507                 'noclose' => true),
3c309a 1508                 html::a($link_attrib, $html_name));
1aceb9 1509
3c309a 1510             if (!empty($folder['folders'])) {
TB 1511                 $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
1512             }
1513
1514             $jslist[$folder['id']] = array(
1aceb9 1515                 'id'      => $folder['id'],
A 1516                 'name'    => $foldername,
9a0153 1517                 'virtual' => $folder['virtual'],
1aceb9 1518             );
A 1519
9a0153 1520             if (!empty($folder_class)) {
AM 1521                 $jslist[$folder['id']]['class'] = $folder_class;
1522             }
1523
1aceb9 1524             if (!empty($folder['folders'])) {
A 1525                 $out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
1526                     $this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1527             }
1528
1529             $out .= "</li>\n";
1530         }
1531
1532         return $out;
1533     }
1534
1535     /**
1536      * Return html for a flat list <select> for the mailbox tree
1537      */
1538     public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
1539     {
1540         $out = '';
1541
3725cf 1542         foreach ($arrFolders as $folder) {
1aceb9 1543             // skip exceptions (and its subfolders)
A 1544             if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
1545                 continue;
1546             }
1547
1548             // skip folders in which it isn't possible to create subfolders
1549             if (!empty($opts['skip_noinferiors'])) {
1550                 $attrs = $this->storage->folder_attributes($folder['id']);
1551                 if ($attrs && in_array('\\Noinferiors', $attrs)) {
1552                     continue;
1553                 }
1554             }
1555
1556             if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
1557                 $foldername = $this->gettext($folder_class);
1558             }
1559             else {
1560                 $foldername = $folder['name'];
1561
1562                 // shorten the folder name to a given length
1563                 if ($maxlength && $maxlength > 1) {
1564                     $foldername = abbreviate_string($foldername, $maxlength);
1565                 }
e7ca04 1566             }
1aceb9 1567
0a1dd5 1568             $select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
1aceb9 1569
e7ca04 1570             if (!empty($folder['folders'])) {
A 1571                 $out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
1572                     $select, $realnames, $nestLevel+1, $opts);
1aceb9 1573             }
A 1574         }
1575
1576         return $out;
1577     }
1578
1579     /**
1580      * Return internal name for the given folder if it matches the configured special folders
1581      */
1582     public function folder_classname($folder_id)
1583     {
1584         if ($folder_id == 'INBOX') {
1585             return 'inbox';
1586         }
1587
1588         // for these mailboxes we have localized labels and css classes
1589         foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1590         {
1591             if ($folder_id === $this->config->get($smbx.'_mbox')) {
1592                 return $smbx;
1593             }
1594         }
1595     }
1596
1597     /**
1598      * Try to localize the given IMAP folder name.
1599      * UTF-7 decode it in case no localized text was found
1600      *
4d7964 1601      * @param string $name      Folder name
AM 1602      * @param bool   $with_path Enable path localization
1aceb9 1603      *
A 1604      * @return string Localized folder name in UTF-8 encoding
1605      */
4d7964 1606     public function localize_foldername($name, $with_path = true)
1aceb9 1607     {
85e65c 1608         $realnames = $this->config->get('show_real_foldernames');
AM 1609
4d7964 1610         // try to localize path of the folder
85e65c 1611         if ($with_path && !$realnames) {
4d7964 1612             $storage   = $this->get_storage();
AM 1613             $delimiter = $storage->get_hierarchy_delimiter();
1614             $path      = explode($delimiter, $name);
1615             $count     = count($path);
1616
1617             if ($count > 1) {
61be82 1618                 for ($i = 0; $i < $count; $i++) {
4d7964 1619                     $folder = implode($delimiter, array_slice($path, 0, -$i));
85e65c 1620                     if ($folder_class = $this->folder_classname($folder)) {
4d7964 1621                         $name = implode($delimiter, array_slice($path, $count - $i));
AM 1622                         return $this->gettext($folder_class) . $delimiter . rcube_charset::convert($name, 'UTF7-IMAP');
1623                     }
1624                 }
1625             }
1626         }
1627
85e65c 1628         if (!$realnames && ($folder_class = $this->folder_classname($name))) {
1aceb9 1629             return $this->gettext($folder_class);
A 1630         }
85e65c 1631
AM 1632         return rcube_charset::convert($name, 'UTF7-IMAP');
1aceb9 1633     }
A 1634
1635
1636     public function localize_folderpath($path)
1637     {
1638         $protect_folders = $this->config->get('protect_default_folders');
1639         $delimiter       = $this->storage->get_hierarchy_delimiter();
1640         $path            = explode($delimiter, $path);
1641         $result          = array();
1642
1643         foreach ($path as $idx => $dir) {
1644             $directory = implode($delimiter, array_slice($path, 0, $idx+1));
dc0b50 1645             if ($protect_folders && $this->storage->is_special_folder($directory)) {
1aceb9 1646                 unset($result);
A 1647                 $result[] = $this->localize_foldername($directory);
1648             }
1649             else {
1650                 $result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
1651             }
1652         }
1653
1654         return implode($delimiter, $result);
1655     }
1656
1657
1658     public static function quota_display($attrib)
1659     {
1660         $rcmail = rcmail::get_instance();
1661
1662         if (!$attrib['id']) {
1663             $attrib['id'] = 'rcmquotadisplay';
1664         }
1665
1666         $_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
1667
1668         $rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
1669
1670         $quota = $rcmail->quota_content($attrib);
1671
1672         $rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
1673
edca65 1674         return html::span($attrib, '&nbsp;');
1aceb9 1675     }
A 1676
1677
1678     public function quota_content($attrib = null)
1679     {
1680         $quota = $this->storage->get_quota();
1681         $quota = $this->plugins->exec_hook('quota', $quota);
1682
1683         $quota_result = (array) $quota;
1684         $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1685
4fee77 1686         if ($quota['total'] > 0) {
1aceb9 1687             if (!isset($quota['percent'])) {
A 1688                 $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1689             }
1690
1691             $title = sprintf('%s / %s (%.0f%%)',
1692                 $this->show_bytes($quota['used'] * 1024), $this->show_bytes($quota['total'] * 1024),
1693                 $quota_result['percent']);
1694
1695             $quota_result['title'] = $title;
1696
1697             if ($attrib['width']) {
1698                 $quota_result['width'] = $attrib['width'];
1699             }
1700             if ($attrib['height']) {
1701                 $quota_result['height']    = $attrib['height'];
1702             }
1703         }
1704         else {
4fee77 1705             $unlimited               = $this->config->get('quota_zero_as_unlimited');
AM 1706             $quota_result['title']   = $this->gettext($unlimited ? 'unlimited' : 'unknown');
1aceb9 1707             $quota_result['percent'] = 0;
A 1708         }
1709
1710         return $quota_result;
1711     }
1712
1713     /**
1714      * Outputs error message according to server error/response codes
1715      *
1716      * @param string $fallback       Fallback message label
1717      * @param array  $fallback_args  Fallback message label arguments
e7c1aa 1718      * @param string $suffix         Message label suffix
1aceb9 1719      */
e7c1aa 1720     public function display_server_error($fallback = null, $fallback_args = null, $suffix = '')
1aceb9 1721     {
A 1722         $err_code = $this->storage->get_error_code();
1723         $res_code = $this->storage->get_response_code();
e7c1aa 1724         $args     = array();
1aceb9 1725
524e48 1726         if ($res_code == rcube_storage::NOPERM) {
e7c1aa 1727             $error = 'errornoperm';
1aceb9 1728         }
A 1729         else if ($res_code == rcube_storage::READONLY) {
e7c1aa 1730             $error = 'errorreadonly';
1aceb9 1731         }
0bf724 1732         else if ($res_code == rcube_storage::OVERQUOTA) {
e7c1aa 1733             $error = 'errorroverquota';
0bf724 1734         }
1aceb9 1735         else if ($err_code && ($err_str = $this->storage->get_error_str())) {
A 1736             // try to detect access rights problem and display appropriate message
1737             if (stripos($err_str, 'Permission denied') !== false) {
e7c1aa 1738                 $error = 'errornoperm';
1aceb9 1739             }
0bf724 1740             // try to detect full mailbox problem and display appropriate message
8c93c2 1741             // there can be e.g. "Quota exceeded" or "quotum would exceed"
AM 1742             else if (stripos($err_str, 'quot') !== false && stripos($err_str, 'exceed') !== false) {
e7c1aa 1743                 $error = 'erroroverquota';
0bf724 1744             }
1aceb9 1745             else {
e7c1aa 1746                 $error = 'servererrormsg';
AM 1747                 $args  = array('msg' => $err_str);
1aceb9 1748             }
A 1749         }
524e48 1750         else if ($err_code < 0) {
e7c1aa 1751             $error = 'storageerror';
524e48 1752         }
1aceb9 1753         else if ($fallback) {
e7c1aa 1754             $error = $fallback;
AM 1755             $args  = $fallback_args;
1756         }
1757
1758         if ($error) {
1759             if ($suffix && $this->text_exists($error . $suffix)) {
1760                 $error .= $suffix;
1761             }
1762             $this->output->show_message($error, 'error', $args);
1aceb9 1763         }
A 1764     }
1765
1766     /**
1767      * Output HTML editor scripts
1768      *
1769      * @param string $mode  Editor mode
1770      */
1771     public function html_editor($mode = '')
1772     {
1773         $hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
1774
1775         if ($hook['abort']) {
1776             return;
1777         }
1778
1779         $lang = strtolower($_SESSION['language']);
1780
1781         // TinyMCE uses two-letter lang codes, with exception of Chinese
1782         if (strpos($lang, 'zh_') === 0) {
1783             $lang = str_replace('_', '-', $lang);
1784         }
1785         else {
1786             $lang = substr($lang, 0, 2);
1787         }
1788
1789         if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
1790             $lang = 'en';
1791         }
1792
1793         $script = json_encode(array(
1794             'mode'       => $mode,
1795             'lang'       => $lang,
1796             'skin_path'  => $this->output->get_skin_path(),
1797             'spellcheck' => intval($this->config->get('enable_spellcheck')),
1798             'spelldict'  => intval($this->config->get('spellcheck_dictionary'))
1799         ));
1800
1801         $this->output->include_script('tiny_mce/tiny_mce.js');
1802         $this->output->include_script('editor.js');
1803         $this->output->add_script("rcmail_editor_init($script)", 'docready');
1804     }
1805
1806     /**
1807      * Replaces TinyMCE's emoticon images with plain-text representation
1808      *
1809      * @param string $html  HTML content
1810      *
1811      * @return string HTML content
1812      */
1813     public static function replace_emoticons($html)
1814     {
1815         $emoticons = array(
1816             '8-)' => 'smiley-cool',
1817             ':-#' => 'smiley-foot-in-mouth',
1818             ':-*' => 'smiley-kiss',
1819             ':-X' => 'smiley-sealed',
1820             ':-P' => 'smiley-tongue-out',
1821             ':-@' => 'smiley-yell',
1822             ":'(" => 'smiley-cry',
1823             ':-(' => 'smiley-frown',
1824             ':-D' => 'smiley-laughing',
1825             ':-)' => 'smiley-smile',
1826             ':-S' => 'smiley-undecided',
1827             ':-$' => 'smiley-embarassed',
1828             'O:-)' => 'smiley-innocent',
1829             ':-|' => 'smiley-money-mouth',
1830             ':-O' => 'smiley-surprised',
1831             ';-)' => 'smiley-wink',
1832         );
1833
1834         foreach ($emoticons as $idx => $file) {
1835             // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1836             $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1837             $replace[] = $idx;
1838         }
1839
1840         return preg_replace($search, $replace, $html);
1841     }
1842
1843     /**
1844      * File upload progress handler.
1845      */
1846     public function upload_progress()
1847     {
1848         $prefix = ini_get('apc.rfc1867_prefix');
1849         $params = array(
1850             'action' => $this->action,
1851             'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET),
1852         );
1853
1854         if (function_exists('apc_fetch')) {
1855             $status = apc_fetch($prefix . $params['name']);
1856
1857             if (!empty($status)) {
1858                 $status['percent'] = round($status['current']/$status['total']*100);
1859                 $params = array_merge($status, $params);
1860             }
1861         }
1862
1863         if (isset($params['percent']))
1864             $params['text'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array(
1865                 'percent' => $params['percent'] . '%',
1866                 'current' => $this->show_bytes($params['current']),
1867                 'total'   => $this->show_bytes($params['total'])
1868         )));
1869
1870         $this->output->command('upload_progress_update', $params);
1871         $this->output->send();
1872     }
1873
1874     /**
1875      * Initializes file uploading interface.
1876      */
1877     public function upload_init()
1878     {
1879         // Enable upload progress bar
39b905 1880         $rfc1867 = filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN);
AM 1881         if ($rfc1867 && ($seconds = $this->config->get('upload_progress'))) {
1aceb9 1882             if ($field_name = ini_get('apc.rfc1867_name')) {
A 1883                 $this->output->set_env('upload_progress_name', $field_name);
1884                 $this->output->set_env('upload_progress_time', (int) $seconds);
1885             }
1886         }
1887
1888         // find max filesize value
1889         $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
1890         $max_postsize = parse_bytes(ini_get('post_max_size'));
0301d9 1891
1aceb9 1892         if ($max_postsize && $max_postsize < $max_filesize) {
A 1893             $max_filesize = $max_postsize;
1894         }
1895
1896         $this->output->set_env('max_filesize', $max_filesize);
6b2b2e 1897         $max_filesize = $this->show_bytes($max_filesize);
1aceb9 1898         $this->output->set_env('filesizeerror', $this->gettext(array(
A 1899             'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
1900
1901         return $max_filesize;
1902     }
1903
1904     /**
1905      * Initializes client-side autocompletion.
1906      */
1907     public function autocomplete_init()
1908     {
1909         static $init;
1910
1911         if ($init) {
1912             return;
1913         }
1914
1915         $init = 1;
1916
1917         if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
1918             $book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
1919             if (count($book_types) > 1) {
1920                 $this->output->set_env('autocomplete_threads', $threads);
1921                 $this->output->set_env('autocomplete_sources', $book_types);
1922             }
1923         }
1924
1925         $this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
1926         $this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
1927         $this->output->add_label('autocompletechars', 'autocompletemore');
1928     }
1929
1930     /**
1931      * Returns supported font-family specifications
1932      *
1933      * @param string $font  Font name
1934      *
1935      * @param string|array Font-family specification array or string (if $font is used)
1936      */
1937     public static function font_defs($font = null)
1938     {
1939         $fonts = array(
1940             'Andale Mono'   => '"Andale Mono",Times,monospace',
1941             'Arial'         => 'Arial,Helvetica,sans-serif',
1942             'Arial Black'   => '"Arial Black","Avant Garde",sans-serif',
1943             'Book Antiqua'  => '"Book Antiqua",Palatino,serif',
1944             'Courier New'   => '"Courier New",Courier,monospace',
1945             'Georgia'       => 'Georgia,Palatino,serif',
1946             'Helvetica'     => 'Helvetica,Arial,sans-serif',
1947             'Impact'        => 'Impact,Chicago,sans-serif',
1948             'Tahoma'        => 'Tahoma,Arial,Helvetica,sans-serif',
1949             'Terminal'      => 'Terminal,Monaco,monospace',
1950             'Times New Roman' => '"Times New Roman",Times,serif',
1951             'Trebuchet MS'  => '"Trebuchet MS",Geneva,sans-serif',
1952             'Verdana'       => 'Verdana,Geneva,sans-serif',
1953         );
1954
1955         if ($font) {
1956             return $fonts[$font];
1957         }
1958
1959         return $fonts;
1960     }
1961
1962     /**
1963      * Create a human readable string for a number of bytes
1964      *
1965      * @param int Number of bytes
1966      *
1967      * @return string Byte string
1968      */
1969     public function show_bytes($bytes)
1970     {
1971         if ($bytes >= 1073741824) {
1972             $gb  = $bytes/1073741824;
1973             $str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->gettext('GB');
1974         }
1975         else if ($bytes >= 1048576) {
1976             $mb  = $bytes/1048576;
1977             $str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->gettext('MB');
1978         }
1979         else if ($bytes >= 1024) {
1980             $str = sprintf("%d ",  round($bytes/1024)) . $this->gettext('KB');
1981         }
1982         else {
1983             $str = sprintf('%d ', $bytes) . $this->gettext('B');
1984         }
1985
1986         return $str;
1987     }
1988
1989     /**
8749e9 1990      * Returns real size (calculated) of the message part
AM 1991      *
1992      * @param rcube_message_part  Message part
1993      *
1994      * @return string Part size (and unit)
1995      */
1996     public function message_part_size($part)
1997     {
1998         if (isset($part->d_parameters['size'])) {
1999             $size = $this->show_bytes((int)$part->d_parameters['size']);
2000         }
2001         else {
2002           $size = $part->size;
2003           if ($part->encoding == 'base64') {
2004             $size = $size / 1.33;
2005           }
2006
2007           $size = '~' . $this->show_bytes($size);
2008         }
2009
2010         return $size;
2011     }
2012
2013
1aceb9 2014     /************************************************************************
A 2015      *********          Deprecated methods (to be removed)          *********
2016      ***********************************************************************/
2017
2018     public static function setcookie($name, $value, $exp = 0)
2019     {
2020         rcube_utils::setcookie($name, $value, $exp);
2021     }
38a08c 2022
AM 2023     public function imap_connect()
2024     {
2025         return $this->storage_connect();
2026     }
5a575b 2027
b97f21 2028     public function imap_init()
TB 2029     {
2030         return $this->storage_init();
2031     }
2032
5a575b 2033     /**
AM 2034      * Connect to the mail storage server with stored session data
2035      *
2036      * @return bool True on success, False on error
2037      */
2038     public function storage_connect()
2039     {
2040         $storage = $this->get_storage();
2041
2042         if ($_SESSION['storage_host'] && !$storage->is_connected()) {
2043             $host = $_SESSION['storage_host'];
2044             $user = $_SESSION['username'];
2045             $port = $_SESSION['storage_port'];
2046             $ssl  = $_SESSION['storage_ssl'];
2047             $pass = $this->decrypt($_SESSION['password']);
2048
2049             if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
2050                 if (is_object($this->output)) {
7ea292 2051                     $this->output->show_message('storageerror', 'error');
5a575b 2052                 }
AM 2053             }
2054             else {
2055                 $this->set_storage_prop();
2056             }
2057         }
2058
5f6c71 2059         return $storage->is_connected();
5a575b 2060     }
197601 2061 }