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