From 99cdca46b7bcc46fe6affd9e9f9f60a546b2e5b8 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Thu, 05 Jun 2014 03:18:07 -0400 Subject: [PATCH] Merge branch 'dev-accessibility' --- program/include/rcmail.php | 1983 +++++++++++++++++++++++++++++++--------------------------- 1 files changed, 1,062 insertions(+), 921 deletions(-) diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 9713cdb..1a22792 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -5,8 +5,8 @@ | program/include/rcmail.php | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2008-2014, The Roundcube Dev Team | + | Copyright (C) 2011-2014, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -21,602 +21,631 @@ +-----------------------------------------------------------------------+ */ - /** * Application class of Roundcube Webmail * implemented as singleton * - * @package Core + * @package Webmail */ class rcmail extends rcube { - /** - * Main tasks. - * - * @var array - */ - static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy'); + /** + * Main tasks. + * + * @var array + */ + static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy'); - /** - * Current task. - * - * @var string - */ - public $task; + /** + * Current task. + * + * @var string + */ + public $task; - /** - * Current action. - * - * @var string - */ - public $action = ''; - public $comm_path = './'; - public $filename = ''; + /** + * Current action. + * + * @var string + */ + public $action = ''; + public $comm_path = './'; + public $filename = ''; - private $address_books = array(); - private $action_map = array(); + private $address_books = array(); + private $action_map = array(); - const ERROR_STORAGE = -2; - const ERROR_INVALID_REQUEST = 1; - const ERROR_INVALID_HOST = 2; - const ERROR_COOKIES_DISABLED = 3; + const ERROR_STORAGE = -2; + const ERROR_INVALID_REQUEST = 1; + const ERROR_INVALID_HOST = 2; + const ERROR_COOKIES_DISABLED = 3; - /** - * This implements the 'singleton' design pattern - * - * @param string Environment name to run (e.g. live, dev, test) - * @return rcmail The one and only instance - */ - static function get_instance($env = '') - { - if (!self::$instance || !is_a(self::$instance, 'rcmail')) { - self::$instance = new rcmail($env); - self::$instance->startup(); // init AFTER object was linked with self::$instance - } - - return self::$instance; - } - - - /** - * Initial startup function - * to register session, create database and imap connections - */ - protected function startup() - { - $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS); - - // set filename if not index.php - if (($basename = basename($_SERVER['SCRIPT_FILENAME'])) && $basename != 'index.php') - $this->filename = $basename; - - // start session - $this->session_init(); - - // create user object - $this->set_user(new rcube_user($_SESSION['user_id'])); - - // set task and action properties - $this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC)); - $this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC)); - - // reset some session parameters when changing task - if ($this->task != 'utils') { - // we reset list page when switching to another task - // but only to the main task interface - empty action (#1489076) - // this will prevent from unintentional page reset on cross-task requests - if ($this->session && $_SESSION['task'] != $this->task && empty($this->action)) - $this->session->remove('page'); - // set current task to session - $_SESSION['task'] = $this->task; - } - - // init output class - if (!empty($_REQUEST['_remote'])) - $GLOBALS['OUTPUT'] = $this->json_init(); - else - $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed'])); - - // load plugins - $this->plugins->init($this, $this->task); - $this->plugins->load_plugins((array)$this->config->get('plugins', array()), array('filesystem_attachments', 'jqueryui')); - } - - - /** - * Setter for application task - * - * @param string Task to set - */ - public function set_task($task) - { - $task = asciiwords($task, true); - - if ($this->user && $this->user->ID) - $task = !$task ? 'mail' : $task; - else - $task = 'login'; - - $this->task = $task; - $this->comm_path = $this->url(array('task' => $this->task)); - - if ($this->output) - $this->output->set_env('task', $this->task); - } - - - /** - * Setter for system user object - * - * @param rcube_user Current user instance - */ - public function set_user($user) - { - if (is_object($user)) { - $this->user = $user; - - // overwrite config with user preferences - $this->config->set_user_prefs((array)$this->user->get_prefs()); - } - - $lang = $this->language_prop($this->config->get('language', $_SESSION['language'])); - $_SESSION['language'] = $this->user->language = $lang; - - // set localization - setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8'); - - // workaround for http://bugs.php.net/bug.php?id=18556 - if (version_compare(PHP_VERSION, '5.5.0', '<') && in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) { - setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8'); - } - } - - - /** - * Return instance of the internal address book class - * - * @param string Address book identifier (-1 for default addressbook) - * @param boolean True if the address book needs to be writeable - * - * @return rcube_contacts Address book object - */ - public function get_address_book($id, $writeable = false) - { - $contacts = null; - $ldap_config = (array)$this->config->get('ldap_public'); - - // 'sql' is the alias for '0' used by autocomplete - if ($id == 'sql') - $id = '0'; - else if ($id == -1) { - $id = $this->config->get('default_addressbook'); - $default = true; - } - - // use existing instance - if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) { - $contacts = $this->address_books[$id]; - } - else if ($id && $ldap_config[$id]) { - $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['storage_host'])); - } - else if ($id === '0') { - $contacts = new rcube_contacts($this->db, $this->get_user_id()); - } - else { - $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable)); - - // plugin returned instance of a rcube_addressbook - if ($plugin['instance'] instanceof rcube_addressbook) { - $contacts = $plugin['instance']; - } - } - - // when user requested default writeable addressbook - // we need to check if default is writeable, if not we - // will return first writeable book (if any exist) - if ($contacts && $default && $contacts->readonly && $writeable) { - $contacts = null; - } - - // Get first addressbook from the list if configured default doesn't exist - // This can happen when user deleted the addressbook (e.g. Kolab folder) - if (!$contacts && (!$id || $default)) { - $source = reset($this->get_address_sources($writeable, !$default)); - if (!empty($source)) { - $contacts = $this->get_address_book($source['id']); - if ($contacts) { - $id = $source['id']; + /** + * This implements the 'singleton' design pattern + * + * @param string Environment name to run (e.g. live, dev, test) + * + * @return rcmail The one and only instance + */ + static function get_instance($env = '') + { + if (!self::$instance || !is_a(self::$instance, 'rcmail')) { + self::$instance = new rcmail($env); + // init AFTER object was linked with self::$instance + self::$instance->startup(); } - } + + return self::$instance; } - if (!$contacts) { - // there's no default, just return - if ($default) { - return null; - } + /** + * Initial startup function + * to register session, create database and imap connections + */ + protected function startup() + { + $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS); - self::raise_error(array( - 'code' => 700, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Addressbook source ($id) not found!"), - true, true); - } - - // add to the 'books' array for shutdown function - $this->address_books[$id] = $contacts; - - if ($writeable && $contacts->readonly) { - return null; - } - - // set configured sort order - if ($sort_col = $this->config->get('addressbook_sort_col')) { - $contacts->set_sort_order($sort_col); - } - - return $contacts; - } - - - /** - * Return identifier of the address book object - * - * @param rcube_addressbook Addressbook source object - * - * @return string Source identifier - */ - public function get_address_book_id($object) - { - foreach ($this->address_books as $index => $book) { - if ($book === $object) { - return $index; - } - } - } - - - /** - * Return address books list - * - * @param boolean True if the address book needs to be writeable - * @param boolean True if the address book needs to be not hidden - * - * @return array Address books array - */ - public function get_address_sources($writeable = false, $skip_hidden = false) - { - $abook_type = (string) $this->config->get('address_book_type'); - $ldap_config = (array) $this->config->get('ldap_public'); - $autocomplete = (array) $this->config->get('autocomplete_addressbooks'); - $list = array(); - - // We are using the DB address book or a plugin address book - if (!empty($abook_type) && strtolower($abook_type) != 'ldap') { - if (!isset($this->address_books['0'])) - $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id()); - $list['0'] = array( - 'id' => '0', - 'name' => $this->gettext('personaladrbook'), - 'groups' => $this->address_books['0']->groups, - 'readonly' => $this->address_books['0']->readonly, - 'autocomplete' => in_array('sql', $autocomplete), - 'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'), - ); - } - - if (!empty($ldap_config)) { - foreach ($ldap_config as $id => $prop) { - // handle misconfiguration - if (empty($prop) || !is_array($prop)) { - continue; + // set filename if not index.php + if (($basename = basename($_SERVER['SCRIPT_FILENAME'])) && $basename != 'index.php') { + $this->filename = $basename; } - $list[$id] = array( - 'id' => $id, - 'name' => html::quote($prop['name']), - 'groups' => !empty($prop['groups']) || !empty($prop['group_filters']), - 'readonly' => !$prop['writable'], - 'hidden' => $prop['hidden'], - 'autocomplete' => in_array($id, $autocomplete) - ); - } + + // start session + $this->session_init(); + + // create user object + $this->set_user(new rcube_user($_SESSION['user_id'])); + + // set task and action properties + $this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC)); + $this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC)); + + // reset some session parameters when changing task + if ($this->task != 'utils') { + // we reset list page when switching to another task + // but only to the main task interface - empty action (#1489076) + // this will prevent from unintentional page reset on cross-task requests + if ($this->session && $_SESSION['task'] != $this->task && empty($this->action)) { + $this->session->remove('page'); + } + + // set current task to session + $_SESSION['task'] = $this->task; + } + + // init output class + if (!empty($_REQUEST['_remote'])) + $GLOBALS['OUTPUT'] = $this->json_init(); + else + $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed'])); + + // load plugins + $this->plugins->init($this, $this->task); + $this->plugins->load_plugins((array)$this->config->get('plugins', array()), + array('filesystem_attachments', 'jqueryui')); } - $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list)); - $list = $plugin['sources']; + /** + * Setter for application task + * + * @param string Task to set + */ + public function set_task($task) + { + $task = asciiwords($task, true); - foreach ($list as $idx => $item) { - // register source for shutdown function - if (!is_object($this->address_books[$item['id']])) { - $this->address_books[$item['id']] = $item; - } - // remove from list if not writeable as requested - if ($writeable && $item['readonly']) { - unset($list[$idx]); - } - // remove from list if hidden as requested - else if ($skip_hidden && $item['hidden']) { - unset($list[$idx]); - } + if ($this->user && $this->user->ID) + $task = !$task ? 'mail' : $task; + else if (php_sapi_name() == 'cli') + $task = 'cli'; + else + $task = 'login'; + + $this->task = $task; + $this->comm_path = $this->url(array('task' => $this->task)); + + if ($this->output) { + $this->output->set_env('task', $this->task); + } } - return $list; - } + /** + * Setter for system user object + * + * @param rcube_user Current user instance + */ + public function set_user($user) + { + parent::set_user($user); - /** - * Getter for compose responses. - * These are stored in local config and user preferences. - * - * @param boolean True to sort the list alphabetically - * @return array List of the current user's stored responses - */ - public function get_compose_responses($sorted = false) - { - foreach ($this->config->get('compose_responses', array()) as $response) { - if (empty($response['key'])) - $response['key'] = substr(md5($response['name']), 0, 16); - $k = $sorted ? strtolower($response['name']) : $response['key']; - $responses[$k] = $response; - } + $lang = $this->language_prop($this->config->get('language', $_SESSION['language'])); + $_SESSION['language'] = $this->user->language = $lang; - if ($sorted) { + // set localization + setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8'); + + // workaround for http://bugs.php.net/bug.php?id=18556 + if (version_compare(PHP_VERSION, '5.5.0', '<') && in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) { + setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8'); + } + } + + /** + * Return instance of the internal address book class + * + * @param string Address book identifier (-1 for default addressbook) + * @param boolean True if the address book needs to be writeable + * + * @return rcube_contacts Address book object + */ + public function get_address_book($id, $writeable = false) + { + $contacts = null; + $ldap_config = (array)$this->config->get('ldap_public'); + + // 'sql' is the alias for '0' used by autocomplete + if ($id == 'sql') + $id = '0'; + else if ($id == -1) { + $id = $this->config->get('default_addressbook'); + $default = true; + } + + // use existing instance + if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) { + $contacts = $this->address_books[$id]; + } + else if ($id && $ldap_config[$id]) { + $domain = $this->config->mail_domain($_SESSION['storage_host']); + $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $domain); + } + else if ($id === '0') { + $contacts = new rcube_contacts($this->db, $this->get_user_id()); + } + else { + $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable)); + + // plugin returned instance of a rcube_addressbook + if ($plugin['instance'] instanceof rcube_addressbook) { + $contacts = $plugin['instance']; + } + } + + // when user requested default writeable addressbook + // we need to check if default is writeable, if not we + // will return first writeable book (if any exist) + if ($contacts && $default && $contacts->readonly && $writeable) { + $contacts = null; + } + + // Get first addressbook from the list if configured default doesn't exist + // This can happen when user deleted the addressbook (e.g. Kolab folder) + if (!$contacts && (!$id || $default)) { + $source = reset($this->get_address_sources($writeable, !$default)); + if (!empty($source)) { + $contacts = $this->get_address_book($source['id']); + if ($contacts) { + $id = $source['id']; + } + } + } + + if (!$contacts) { + // there's no default, just return + if ($default) { + return null; + } + + self::raise_error(array( + 'code' => 700, + 'file' => __FILE__, + 'line' => __LINE__, + 'message' => "Addressbook source ($id) not found!" + ), + true, true); + } + + // add to the 'books' array for shutdown function + $this->address_books[$id] = $contacts; + + if ($writeable && $contacts->readonly) { + return null; + } + + // set configured sort order + if ($sort_col = $this->config->get('addressbook_sort_col')) { + $contacts->set_sort_order($sort_col); + } + + return $contacts; + } + + /** + * Return identifier of the address book object + * + * @param rcube_addressbook Addressbook source object + * + * @return string Source identifier + */ + public function get_address_book_id($object) + { + foreach ($this->address_books as $index => $book) { + if ($book === $object) { + return $index; + } + } + } + + /** + * Return address books list + * + * @param boolean True if the address book needs to be writeable + * @param boolean True if the address book needs to be not hidden + * + * @return array Address books array + */ + public function get_address_sources($writeable = false, $skip_hidden = false) + { + $abook_type = (string) $this->config->get('address_book_type'); + $ldap_config = (array) $this->config->get('ldap_public'); + $autocomplete = (array) $this->config->get('autocomplete_addressbooks'); + $list = array(); + + // We are using the DB address book or a plugin address book + if (!empty($abook_type) && strtolower($abook_type) != 'ldap') { + if (!isset($this->address_books['0'])) { + $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id()); + } + + $list['0'] = array( + 'id' => '0', + 'name' => $this->gettext('personaladrbook'), + 'groups' => $this->address_books['0']->groups, + 'readonly' => $this->address_books['0']->readonly, + 'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'), + 'autocomplete' => in_array('sql', $autocomplete), + ); + } + + if (!empty($ldap_config)) { + foreach ($ldap_config as $id => $prop) { + // handle misconfiguration + if (empty($prop) || !is_array($prop)) { + continue; + } + + $list[$id] = array( + 'id' => $id, + 'name' => html::quote($prop['name']), + 'groups' => !empty($prop['groups']) || !empty($prop['group_filters']), + 'readonly' => !$prop['writable'], + 'hidden' => $prop['hidden'], + 'autocomplete' => in_array($id, $autocomplete) + ); + } + } + + $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list)); + $list = $plugin['sources']; + + foreach ($list as $idx => $item) { + // register source for shutdown function + if (!is_object($this->address_books[$item['id']])) { + $this->address_books[$item['id']] = $item; + } + // remove from list if not writeable as requested + if ($writeable && $item['readonly']) { + unset($list[$idx]); + } + // remove from list if hidden as requested + else if ($skip_hidden && $item['hidden']) { + unset($list[$idx]); + } + } + + return $list; + } + + /** + * Getter for compose responses. + * These are stored in local config and user preferences. + * + * @param boolean True to sort the list alphabetically + * @param boolean True if only this user's responses shall be listed + * + * @return array List of the current user's stored responses + */ + public function get_compose_responses($sorted = false, $user_only = false) + { + $responses = array(); + + if (!$user_only) { + foreach ($this->config->get('compose_responses_static', array()) as $response) { + if (empty($response['key'])) { + $response['key'] = substr(md5($response['name']), 0, 16); + } + + $response['static'] = true; + $response['class'] = 'readonly'; + + $k = $sorted ? '0000-' . strtolower($response['name']) : $response['key']; + $responses[$k] = $response; + } + } + + foreach ($this->config->get('compose_responses', array()) as $response) { + if (empty($response['key'])) { + $response['key'] = substr(md5($response['name']), 0, 16); + } + + $k = $sorted ? strtolower($response['name']) : $response['key']; + $responses[$k] = $response; + } + // sort list by name - ksort($responses, SORT_LOCALE_STRING); - return array_values($responses); - } - - return $responses; - } - - - /** - * Init output object for GUI and add common scripts. - * This will instantiate a rcmail_output_html object and set - * environment vars according to the current session and configuration - * - * @param boolean True if this request is loaded in a (i)frame - * @return rcube_output Reference to HTML output object - */ - public function load_gui($framed = false) - { - // init output page - if (!($this->output instanceof rcmail_output_html)) - $this->output = new rcmail_output_html($this->task, $framed); - - // set refresh interval - $this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0)); - $this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60); - - if ($framed) { - $this->comm_path .= '&_framed=1'; - $this->output->set_env('framed', true); - } - - $this->output->set_env('task', $this->task); - $this->output->set_env('action', $this->action); - $this->output->set_env('comm_path', $this->comm_path); - $this->output->set_charset(RCUBE_CHARSET); - - // add some basic labels to client - $this->output->add_label('loading', 'servererror', 'requesttimedout', 'refreshing'); - - return $this->output; - } - - - /** - * Create an output object for JSON responses - * - * @return rcube_output Reference to JSON output object - */ - public function json_init() - { - if (!($this->output instanceof rcmail_output_json)) - $this->output = new rcmail_output_json($this->task); - - return $this->output; - } - - - /** - * Create session object and start the session. - */ - public function session_init() - { - parent::session_init(); - - // set initial session vars - if (!$_SESSION['user_id']) - $_SESSION['temp'] = true; - - // restore skin selection after logout - if ($_SESSION['temp'] && !empty($_SESSION['skin'])) - $this->config->set('skin', $_SESSION['skin']); - } - - - /** - * Perfom login to the mail server and to the webmail service. - * This will also create a new user entry if auto_create_user is configured. - * - * @param string Mail storage (IMAP) user name - * @param string Mail storage (IMAP) password - * @param string Mail storage (IMAP) host - * @param bool Enables cookie check - * - * @return boolean True on success, False on failure - */ - function login($username, $pass, $host = null, $cookiecheck = false) - { - $this->login_error = null; - - if (empty($username)) { - return false; - } - - if ($cookiecheck && empty($_COOKIE)) { - $this->login_error = self::ERROR_COOKIES_DISABLED; - return false; - } - - $config = $this->config->all(); - - if (!$host) - $host = $config['default_host']; - - // Validate that selected host is in the list of configured hosts - if (is_array($config['default_host'])) { - $allowed = false; - foreach ($config['default_host'] as $key => $host_allowed) { - if (!is_numeric($key)) - $host_allowed = $key; - if ($host == $host_allowed) { - $allowed = true; - break; + if ($sorted) { + ksort($responses, SORT_LOCALE_STRING); } - } - if (!$allowed) { - $host = null; - } - } - else if (!empty($config['default_host']) && $host != rcube_utils::parse_host($config['default_host'])) { - $host = null; + + return array_values($responses); } - if (!$host) { - $this->login_error = self::ERROR_INVALID_HOST; - return false; + /** + * Init output object for GUI and add common scripts. + * This will instantiate a rcmail_output_html object and set + * environment vars according to the current session and configuration + * + * @param boolean True if this request is loaded in a (i)frame + * + * @return rcube_output Reference to HTML output object + */ + public function load_gui($framed = false) + { + // init output page + if (!($this->output instanceof rcmail_output_html)) { + $this->output = new rcmail_output_html($this->task, $framed); + } + + // set refresh interval + $this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0)); + $this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60); + + if ($framed) { + $this->comm_path .= '&_framed=1'; + $this->output->set_env('framed', true); + } + + $this->output->set_env('task', $this->task); + $this->output->set_env('action', $this->action); + $this->output->set_env('comm_path', $this->comm_path); + $this->output->set_charset(RCUBE_CHARSET); + + if ($this->user && $this->user->ID) { + $this->output->set_env('user_id', $this->user->get_hash()); + } + + // add some basic labels to client + $this->output->add_label('loading', 'servererror', 'connerror', 'requesttimedout', + 'refreshing', 'windowopenerror'); + + return $this->output; } - // parse $host URL - $a_host = parse_url($host); - if ($a_host['host']) { - $host = $a_host['host']; - $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; - if (!empty($a_host['port'])) - $port = $a_host['port']; - else if ($ssl && $ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143)) - $port = 993; + /** + * Create an output object for JSON responses + * + * @return rcube_output Reference to JSON output object + */ + public function json_init() + { + if (!($this->output instanceof rcmail_output_json)) { + $this->output = new rcmail_output_json($this->task); + } + + return $this->output; } - if (!$port) { - $port = $config['default_port']; + /** + * Create session object and start the session. + */ + public function session_init() + { + parent::session_init(); + + // set initial session vars + if (!$_SESSION['user_id']) { + $_SESSION['temp'] = true; + } + + // restore skin selection after logout + if ($_SESSION['temp'] && !empty($_SESSION['skin'])) { + $this->config->set('skin', $_SESSION['skin']); + } } - /* Modify username with domain if required - Inspired by Marco <P0L0_notspam_binware.org> - */ - // Check if we need to add domain - if (!empty($config['username_domain']) && strpos($username, '@') === false) { - if (is_array($config['username_domain']) && isset($config['username_domain'][$host])) - $username .= '@'.rcube_utils::parse_host($config['username_domain'][$host], $host); - else if (is_string($config['username_domain'])) - $username .= '@'.rcube_utils::parse_host($config['username_domain'], $host); + /** + * Perfom login to the mail server and to the webmail service. + * This will also create a new user entry if auto_create_user is configured. + * + * @param string Mail storage (IMAP) user name + * @param string Mail storage (IMAP) password + * @param string Mail storage (IMAP) host + * @param bool Enables cookie check + * + * @return boolean True on success, False on failure + */ + function login($username, $pass, $host = null, $cookiecheck = false) + { + $this->login_error = null; + + if (empty($username)) { + return false; + } + + if ($cookiecheck && empty($_COOKIE)) { + $this->login_error = self::ERROR_COOKIES_DISABLED; + return false; + } + + $default_host = $this->config->get('default_host'); + $default_port = $this->config->get('default_port'); + $username_domain = $this->config->get('username_domain'); + $login_lc = $this->config->get('login_lc', 2); + + // host is validated in rcmail::autoselect_host(), so here + // we'll only handle unset host (if possible) + if (!$host && !empty($default_host)) { + if (is_array($default_host)) { + list($key, $val) = each($default_host); + $host = is_numeric($key) ? $val : $key; + } + else { + $host = $default_host; + } + + $host = rcube_utils::parse_host($host); + } + + if (!$host) { + $this->login_error = self::ERROR_INVALID_HOST; + return false; + } + + // parse $host URL + $a_host = parse_url($host); + if ($a_host['host']) { + $host = $a_host['host']; + $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; + + if (!empty($a_host['port'])) + $port = $a_host['port']; + else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) + $port = 993; + } + + if (!$port) { + $port = $default_port; + } + + // Check if we need to add/force domain to username + if (!empty($username_domain)) { + $domain = is_array($username_domain) ? $username_domain[$host] : $username_domain; + + if ($domain = rcube_utils::parse_host((string)$domain, $host)) { + $pos = strpos($username, '@'); + + // force configured domains + if ($pos !== false && $this->config->get('username_domain_forced')) { + $username = substr($username, 0, $pos) . '@' . $domain; + } + // just add domain if not specified + else if ($pos === false) { + $username .= '@' . $domain; + } + } + } + + // Convert username to lowercase. If storage backend + // is case-insensitive we need to store always the same username (#1487113) + if ($login_lc) { + if ($login_lc == 2 || $login_lc === true) { + $username = mb_strtolower($username); + } + else if (strpos($username, '@')) { + // lowercase domain name + list($local, $domain) = explode('@', $username); + $username = $local . '@' . mb_strtolower($domain); + } + } + + // try to resolve email address from virtuser table + if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) { + $username = $virtuser; + } + + // Here we need IDNA ASCII + // Only rcube_contacts class is using domain names in Unicode + $host = rcube_utils::idn_to_ascii($host); + $username = rcube_utils::idn_to_ascii($username); + + // user already registered -> overwrite username + if ($user = rcube_user::query($username, $host)) { + $username = $user->data['username']; + } + + $storage = $this->get_storage(); + + // try to log in + if (!$storage->connect($host, $username, $pass, $port, $ssl)) { + return false; + } + + // user already registered -> update user's record + if (is_object($user)) { + // update last login timestamp + $user->touch(); + } + // create new system user + else if ($this->config->get('auto_create_user')) { + if ($created = rcube_user::create($username, $host)) { + $user = $created; + } + else { + self::raise_error(array( + 'code' => 620, + 'file' => __FILE__, + 'line' => __LINE__, + 'message' => "Failed to create a user record. Maybe aborted by a plugin?" + ), + true, false); + } + } + else { + self::raise_error(array( + 'code' => 621, + 'file' => __FILE__, + 'line' => __LINE__, + 'message' => "Access denied for new user $username. 'auto_create_user' is disabled" + ), + true, false); + } + + // login succeeded + if (is_object($user) && $user->ID) { + // Configure environment + $this->set_user($user); + $this->set_storage_prop(); + + // set session vars + $_SESSION['user_id'] = $user->ID; + $_SESSION['username'] = $user->data['username']; + $_SESSION['storage_host'] = $host; + $_SESSION['storage_port'] = $port; + $_SESSION['storage_ssl'] = $ssl; + $_SESSION['password'] = $this->encrypt($pass); + $_SESSION['login_time'] = time(); + + if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_') { + $_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC); + } + + // fix some old settings according to namespace prefix + $this->fix_namespace_settings($user); + + // set/create special folders + $this->set_special_folders(); + + // clear all mailboxes related cache(s) + $storage->clear_cache('mailboxes', true); + + return true; + } + + return false; } - - if (!isset($config['login_lc'])) { - $config['login_lc'] = 2; // default - } - - // Convert username to lowercase. If storage backend - // is case-insensitive we need to store always the same username (#1487113) - if ($config['login_lc']) { - if ($config['login_lc'] == 2 || $config['login_lc'] === true) { - $username = mb_strtolower($username); - } - else if (strpos($username, '@')) { - // lowercase domain name - list($local, $domain) = explode('@', $username); - $username = $local . '@' . mb_strtolower($domain); - } - } - - // try to resolve email address from virtuser table - if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) { - $username = $virtuser; - } - - // Here we need IDNA ASCII - // Only rcube_contacts class is using domain names in Unicode - $host = rcube_utils::idn_to_ascii($host); - $username = rcube_utils::idn_to_ascii($username); - - // user already registered -> overwrite username - if ($user = rcube_user::query($username, $host)) { - $username = $user->data['username']; - } - - $storage = $this->get_storage(); - - // try to log in - if (!$storage->connect($host, $username, $pass, $port, $ssl)) { - return false; - } - - // user already registered -> update user's record - if (is_object($user)) { - // update last login timestamp - $user->touch(); - } - // create new system user - else if ($config['auto_create_user']) { - if ($created = rcube_user::create($username, $host)) { - $user = $created; - } - else { - self::raise_error(array( - 'code' => 620, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to create a user record. Maybe aborted by a plugin?" - ), true, false); - } - } - else { - self::raise_error(array( - 'code' => 621, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Access denied for new user $username. 'auto_create_user' is disabled" - ), true, false); - } - - // login succeeded - if (is_object($user) && $user->ID) { - // Configure environment - $this->set_user($user); - $this->set_storage_prop(); - - // fix some old settings according to namespace prefix - $this->fix_namespace_settings($user); - - // create default folders on first login - if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) { - $storage->create_default_folders(); - } - - // set session vars - $_SESSION['user_id'] = $user->ID; - $_SESSION['username'] = $user->data['username']; - $_SESSION['storage_host'] = $host; - $_SESSION['storage_port'] = $port; - $_SESSION['storage_ssl'] = $ssl; - $_SESSION['password'] = $this->encrypt($pass); - $_SESSION['login_time'] = time(); - - if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_') - $_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC); - - // force reloading complete list of subscribed mailboxes - $storage->clear_cache('mailboxes', true); - - return true; - } - - return false; - } - /** * Returns error code of last login operation @@ -634,315 +663,312 @@ } } + /** + * Auto-select IMAP host based on the posted login information + * + * @return string Selected IMAP host + */ + public function autoselect_host() + { + $default_host = $this->config->get('default_host'); + $host = null; - /** - * Auto-select IMAP host based on the posted login information - * - * @return string Selected IMAP host - */ - public function autoselect_host() - { - $default_host = $this->config->get('default_host'); - $host = null; + if (is_array($default_host)) { + $post_host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST); + $post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST); - if (is_array($default_host)) { - $post_host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST); - $post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST); + list(, $domain) = explode('@', $post_user); - list(, $domain) = explode('@', $post_user); + // direct match in default_host array + if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) { + $host = $post_host; + } + // try to select host by mail domain + else if (!empty($domain)) { + foreach ($default_host as $storage_host => $mail_domains) { + if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { + $host = $storage_host; + break; + } + else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { + $host = is_numeric($storage_host) ? $mail_domains : $storage_host; + break; + } + } + } - // direct match in default_host array - if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) { - $host = $post_host; - } - // try to select host by mail domain - else if (!empty($domain)) { - foreach ($default_host as $storage_host => $mail_domains) { - if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { - $host = $storage_host; - break; - } - else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { - $host = is_numeric($storage_host) ? $mail_domains : $storage_host; - break; - } + // take the first entry if $host is still not set + if (empty($host)) { + list($key, $val) = each($default_host); + $host = is_numeric($key) ? $val : $key; + } } - } - - // take the first entry if $host is still not set - if (empty($host)) { - list($key, $val) = each($default_host); - $host = is_numeric($key) ? $val : $key; - } - } - else if (empty($default_host)) { - $host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST); - } - else - $host = rcube_utils::parse_host($default_host); - - return $host; - } - - - /** - * Destroy session data and remove cookie - */ - public function kill_session() - { - $this->plugins->exec_hook('session_destroy'); - - $this->session->kill(); - $_SESSION = array('language' => $this->user->language, 'temp' => true, 'skin' => $this->config->get('skin')); - $this->user->reset(); - } - - - /** - * Do server side actions on logout - */ - public function logout_actions() - { - $config = $this->config->all(); - $storage = $this->get_storage(); - - if ($config['logout_purge'] && !empty($config['trash_mbox'])) { - $storage->clear_folder($config['trash_mbox']); - } - - if ($config['logout_expunge']) { - $storage->expunge_folder('INBOX'); - } - - // Try to save unsaved user preferences - if (!empty($_SESSION['preferences'])) { - $this->user->save_prefs(unserialize($_SESSION['preferences'])); - } - } - - - /** - * Generate a unique token to be used in a form request - * - * @return string The request token - */ - public function get_request_token() - { - $sess_id = $_COOKIE[ini_get('session.name')]; - if (!$sess_id) $sess_id = session_id(); - - $plugin = $this->plugins->exec_hook('request_token', array( - 'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id))); - - return $plugin['value']; - } - - - /** - * Check if the current request contains a valid token - * - * @param int Request method - * @return boolean True if request token is valid false if not - */ - public function check_request($mode = rcube_utils::INPUT_POST) - { - $token = rcube_utils::get_input_value('_token', $mode); - $sess_id = $_COOKIE[ini_get('session.name')]; - return !empty($sess_id) && $token == $this->get_request_token(); - } - - - /** - * Build a valid URL to this instance of Roundcube - * - * @param mixed Either a string with the action or url parameters as key-value pairs - * - * @return string Valid application URL - */ - public function url($p) - { - if (!is_array($p)) { - if (strpos($p, 'http') === 0) - return $p; - - $p = array('_action' => @func_get_arg(0)); - } - - $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task); - $p['_task'] = $task; - unset($p['task']); - - $url = './' . $this->filename; - $delm = '?'; - foreach (array_reverse($p) as $key => $val) { - if ($val !== '' && $val !== null) { - $par = $key[0] == '_' ? $key : '_'.$key; - $url .= $delm.urlencode($par).'='.urlencode($val); - $delm = '&'; - } - } - return $url; - } - - - /** - * Function to be executed in script shutdown - */ - public function shutdown() - { - parent::shutdown(); - - foreach ($this->address_books as $book) { - if (is_object($book) && is_a($book, 'rcube_addressbook')) - $book->close(); - } - - // write performance stats to logs/console - if ($this->config->get('devel_mode')) { - if (function_exists('memory_get_usage')) - $mem = $this->show_bytes(memory_get_usage()); - if (function_exists('memory_get_peak_usage')) - $mem .= '/'.$this->show_bytes(memory_get_peak_usage()); - - $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : ''); - if (defined('RCMAIL_START')) - self::print_timer(RCMAIL_START, $log); - else - self::console($log); - } - } - - - /** - * Registers action aliases for current task - * - * @param array $map Alias-to-filename hash array - */ - public function register_action_map($map) - { - if (is_array($map)) { - foreach ($map as $idx => $val) { - $this->action_map[$idx] = $val; - } - } - } - - - /** - * Returns current action filename - * - * @param array $map Alias-to-filename hash array - */ - public function get_action_file() - { - if (!empty($this->action_map[$this->action])) { - return $this->action_map[$this->action]; - } - - return strtr($this->action, '-', '_') . '.inc'; - } - - - /** - * Fixes some user preferences according to namespace handling change. - * Old Roundcube versions were using folder names with removed namespace prefix. - * Now we need to add the prefix on servers where personal namespace has prefix. - * - * @param rcube_user $user User object - */ - private function fix_namespace_settings($user) - { - $prefix = $this->storage->get_namespace('prefix'); - $prefix_len = strlen($prefix); - - if (!$prefix_len) - return; - - $prefs = $this->config->all(); - if (!empty($prefs['namespace_fixed'])) - return; - - // Build namespace prefix regexp - $ns = $this->storage->get_namespace(); - $regexp = array(); - - foreach ($ns as $entry) { - if (!empty($entry)) { - foreach ($entry as $item) { - if (strlen($item[0])) { - $regexp[] = preg_quote($item[0], '/'); - } + else if (empty($default_host)) { + $host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST); } - } - } - $regexp = '/^('. implode('|', $regexp).')/'; - - // Fix preferences - $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox'); - foreach ($opts as $opt) { - if ($value = $prefs[$opt]) { - if ($value != 'INBOX' && !preg_match($regexp, $value)) { - $prefs[$opt] = $prefix.$value; + else { + $host = rcube_utils::parse_host($default_host); } - } + + return $host; } - if (!empty($prefs['default_folders'])) { - foreach ($prefs['default_folders'] as $idx => $name) { - if ($name != 'INBOX' && !preg_match($regexp, $name)) { - $prefs['default_folders'][$idx] = $prefix.$name; + /** + * Destroy session data and remove cookie + */ + public function kill_session() + { + $this->plugins->exec_hook('session_destroy'); + + $this->session->kill(); + $_SESSION = array('language' => $this->user->language, 'temp' => true, 'skin' => $this->config->get('skin')); + $this->user->reset(); + } + + /** + * Do server side actions on logout + */ + public function logout_actions() + { + $config = $this->config->all(); + $storage = $this->get_storage(); + + if ($config['logout_purge'] && !empty($config['trash_mbox'])) { + $storage->clear_folder($config['trash_mbox']); } - } - } - if (!empty($prefs['search_mods'])) { - $folders = array(); - foreach ($prefs['search_mods'] as $idx => $value) { - if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) { - $idx = $prefix.$idx; + if ($config['logout_expunge']) { + $storage->expunge_folder('INBOX'); } - $folders[$idx] = $value; - } - $prefs['search_mods'] = $folders; - } - if (!empty($prefs['message_threading'])) { - $folders = array(); - foreach ($prefs['message_threading'] as $idx => $value) { - if ($idx != 'INBOX' && !preg_match($regexp, $idx)) { - $idx = $prefix.$idx; + // Try to save unsaved user preferences + if (!empty($_SESSION['preferences'])) { + $this->user->save_prefs(unserialize($_SESSION['preferences'])); } - $folders[$prefix.$idx] = $value; - } - $prefs['message_threading'] = $folders; } - if (!empty($prefs['collapsed_folders'])) { - $folders = explode('&&', $prefs['collapsed_folders']); - $count = count($folders); - $folders_str = ''; + /** + * Generate a unique token to be used in a form request + * + * @return string The request token + */ + public function get_request_token() + { + $sess_id = $_COOKIE[ini_get('session.name')]; - if ($count) { - $folders[0] = substr($folders[0], 1); - $folders[$count-1] = substr($folders[$count-1], 0, -1); - } - - foreach ($folders as $value) { - if ($value != 'INBOX' && !preg_match($regexp, $value)) { - $value = $prefix.$value; + if (!$sess_id) { + $sess_id = session_id(); } - $folders_str .= '&'.$value.'&'; - } - $prefs['collapsed_folders'] = $folders_str; + + $plugin = $this->plugins->exec_hook('request_token', array( + 'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id))); + + return $plugin['value']; } - $prefs['namespace_fixed'] = true; + /** + * Check if the current request contains a valid token + * + * @param int Request method + * + * @return boolean True if request token is valid false if not + */ + public function check_request($mode = rcube_utils::INPUT_POST) + { + $token = rcube_utils::get_input_value('_token', $mode); + $sess_id = $_COOKIE[ini_get('session.name')]; - // save updated preferences and reset imap settings (default folders) - $user->save_prefs($prefs); - $this->set_storage_prop(); - } + return !empty($sess_id) && $token == $this->get_request_token(); + } + /** + * Build a valid URL to this instance of Roundcube + * + * @param mixed Either a string with the action or url parameters as key-value pairs + * + * @return string Valid application URL + */ + public function url($p) + { + if (!is_array($p)) { + if (strpos($p, 'http') === 0) { + return $p; + } + + $p = array('_action' => @func_get_arg(0)); + } + + $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task); + $p['_task'] = $task; + unset($p['task']); + + $url = './' . $this->filename; + $delm = '?'; + + foreach (array_reverse($p) as $key => $val) { + if ($val !== '' && $val !== null) { + $par = $key[0] == '_' ? $key : '_'.$key; + $url .= $delm.urlencode($par).'='.urlencode($val); + $delm = '&'; + } + } + + return $url; + } + + /** + * Function to be executed in script shutdown + */ + public function shutdown() + { + parent::shutdown(); + + foreach ($this->address_books as $book) { + if (is_object($book) && is_a($book, 'rcube_addressbook')) + $book->close(); + } + + // write performance stats to logs/console + if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) { + // make sure logged numbers use unified format + setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); + + if (function_exists('memory_get_usage')) + $mem = $this->show_bytes(memory_get_usage()); + if (function_exists('memory_get_peak_usage')) + $mem .= '/'.$this->show_bytes(memory_get_peak_usage()); + + $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : ''); + + if (defined('RCMAIL_START')) + self::print_timer(RCMAIL_START, $log); + else + self::console($log); + } + } + + /** + * Registers action aliases for current task + * + * @param array $map Alias-to-filename hash array + */ + public function register_action_map($map) + { + if (is_array($map)) { + foreach ($map as $idx => $val) { + $this->action_map[$idx] = $val; + } + } + } + + /** + * Returns current action filename + * + * @param array $map Alias-to-filename hash array + */ + public function get_action_file() + { + if (!empty($this->action_map[$this->action])) { + return $this->action_map[$this->action]; + } + + return strtr($this->action, '-', '_') . '.inc'; + } + + /** + * Fixes some user preferences according to namespace handling change. + * Old Roundcube versions were using folder names with removed namespace prefix. + * Now we need to add the prefix on servers where personal namespace has prefix. + * + * @param rcube_user $user User object + */ + private function fix_namespace_settings($user) + { + $prefix = $this->storage->get_namespace('prefix'); + $prefix_len = strlen($prefix); + + if (!$prefix_len) + return; + + $prefs = $this->config->all(); + if (!empty($prefs['namespace_fixed'])) + return; + + // Build namespace prefix regexp + $ns = $this->storage->get_namespace(); + $regexp = array(); + + foreach ($ns as $entry) { + if (!empty($entry)) { + foreach ($entry as $item) { + if (strlen($item[0])) { + $regexp[] = preg_quote($item[0], '/'); + } + } + } + } + $regexp = '/^('. implode('|', $regexp).')/'; + + // Fix preferences + $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox'); + foreach ($opts as $opt) { + if ($value = $prefs[$opt]) { + if ($value != 'INBOX' && !preg_match($regexp, $value)) { + $prefs[$opt] = $prefix.$value; + } + } + } + + if (!empty($prefs['search_mods'])) { + $folders = array(); + foreach ($prefs['search_mods'] as $idx => $value) { + if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) { + $idx = $prefix.$idx; + } + $folders[$idx] = $value; + } + + $prefs['search_mods'] = $folders; + } + + if (!empty($prefs['message_threading'])) { + $folders = array(); + foreach ($prefs['message_threading'] as $idx => $value) { + if ($idx != 'INBOX' && !preg_match($regexp, $idx)) { + $idx = $prefix.$idx; + } + $folders[$prefix.$idx] = $value; + } + + $prefs['message_threading'] = $folders; + } + + if (!empty($prefs['collapsed_folders'])) { + $folders = explode('&&', $prefs['collapsed_folders']); + $count = count($folders); + $folders_str = ''; + + if ($count) { + $folders[0] = substr($folders[0], 1); + $folders[$count-1] = substr($folders[$count-1], 0, -1); + } + + foreach ($folders as $value) { + if ($value != 'INBOX' && !preg_match($regexp, $value)) { + $value = $prefix.$value; + } + $folders_str .= '&'.$value.'&'; + } + + $prefs['collapsed_folders'] = $folders_str; + } + + $prefs['namespace_fixed'] = true; + + // save updated preferences and reset imap settings (default folders) + $user->save_prefs($prefs); + $this->set_storage_prop(); + } /** * Overwrite action variable @@ -955,6 +981,17 @@ $this->output->set_env('action', $action); } + /** + * Set environment variables for specified config options + */ + public function set_env_config($options) + { + foreach ((array) $options as $option) { + if ($this->config->get($option)) { + $this->output->set_env($option, true); + } + } + } /** * Returns RFC2822 formatted current date in user's timezone @@ -974,7 +1011,6 @@ return $date->format('r'); } - /** * Write login data (name, ID, IP address) to the 'userlogins' log file. @@ -1000,13 +1036,12 @@ } $message = sprintf('Successful login for %s (ID: %d) from %s in session %s', - $user_name, $user_id, rcube_utils::remote_ip(), session_id()); + $user_name, $user_id, rcube_utils::remote_ip(), session_id()); } // log login self::write_log('userlogins', $message); } - /** * Create a HTML table based on the given data @@ -1042,21 +1077,23 @@ } else { foreach ($table_data as $row_data) { - $class = !empty($row_data['class']) ? $row_data['class'] : ''; + $class = !empty($row_data['class']) ? $row_data['class'] : null; + if (!empty($attrib['rowclass'])) + $class = trim($class . ' ' . $attrib['rowclass']); $rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]); $table->add_row(array('id' => $rowid, 'class' => $class)); // format each col foreach ($a_show_cols as $col) { - $table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col])); + $val = is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]; + $table->add($col, empty($attrib['ishtml']) ? $this->Q($val) : $val); } } } return $table->show($attrib); } - /** * Convert the given date to a human readable form @@ -1112,11 +1149,11 @@ $week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']); $pretty_date = $this->config->get('prettydate'); - if ($pretty_date && $timestamp > $today_limit && $timestamp < $now) { + if ($pretty_date && $timestamp > $today_limit && $timestamp <= $now) { $format = $this->config->get('date_today', $this->config->get('time_format', 'H:i')); $today = true; } - else if ($pretty_date && $timestamp > $week_limit && $timestamp < $now) { + else if ($pretty_date && $timestamp > $week_limit && $timestamp <= $now) { $format = $this->config->get('date_short', 'D H:i'); } else { @@ -1186,7 +1223,6 @@ return $out; } - /** * Return folders list in HTML @@ -1259,18 +1295,26 @@ } else { $js_mailboxlist = array(); - $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib); + $tree = $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib); - $rcmail->output->include_script('treelist.js'); - $rcmail->output->add_gui_object('mailboxlist', $attrib['id']); + if ($type != 'js') { + $out = html::tag('ul', $attrib, $tree, html::$common_attrib); + + $rcmail->output->include_script('treelist.js'); + $rcmail->output->add_gui_object('mailboxlist', $attrib['id']); + $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']); + $rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders')); + } + $rcmail->output->set_env('mailboxes', $js_mailboxlist); - $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']); - $rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders')); + + // we can't use object keys in javascript because they are unordered + // we need sorted folders list for folder-selector widget + $rcmail->output->set_env('mailboxes_list', array_keys($js_mailboxlist)); } return $out; } - /** * Return folders list as html_select object @@ -1298,10 +1342,29 @@ $delimiter = $storage->get_hierarchy_delimiter(); - foreach ($list as $folder) { - if (empty($p['exceptions']) || !in_array($folder, $p['exceptions'])) { - $this->build_folder_tree($a_mailboxes, $folder, $delimiter); + if (!empty($p['exceptions'])) { + $list = array_diff($list, (array) $p['exceptions']); + } + + if (!empty($p['additional'])) { + foreach ($p['additional'] as $add_folder) { + $add_items = explode($delimiter, $add_folder); + $folder = ''; + while (count($add_items)) { + $folder .= array_shift($add_items); + + // @TODO: sorting + if (!in_array($folder, $list)) { + $list[] = $folder; + } + + $folder .= $delimiter; + } } + } + + foreach ($list as $folder) { + $this->build_folder_tree($a_mailboxes, $folder, $delimiter); } $select = new html_select($p); @@ -1314,7 +1377,6 @@ return $select; } - /** * Create a hierarchical array of the mailbox list @@ -1372,7 +1434,6 @@ $this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm); } } - /** * Return html for a structured list <ul> for the mailbox tree @@ -1432,7 +1493,7 @@ $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : ''); $link_attrib = $folder['virtual'] ? array() : array( 'href' => $this->url(array('_mbox' => $folder['id'])), - 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::JS_OBJECT_NAME, $js_name), + 'onclick' => sprintf("return %s.command('list','%s',this,event)", rcmail_output::JS_OBJECT_NAME, $js_name), 'rel' => $folder['id'], 'title' => $title, ); @@ -1450,8 +1511,12 @@ $jslist[$folder['id']] = array( 'id' => $folder['id'], 'name' => $foldername, - 'virtual' => $folder['virtual'] + 'virtual' => $folder['virtual'], ); + + if (!empty($folder_class)) { + $jslist[$folder['id']]['class'] = $folder_class; + } if (!empty($folder['folders'])) { $out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), @@ -1463,7 +1528,6 @@ return $out; } - /** * Return html for a flat list <select> for the mailbox tree @@ -1509,7 +1573,6 @@ return $out; } - /** * Return internal name for the given folder if it matches the configured special folders */ @@ -1528,7 +1591,6 @@ } } - /** * Try to localize the given IMAP folder name. * UTF-7 decode it in case no localized text was found @@ -1538,9 +1600,13 @@ * * @return string Localized folder name in UTF-8 encoding */ - public function localize_foldername($name, $with_path = true) + public function localize_foldername($name, $with_path = false) { $realnames = $this->config->get('show_real_foldernames'); + + if (!$realnames && ($folder_class = $this->folder_classname($name))) { + return $this->gettext($folder_class); + } // try to localize path of the folder if ($with_path && !$realnames) { @@ -1550,7 +1616,7 @@ $count = count($path); if ($count > 1) { - for ($i = 0; $i < $count; $i++) { + for ($i = 1; $i < $count; $i++) { $folder = implode($delimiter, array_slice($path, 0, -$i)); if ($folder_class = $this->folder_classname($folder)) { $name = implode($delimiter, array_slice($path, $count - $i)); @@ -1560,10 +1626,6 @@ } } - if (!$realnames && ($folder_class = $this->folder_classname($name))) { - return $this->gettext($folder_class); - } - return rcube_charset::convert($name, 'UTF7-IMAP'); } @@ -1571,14 +1633,13 @@ public function localize_folderpath($path) { $protect_folders = $this->config->get('protect_default_folders'); - $default_folders = (array) $this->config->get('default_folders'); $delimiter = $this->storage->get_hierarchy_delimiter(); $path = explode($delimiter, $path); $result = array(); foreach ($path as $idx => $dir) { $directory = implode($delimiter, array_slice($path, 0, $idx+1)); - if ($protect_folders && in_array($directory, $default_folders)) { + if ($protect_folders && $this->storage->is_special_folder($directory)) { unset($result); $result[] = $this->localize_foldername($directory); } @@ -1607,7 +1668,7 @@ $rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready'); - return html::span($attrib, ''); + return html::span($attrib, ' '); } @@ -1645,7 +1706,6 @@ return $quota_result; } - /** * Outputs error message according to server error/response codes @@ -1700,7 +1760,6 @@ } } - /** * Output HTML editor scripts * @@ -1714,33 +1773,37 @@ return; } - $lang = strtolower($_SESSION['language']); + $lang_codes = array($_SESSION['language']); - // TinyMCE uses two-letter lang codes, with exception of Chinese - if (strpos($lang, 'zh_') === 0) { - $lang = str_replace('_', '-', $lang); - } - else { - $lang = substr($lang, 0, 2); + if ($pos = strpos($_SESSION['language'], '_')) { + $lang_codes[] = substr($_SESSION['language'], 0, $pos); } - if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) { + foreach ($lang_codes as $code) { + if (file_exists(INSTALL_PATH . 'program/js/tinymce/langs/'.$code.'.js')) { + $lang = $code; + break; + } + } + + if (empty($lang)) { $lang = 'en'; } - $script = json_encode(array( + $config = array( 'mode' => $mode, 'lang' => $lang, 'skin_path' => $this->output->get_skin_path(), 'spellcheck' => intval($this->config->get('enable_spellcheck')), 'spelldict' => intval($this->config->get('spellcheck_dictionary')) - )); + ); - $this->output->include_script('tiny_mce/tiny_mce.js'); + $this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia'); + $this->output->set_env('editor_config', $config); + $this->output->include_css('program/js/tinymce/roundcube/browser.css'); + $this->output->include_script('tinymce/tinymce.min.js'); $this->output->include_script('editor.js'); - $this->output->add_script("rcmail_editor_init($script)", 'docready'); } - /** * Replaces TinyMCE's emoticon images with plain-text representation @@ -1771,46 +1834,69 @@ ); foreach ($emoticons as $idx => $file) { - // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" /> - $search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i'; + // <img title="Cry" src="http://.../program/js/tinymce/plugins/emoticons/img/smiley-cry.gif" border="0" alt="Cry" /> + $search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tinymce\/plugins\/emoticons\/img\/'.$file.'.gif"[^>]+\/>/i'; $replace[] = $idx; } return preg_replace($search, $replace, $html); } - /** * File upload progress handler. */ public function upload_progress() { - $prefix = ini_get('apc.rfc1867_prefix'); $params = array( 'action' => $this->action, - 'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET), + 'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET), ); - if (function_exists('apc_fetch')) { - $status = apc_fetch($prefix . $params['name']); + if (function_exists('uploadprogress_get_info')) { + $status = uploadprogress_get_info($params['name']); if (!empty($status)) { - $status['percent'] = round($status['current']/$status['total']*100); - $params = array_merge($status, $params); + $params['current'] = $status['bytes_uploaded']; + $params['total'] = $status['bytes_total']; } } - if (isset($params['percent'])) - $params['text'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array( - 'percent' => $params['percent'] . '%', - 'current' => $this->show_bytes($params['current']), - 'total' => $this->show_bytes($params['total']) - ))); + if (!isset($status) && filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN) + && ini_get('apc.rfc1867_name') + ) { + $prefix = ini_get('apc.rfc1867_prefix'); + $status = apc_fetch($prefix . $params['name']); + + if (!empty($status)) { + $params['current'] = $status['current']; + $params['total'] = $status['total']; + } + } + + if (!isset($status) && filter_var(ini_get('session.upload_progress.enabled'), FILTER_VALIDATE_BOOLEAN) + && ini_get('session.upload_progress.name') + ) { + $key = ini_get('session.upload_progress.prefix') . $params['name']; + + $params['total'] = $_SESSION[$key]['content_length']; + $params['current'] = $_SESSION[$key]['bytes_processed']; + } + + if (!empty($params['total'])) { + $params['percent'] = round($status['current']/$status['total']*100); + $params['text'] = $this->gettext(array( + 'name' => 'uploadprogress', + 'vars' => array( + 'percent' => $params['percent'] . '%', + 'current' => $this->show_bytes($params['current']), + 'total' => $this->show_bytes($params['total']) + ) + )); + } $this->output->command('upload_progress_update', $params); $this->output->send(); } - /** * Initializes file uploading interface. @@ -1818,9 +1904,18 @@ public function upload_init() { // Enable upload progress bar - $rfc1867 = filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN); - if ($rfc1867 && ($seconds = $this->config->get('upload_progress'))) { - if ($field_name = ini_get('apc.rfc1867_name')) { + if ($seconds = $this->config->get('upload_progress')) { + if (function_exists('uploadprogress_get_info')) { + $field_name = 'UPLOAD_IDENTIFIER'; + } + if (!$field_name && filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN)) { + $field_name = ini_get('apc.rfc1867_name'); + } + if (!$field_name && filter_var(ini_get('session.upload_progress.enabled'), FILTER_VALIDATE_BOOLEAN)) { + $field_name = ini_get('session.upload_progress.name'); + } + + if ($field_name) { $this->output->set_env('upload_progress_name', $field_name); $this->output->set_env('upload_progress_time', (int) $seconds); } @@ -1829,18 +1924,18 @@ // find max filesize value $max_filesize = parse_bytes(ini_get('upload_max_filesize')); $max_postsize = parse_bytes(ini_get('post_max_size')); + if ($max_postsize && $max_postsize < $max_filesize) { $max_filesize = $max_postsize; } $this->output->set_env('max_filesize', $max_filesize); - $max_filesize = self::show_bytes($max_filesize); + $max_filesize = $this->show_bytes($max_filesize); $this->output->set_env('filesizeerror', $this->gettext(array( 'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize)))); return $max_filesize; } - /** * Initializes client-side autocompletion. @@ -1867,7 +1962,6 @@ $this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length')); $this->output->add_label('autocompletechars', 'autocompletemore'); } - /** * Returns supported font-family specifications @@ -1901,7 +1995,6 @@ return $fonts; } - /** * Create a human readable string for a number of bytes * @@ -1929,7 +2022,6 @@ return $str; } - /** * Returns real size (calculated) of the message part * @@ -1954,6 +2046,55 @@ return $size; } + /** + * Returns message UID(s) and IMAP folder(s) from GET/POST data + * + * @param string UID value to decode + * @param string Default mailbox value (if not encoded in UIDs) + * @return array List of message UIDs per folder + */ + public static function get_uids($uids = null, $mbox = null) + { + // message UID (or comma-separated list of IDs) is provided in + // the form of <ID>-<MBOX>[,<ID>-<MBOX>]* + + $_uid = $uids ?: rcube_utils::get_input_value('_uid', RCUBE_INPUT_GPC); + $_mbox = $mbox ?: (string)rcube_utils::get_input_value('_mbox', RCUBE_INPUT_GPC); + + // already a hash array + if (is_array($_uid) && !isset($_uid[0])) { + return $_uid; + } + + $result = array(); + + // special case: * + if ($_uid == '*' && is_object($_SESSION['search'][1]) && $_SESSION['search'][1]->multi) { + // extract the full list of UIDs per folder from the search set + foreach ($_SESSION['search'][1]->sets as $subset) { + $mbox = $subset->get_parameters('MAILBOX'); + $result[$mbox] = $subset->get(); + } + } + else { + if (is_string($_uid)) + $_uid = explode(',', $_uid); + + // create a per-folder UIDs array + foreach ((array)$_uid as $uid) { + list($uid, $mbox) = explode('-', $uid, 2); + if (!strlen($mbox)) + $mbox = $_mbox; + if ($uid == '*') + $result[$mbox] = $uid; + else + $result[$mbox][] = $uid; + } + } + + return $result; + } + /************************************************************************ ********* Deprecated methods (to be removed) ********* -- Gitblit v1.9.1