source: subversion/trunk/roundcubemail/program/include/rcmail.php @ 6093

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