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

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