Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2527809
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
143 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 535da2e09..83f945fce 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,2578 +1,2583 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Application class providing core functions and holding |
| instances of all 'global' objects like db- and imap-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Application class of Roundcube Webmail
* implemented as singleton
*
* @package Webmail
*/
class rcmail extends rcube
{
/**
* Main tasks.
*
* @var array
*/
static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
/**
* Current task.
*
* @var string
*/
public $task;
/**
* Current action.
*
* @var string
*/
public $action = '';
public $comm_path = './';
public $filename = '';
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_RATE_LIMIT = 4;
/**
* This implements the 'singleton' design pattern
*
* @param integer $mode Ignored rcube::get_instance() argument
* @param string $env Environment name to run (e.g. live, dev, test)
*
* @return rcmail The one and only instance
*/
static function get_instance($mode = 0, $env = '')
{
if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
+ // In cli-server mode env=test
+ if ($env === null && php_sapi_name() == 'cli-server') {
+ $env = 'test';
+ }
+
self::$instance = new rcmail($env);
// init AFTER object was linked with self::$instance
self::$instance->startup();
}
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;
}
// load all configured plugins
$plugins = (array) $this->config->get('plugins', array());
$required_plugins = array('filesystem_attachments', 'jqueryui');
$this->plugins->load_plugins($plugins, $required_plugins);
// start session
$this->session_init();
// Remember default skin, before it's replaced by user prefs
$this->default_skin = $this->config->get('skin');
// 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, #1490116)
// 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 (not in CLI mode)
if (!empty($_REQUEST['_remote'])) {
$GLOBALS['OUTPUT'] = $this->json_init();
}
else if ($_SERVER['REMOTE_ADDR']) {
$GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
}
// run init method on all the plugins
$this->plugins->init($this, $this->task);
}
/**
* Setter for application task
*
* @param string $task Task to set
*/
public function set_task($task)
{
if (php_sapi_name() == 'cli') {
$task = 'cli';
}
else if (!$this->user || !$this->user->ID) {
$task = 'login';
}
else {
$task = asciiwords($task, true) ?: 'mail';
}
$this->task = $task;
$this->comm_path = $this->url(array('task' => $this->task));
if (!empty($_REQUEST['_framed'])) {
$this->comm_path .= '&_framed=1';
}
if ($this->output) {
$this->output->set_env('task', $this->task);
$this->output->set_env('comm_path', $this->comm_path);
}
}
/**
* Setter for system user object
*
* @param rcube_user $user Current user instance
*/
public function set_user($user)
{
parent::set_user($user);
$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
// Also strtoupper/strtolower and other methods are locale-aware
// for these locales it is problematic (#1490519)
if (in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8', 'C');
}
}
/**
* Return instance of the internal address book class
*
* @param string $id Address book identifier (-1 for default addressbook)
* @param boolean $writeable 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 $object 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 $writeable True if the address book needs to be writeable
* @param boolean $skip_hidden 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 = strtolower((string) $this->config->get('address_book_type', 'sql'));
$ldap_config = (array) $this->config->get('ldap_public');
$autocomplete = (array) $this->config->get('autocomplete_addressbooks');
$list = array();
// SQL-based (built-in) address book
if ($abook_type === 'sql') {
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_nocase('sql', $autocomplete),
);
}
// LDAP address book(s)
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)
);
}
}
// Plugins can also add address books, or re-order the list
$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 $sorted True to sort the list alphabetically
* @param boolean $user_only 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-' . mb_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 ? mb_strtolower($response['name']) : $response['key'];
$responses[$k] = $response;
}
// sort list by name
if ($sorted) {
ksort($responses, SORT_LOCALE_STRING);
}
$responses = array_values($responses);
$hook = $this->plugins->exec_hook('get_compose_responses', array(
'list' => $responses,
'sorted' => $sorted,
'user_only' => $user_only,
));
return $hook['list'];
}
/**
* 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 $framed 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());
}
// set compose mode for all tasks (message compose step can be triggered from everywhere)
$this->output->set_env('compose_extwin', $this->config->get('compose_extwin',false));
// add some basic labels to client
$this->output->add_label('loading', 'servererror', 'connerror', 'requesttimedout',
'refreshing', 'windowopenerror', 'uploadingmany', 'uploading', 'close', 'save', 'cancel',
'alerttitle', 'confirmationtitle', 'delete', 'continue', 'ok');
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;
}
}
/**
* Perform 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 $username Mail storage (IMAP) user name
* @param string $password Mail storage (IMAP) password
* @param string $host Mail storage (IMAP) host
* @param bool $cookiecheck Enables cookie check
*
* @return boolean True on success, False on failure
*/
function login($username, $password, $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;
}
$username_filter = $this->config->get('login_username_filter');
$username_maxlen = $this->config->get('login_username_maxlen', 1024);
$password_maxlen = $this->config->get('login_password_maxlen', 1024);
$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);
// check input for security (#1490500)
if (($username_maxlen && strlen($username) > $username_maxlen)
|| ($username_filter && !preg_match($username_filter, $username))
|| ($password_maxlen && strlen($password) > $password_maxlen)
) {
$this->login_error = self::ERROR_INVALID_REQUEST;
return false;
}
// 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)) {
$key = key($default_host);
$host = is_numeric($key) ? $default_host[$key] : $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);
if (strpos($username, '@')) {
$username = rcube_utils::idn_to_ascii($username);
}
// user already registered -> overwrite username
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
// Brute-force prevention
if ($user->is_locked()) {
$this->login_error = self::ERROR_RATE_LIMIT;
return false;
}
}
$storage = $this->get_storage();
// try to log in
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
if ($user) {
$user->failed_login();
}
// Wait a second to slow down brute-force attacks (#1490549)
sleep(1);
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($password);
$_SESSION['login_time'] = time();
$timezone = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC);
if ($timezone && is_string($timezone) && $timezone != '_default_') {
$_SESSION['timezone'] = $timezone;
}
// 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;
}
/**
* Returns error code of last login operation
*
* @return int Error code
*/
public function login_error()
{
if ($this->login_error) {
return $this->login_error;
}
if ($this->storage && $this->storage->get_error_code() < -1) {
return self::ERROR_STORAGE;
}
}
/**
* Detects session errors
*
* @return string Error label
*/
public function session_error()
{
// log session failures
$task = rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC);
if ($task && !in_array($task, array('login', 'logout')) && ($sess_id = $_COOKIE[ini_get('session.name')])) {
$log = "Aborted session $sess_id; no valid session data found";
$error = 'sessionerror';
// In rare cases web browser might end up with multiple cookies of the same name
// but different params, e.g. domain (webmail.domain.tld and .webmail.domain.tld).
// In such case browser will send both cookies in the request header
// problem is that PHP session handler can use only one and if that one session
// does not exist we'll end up here
$cookie = rcube_utils::request_header('Cookie');
$cookie_sessid = $this->config->get('session_name') ?: 'roundcube_sessid';
$cookie_sessauth = $this->config->get('session_auth_name') ?: 'roundcube_sessauth';
if (substr_count($cookie, $cookie_sessid.'=') > 1 || substr_count($cookie, $cookie_sessauth.'=') > 1) {
$log .= ". Cookies mismatch";
$error = 'cookiesmismatch';
}
$this->session->log($log);
return $error;
}
}
/**
* 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);
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;
}
}
}
// take the first entry if $host is still not set
if (empty($host)) {
$key = key($default_host);
$host = is_numeric($key) ? $default_host[$key] : $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);
$this->user->reset();
if ($this->config->get('skin') != $this->default_skin && method_exists($this->output, 'set_skin')) {
$this->output->set_skin($this->default_skin);
}
}
/**
* Do server side actions on logout
*/
public function logout_actions()
{
$storage = $this->get_storage();
$logout_expunge = $this->config->get('logout_expunge');
$logout_purge = $this->config->get('logout_purge');
$trash_mbox = $this->config->get('trash_mbox');
if ($logout_purge && !empty($trash_mbox)) {
$storage->clear_folder($trash_mbox);
}
if ($logout_expunge) {
$storage->expunge_folder('INBOX');
}
// Try to save unsaved user preferences
if (!empty($_SESSION['preferences'])) {
$this->user->save_prefs(unserialize($_SESSION['preferences']));
}
}
/**
* Build a valid URL to this instance of Roundcube
*
* @param mixed $p Either a string with the action or
* url parameters as key-value pairs
* @param boolean $absolute Build an URL absolute to document root
* @param boolean $full Create fully qualified URL including http(s):// and hostname
* @param bool $secure Return absolute URL in secure location
*
* @return string Valid application URL
*/
public function url($p, $absolute = false, $full = false, $secure = false)
{
if (!is_array($p)) {
if (strpos($p, 'http') === 0) {
return $p;
}
$p = array('_action' => @func_get_arg(0));
}
$pre = array();
$task = $p['_task'] ?: ($p['task'] ?: $this->task);
$pre['_task'] = $task;
unset($p['task'], $p['_task']);
$url = $this->filename;
$delm = '?';
foreach (array_merge($pre, $p) as $key => $val) {
if ($val !== '' && $val !== null) {
$par = $key[0] == '_' ? $key : '_'.$key;
$url .= $delm.urlencode($par).'='.urlencode($val);
$delm = '&';
}
}
$base_path = strval($_SERVER['REDIRECT_SCRIPT_URL'] ?: $_SERVER['SCRIPT_NAME']);
$base_path = preg_replace('![^/]+$!', '', $base_path);
if ($secure && ($token = $this->get_secure_url_token(true))) {
// add token to the url
$url = $token . '/' . $url;
// remove old token from the path
$base_path = rtrim($base_path, '/');
$base_path = preg_replace('/\/[a-zA-Z0-9]{' . strlen($token) . '}$/', '', $base_path);
// this need to be full url to make redirects work
$absolute = true;
}
else if ($secure && ($token = $this->get_request_token()))
$url .= $delm . '_token=' . urlencode($token);
if ($absolute || $full) {
// add base path to this Roundcube installation
if ($base_path == '') $base_path = '/';
$prefix = $base_path;
// prepend protocol://hostname:port
if ($full) {
$prefix = rcube_utils::resolve_url($prefix);
}
$prefix = rtrim($prefix, '/') . '/';
}
else {
$prefix = './';
}
return $prefix . $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')) {
// we have to disable per_user_logging to make sure stats end up in the main console log
$this->config->set('per_user_logging', false);
// 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 = round(memory_get_usage() / 1024 /1024, 1);
}
if (function_exists('memory_get_peak_usage')) {
$mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
}
$log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
if (defined('RCMAIL_START')) {
self::print_timer(RCMAIL_START, $log);
}
else {
self::console($log);
}
}
}
/**
* CSRF attack prevention code. Raises error when check fails.
*
* @param int $mode Request mode
*/
public function request_security_check($mode = rcube_utils::INPUT_POST)
{
// check request token
if (!$this->check_request($mode)) {
$error = array('code' => 403, 'message' => "Request security check failed");
self::raise_error($error, false, true);
}
}
/**
* 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;
}
if ($this->config->get('namespace_fixed')) {
return;
}
$prefs = array();
// 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 = $this->config->get($opt)) {
if ($value != 'INBOX' && !preg_match($regexp, $value)) {
$prefs[$opt] = $prefix.$value;
}
}
}
if (($search_mods = $this->config->get('search_mods')) && !empty($search_mods)) {
$folders = array();
foreach ($search_mods as $idx => $value) {
if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$idx] = $value;
}
$prefs['search_mods'] = $folders;
}
if (($threading = $this->config->get('message_threading')) && !empty($threading)) {
$folders = array();
foreach ($threading as $idx => $value) {
if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$prefix.$idx] = $value;
}
$prefs['message_threading'] = $folders;
}
if ($collapsed = $this->config->get('collapsed_folders')) {
$folders = explode('&&', $collapsed);
$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
*
* @param string $action New action value
*/
public function overwrite_action($action)
{
$this->action = $action;
$this->output->set_env('action', $action);
}
/**
* Set environment variables for specified config options
*
* @param array $options List of configuration option names
*/
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
*
* @return string Date
*/
public function user_date()
{
// get user's timezone
try {
$tz = new DateTimeZone($this->config->get('timezone'));
$date = new DateTime('now', $tz);
}
catch (Exception $e) {
$date = new DateTime();
}
return $date->format('r');
}
/**
* Write login data (name, ID, IP address) to the 'userlogins' log file.
*/
public function log_login($user = null, $failed_login = false, $error_code = 0)
{
if (!$this->config->get('log_logins')) {
return;
}
// don't log full session id for security reasons
$session_id = session_id();
$session_id = $session_id ? substr($session_id, 0, 16) : 'no-session';
// failed login
if ($failed_login) {
// don't fill the log with complete input, which could
// have been prepared by a hacker
if (strlen($user) > 256) {
$user = substr($user, 0, 256) . '...';
}
$message = sprintf('Failed login for %s from %s in session %s (error: %d)',
$user, rcube_utils::remote_ip(), $session_id, $error_code);
}
// successful login
else {
$user_name = $this->get_user_name();
$user_id = $this->get_user_id();
if (!$user_id) {
return;
}
$message = sprintf('Successful login for %s (ID: %d) from %s in session %s',
$user_name, $user_id, rcube_utils::remote_ip(), $session_id);
}
// log login
self::write_log('userlogins', $message);
}
/**
* Check if specified asset file exists
*
* @param string $path Asset path
* @param bool $minified Fallback to minified version of the file
*
* @return string Asset path if found (modified if minified file found)
*/
public function find_asset($path, $minified = true)
{
if (empty($path)) {
return;
}
$assets_dir = $this->config->get('assets_dir');
$root_path = unslashify($assets_dir ?: INSTALL_PATH) . '/';
$full_path = $root_path . trim($path, '/');
if (file_exists($full_path)) {
return $path;
}
if ($minified && preg_match('/(?<!\.min)\.(js|css)$/', $path)) {
$path = preg_replace('/\.(js|css)$/', '.min.\\1', $path);
$full_path = $root_path . trim($path, '/');
if (file_exists($full_path)) {
return $path;
}
}
}
/**
* Create a HTML table based on the given data
*
* @param array $attrib Named table attributes
* @param mixed $table_data Table row data. Either a two-dimensional array
* or a valid SQL result set
* @param array $show_cols List of cols to show
* @param string $id_col Name of the identifier col
*
* @return string HTML table code
*/
public function table_output($attrib, $table_data, $show_cols, $id_col)
{
$table = new html_table($attrib);
// add table header
if (!$attrib['noheader']) {
foreach ($show_cols as $col) {
$table->add_header($col, $this->Q($this->gettext($col)));
}
}
if (!is_array($table_data)) {
$db = $this->get_dbh();
while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
$table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
// format each col
foreach ($show_cols as $col) {
$table->add($col, $this->Q($sql_arr[$col]));
}
}
}
else {
foreach ($table_data as $row_data) {
$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 ($show_cols as $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
* This uses the date formatting properties from config
*
* @param mixed $date Date representation (string, timestamp or DateTime object)
* @param string $format Date format to use
* @param bool $convert Enables date conversion according to user timezone
*
* @return string Formatted date string
*/
public function format_date($date, $format = null, $convert = true)
{
if (is_object($date) && is_a($date, 'DateTime')) {
$timestamp = $date->format('U');
}
else {
if (!empty($date)) {
$timestamp = rcube_utils::strtotime($date);
}
if (empty($timestamp)) {
return '';
}
try {
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return '';
}
}
if ($convert) {
try {
// convert to the right timezone
$stz = date_default_timezone_get();
$tz = new DateTimeZone($this->config->get('timezone'));
$date->setTimezone($tz);
date_default_timezone_set($tz->getName());
$timestamp = $date->format('U');
}
catch (Exception $e) {
}
}
// define date format depending on current time
if (!$format) {
$now = time();
$now_date = getdate($now);
$today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
$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) {
$format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
$today = true;
}
else if ($pretty_date && $timestamp > $week_limit && $timestamp <= $now) {
$format = $this->config->get('date_short', 'D H:i');
}
else {
$format = $this->config->get('date_long', 'Y-m-d H:i');
}
}
// strftime() format
if (preg_match('/%[a-z]+/i', $format)) {
$format = strftime($format, $timestamp);
if ($stz) {
date_default_timezone_set($stz);
}
return $today ? ($this->gettext('today') . ' ' . $format) : $format;
}
// parse format string manually in order to provide localized weekday and month names
// an alternative would be to convert the date() format string to fit with strftime()
$out = '';
for ($i=0; $i<strlen($format); $i++) {
if ($format[$i] == "\\") { // skip escape chars
continue;
}
// write char "as-is"
if ($format[$i] == ' ' || $format[$i-1] == "\\") {
$out .= $format[$i];
}
// weekday (short)
else if ($format[$i] == 'D') {
$out .= $this->gettext(strtolower(date('D', $timestamp)));
}
// weekday long
else if ($format[$i] == 'l') {
$out .= $this->gettext(strtolower(date('l', $timestamp)));
}
// month name (short)
else if ($format[$i] == 'M') {
$out .= $this->gettext(strtolower(date('M', $timestamp)));
}
// month name (long)
else if ($format[$i] == 'F') {
$out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
}
else if ($format[$i] == 'x') {
$out .= strftime('%x %X', $timestamp);
}
else {
$out .= date($format[$i], $timestamp);
}
}
if ($today) {
$label = $this->gettext('today');
// replcae $ character with "Today" label (#1486120)
if (strpos($out, '$') !== false) {
$out = preg_replace('/\$/', $label, $out, 1);
}
else {
$out = $label . ' ' . $out;
}
}
if ($stz) {
date_default_timezone_set($stz);
}
return $out;
}
/**
* Return folders list in HTML
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function folder_list($attrib)
{
static $a_mailboxes;
$attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
$type = $attrib['type'] ? $attrib['type'] : 'ul';
unset($attrib['type']);
if ($type == 'ul' && !$attrib['id']) {
$attrib['id'] = 'rcmboxlist';
}
if (empty($attrib['folder_name'])) {
$attrib['folder_name'] = '*';
}
// get current folder
$storage = $this->get_storage();
$mbox_name = $storage->get_folder();
// build the folders tree
if (empty($a_mailboxes)) {
// get mailbox list
$a_folders = $storage->list_folders_subscribed(
'', $attrib['folder_name'], $attrib['folder_filter']);
$delimiter = $storage->get_hierarchy_delimiter();
$a_mailboxes = array();
foreach ($a_folders as $folder) {
$this->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $this->plugins->exec_hook('render_mailboxlist', array(
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'type' => $type,
'attribs' => $attrib,
));
$a_mailboxes = $hook['list'];
$attrib = $hook['attribs'];
if ($type == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
// add no-selection option
if ($attrib['noselection']) {
$select->add(html::quote($this->gettext($attrib['noselection'])), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
$out = $select->show($attrib['default']);
}
else {
$js_mailboxlist = array();
$tree = $this->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
if ($type != 'js') {
$out = html::tag('ul', $attrib, $tree, html::$common_attrib);
$this->output->include_script('treelist.js');
$this->output->add_gui_object('mailboxlist', $attrib['id']);
$this->output->set_env('unreadwrap', $attrib['unreadwrap']);
$this->output->set_env('collapsed_folders', (string) $this->config->get('collapsed_folders'));
}
$this->output->set_env('mailboxes', $js_mailboxlist);
// we can't use object keys in javascript because they are unordered
// we need sorted folders list for folder-selector widget
$this->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
}
// add some labels to client
$this->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
return $out;
}
/**
* Return folders list as html_select object
*
* @param array $p Named parameters
*
* @return html_select HTML drop-down object
*/
public function folder_selector($p = array())
{
$realnames = $this->config->get('show_real_foldernames');
$p += array('maxlength' => 100, 'realnames' => $realnames, 'is_escaped' => true);
$a_mailboxes = array();
$storage = $this->get_storage();
if (empty($p['folder_name'])) {
$p['folder_name'] = '*';
}
if ($p['unsubscribed']) {
$list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
else {
$list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
$delimiter = $storage->get_hierarchy_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);
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $this->plugins->exec_hook('render_folder_selector', array(
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'attribs' => $p,
));
$a_mailboxes = $hook['list'];
$p = $hook['attribs'];
$select = new html_select($p);
if ($p['noselection']) {
$select->add(html::quote($p['noselection']), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
return $select;
}
/**
* Create a hierarchical array of the mailbox list
*/
public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
{
// Handle namespace prefix
$prefix = '';
if (!$path) {
$n_folder = $folder;
$folder = $this->storage->mod_folder($folder);
if ($n_folder != $folder) {
$prefix = substr($n_folder, 0, -strlen($folder));
}
}
$pos = strpos($folder, $delm);
if ($pos !== false) {
$subFolders = substr($folder, $pos+1);
$currentFolder = substr($folder, 0, $pos);
// sometimes folder has a delimiter as the last character
if (!strlen($subFolders)) {
$virtual = false;
}
else if (!isset($arrFolders[$currentFolder])) {
$virtual = true;
}
else {
$virtual = $arrFolders[$currentFolder]['virtual'];
}
}
else {
$subFolders = false;
$currentFolder = $folder;
$virtual = false;
}
$path .= $prefix . $currentFolder;
if (!isset($arrFolders[$currentFolder])) {
$arrFolders[$currentFolder] = array(
'id' => $path,
'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
'virtual' => $virtual,
'folders' => array()
);
}
else {
$arrFolders[$currentFolder]['virtual'] = $virtual;
}
if (strlen($subFolders)) {
$this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
}
}
/**
* Return html for a structured list <ul> for the mailbox tree
*/
public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
{
$maxlength = intval($attrib['maxlength']);
$realnames = (bool)$attrib['realnames'];
$msgcounts = $this->storage->get_cache('messagecount');
$collapsed = $this->config->get('collapsed_folders');
$realnames = $this->config->get('show_real_foldernames');
$out = '';
foreach ($arrFolders as $folder) {
$title = null;
$folder_class = $this->folder_classname($folder['id']);
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
if ($folder_class && !$realnames) {
$foldername = $this->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$fname = abbreviate_string($foldername, $maxlength);
if ($fname != $foldername) {
$title = $foldername;
}
$foldername = $fname;
}
}
// make folder name safe for ids and class names
$folder_id = rcube_utils::html_identifier($folder['id'], true);
$classes = array('mailbox');
// set special class for Sent, Drafts, Trash and Junk
if ($folder_class) {
$classes[] = $folder_class;
}
if ($folder['id'] == $mbox_name) {
$classes[] = 'selected';
}
if ($folder['virtual']) {
$classes[] = 'virtual';
}
else if ($unread) {
$classes[] = 'unread';
}
$js_name = $this->JQ($folder['id']);
$html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount skip-content', 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,event)", rcmail_output::JS_OBJECT_NAME, $js_name),
'rel' => $folder['id'],
'title' => $title,
);
$out .= html::tag('li', array(
'id' => "rcmli" . $folder_id,
'class' => implode(' ', $classes),
'noclose' => true
),
html::a($link_attrib, $html_name));
if (!empty($folder['folders'])) {
$out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' ');
}
$jslist[$folder['id']] = array(
'id' => $folder['id'],
'name' => $foldername,
'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)),
$this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
}
$out .= "</li>\n";
}
return $out;
}
/**
* Return html for a flat list <select> for the mailbox tree
*/
public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
{
$out = '';
foreach ($arrFolders as $folder) {
// skip exceptions (and its subfolders)
if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
continue;
}
// skip folders in which it isn't possible to create subfolders
if (!empty($opts['skip_noinferiors'])) {
$attrs = $this->storage->folder_attributes($folder['id']);
if ($attrs && in_array_nocase('\\Noinferiors', $attrs)) {
continue;
}
}
if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
$foldername = $this->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$foldername = abbreviate_string($foldername, $maxlength);
}
}
$select->add(str_repeat(' ', $nestLevel*4) . html::quote($foldername), $folder['id']);
if (!empty($folder['folders'])) {
$out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
$select, $realnames, $nestLevel+1, $opts);
}
}
return $out;
}
/**
* Return internal name for the given folder if it matches the configured special folders
*/
public function folder_classname($folder_id)
{
if ($folder_id == 'INBOX') {
return 'inbox';
}
// for these mailboxes we have localized labels and css classes
foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
{
if ($folder_id === $this->config->get($smbx.'_mbox')) {
return $smbx;
}
}
}
/**
* Try to localize the given IMAP folder name.
* UTF-7 decode it in case no localized text was found
*
* @param string $name Folder name
* @param bool $with_path Enable path localization
* @param bool $path_remove Remove the path
*
* @return string Localized folder name in UTF-8 encoding
*/
public function localize_foldername($name, $with_path = false, $path_remove = false)
{
$realnames = $this->config->get('show_real_foldernames');
if (!$realnames && ($folder_class = $this->folder_classname($name))) {
return $this->gettext($folder_class);
}
$storage = $this->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
// Remove the path
if ($path_remove) {
if (strpos($name, $delimiter)) {
$path = explode($delimiter, $name);
$name = array_pop($path);
}
}
// try to localize path of the folder
else if ($with_path && !$realnames) {
$path = explode($delimiter, $name);
$count = count($path);
if ($count > 1) {
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));
$name = rcube_charset::convert($name, 'UTF7-IMAP');
return $this->gettext($folder_class) . $delimiter . $name;
}
}
}
}
return rcube_charset::convert($name, 'UTF7-IMAP');
}
/**
* Localize folder path
*/
public function localize_folderpath($path)
{
$protect_folders = $this->config->get('protect_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 && $this->storage->is_special_folder($directory)) {
unset($result);
$result[] = $this->localize_foldername($directory);
}
else {
$result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
}
}
return implode($delimiter, $result);
}
/**
* Return HTML for quota indicator object
*
* @param array $attrib Named parameters
*
* @return string HTML code for the quota indicator object
*/
public static function quota_display($attrib)
{
$rcmail = rcmail::get_instance();
if (!$attrib['id']) {
$attrib['id'] = 'rcmquotadisplay';
}
$_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
$rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
$quota = $rcmail->quota_content($attrib);
$rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
return html::span($attrib, ' ');
}
/**
* Return (parsed) quota information
*
* @param array $attrib Named parameters
* @param array $folder Current folder
*
* @return array Quota information
*/
public function quota_content($attrib = null, $folder = null)
{
$quota = $this->storage->get_quota($folder);
$quota = $this->plugins->exec_hook('quota', $quota);
$quota_result = (array) $quota;
$quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
$quota_result['folder'] = $folder !== null && $folder !== '' ? $folder : 'INBOX';
if ($quota['total'] > 0) {
if (!isset($quota['percent'])) {
$quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
}
$title = $this->gettext('quota') . ': ' . sprintf('%s / %s (%.0f%%)',
$this->show_bytes($quota['used'] * 1024),
$this->show_bytes($quota['total'] * 1024),
$quota_result['percent']
);
$quota_result['title'] = $title;
if ($attrib['width']) {
$quota_result['width'] = $attrib['width'];
}
if ($attrib['height']) {
$quota_result['height'] = $attrib['height'];
}
// build a table of quota types/roots info
if (($root_cnt = count($quota_result['all'])) > 1 || count($quota_result['all'][key($quota_result['all'])]) > 1) {
$table = new html_table(array('cols' => 3, 'class' => 'quota-info'));
$table->add_header(null, self::Q($this->gettext('quotatype')));
$table->add_header(null, self::Q($this->gettext('quotatotal')));
$table->add_header(null, self::Q($this->gettext('quotaused')));
foreach ($quota_result['all'] as $root => $data) {
if ($root_cnt > 1 && $root) {
$table->add(array('colspan' => 3, 'class' => 'root'), self::Q($root));
}
if ($storage = $data['storage']) {
$percent = min(100, round(($storage['used']/max(1,$storage['total']))*100));
$table->add('name', self::Q($this->gettext('quotastorage')));
$table->add(null, $this->show_bytes($storage['total'] * 1024));
$table->add(null, sprintf('%s (%.0f%%)', $this->show_bytes($storage['used'] * 1024), $percent));
}
if ($message = $data['message']) {
$percent = min(100, round(($message['used']/max(1,$message['total']))*100));
$table->add('name', self::Q($this->gettext('quotamessage')));
$table->add(null, intval($message['total']));
$table->add(null, sprintf('%d (%.0f%%)', $message['used'], $percent));
}
}
$quota_result['table'] = $table->show();
}
}
else {
$unlimited = $this->config->get('quota_zero_as_unlimited');
$quota_result['title'] = $this->gettext($unlimited ? 'unlimited' : 'unknown');
$quota_result['percent'] = 0;
}
// cleanup
unset($quota_result['abort']);
if (empty($quota_result['table'])) {
unset($quota_result['all']);
}
return $quota_result;
}
/**
* Outputs error message according to server error/response codes
*
* @param string $fallback Fallback message label
* @param array $fallback_args Fallback message label arguments
* @param string $suffix Message label suffix
* @param array $params Additional parameters (type, prefix)
*/
public function display_server_error($fallback = null, $fallback_args = null, $suffix = '', $params = array())
{
$err_code = $this->storage->get_error_code();
$res_code = $this->storage->get_response_code();
$args = array();
if ($res_code == rcube_storage::NOPERM) {
$error = 'errornoperm';
}
else if ($res_code == rcube_storage::READONLY) {
$error = 'errorreadonly';
}
else if ($res_code == rcube_storage::OVERQUOTA) {
$error = 'erroroverquota';
}
else if ($err_code && ($err_str = $this->storage->get_error_str())) {
// try to detect access rights problem and display appropriate message
if (stripos($err_str, 'Permission denied') !== false) {
$error = 'errornoperm';
}
// try to detect full mailbox problem and display appropriate message
// there can be e.g. "Quota exceeded" / "quotum would exceed" / "Over quota"
else if (stripos($err_str, 'quot') !== false && preg_match('/exceed|over/i', $err_str)) {
$error = 'erroroverquota';
}
else {
$error = 'servererrormsg';
$args = array('msg' => rcube::Q($err_str));
}
}
else if ($err_code < 0) {
$error = 'storageerror';
}
else if ($fallback) {
$error = $fallback;
$args = $fallback_args;
$params['prefix'] = false;
}
if ($error) {
if ($suffix && $this->text_exists($error . $suffix)) {
$error .= $suffix;
}
$msg = $this->gettext(array('name' => $error, 'vars' => $args));
if ($params['prefix'] && $fallback) {
$msg = $this->gettext(array('name' => $fallback, 'vars' => $fallback_args)) . ' ' . $msg;
}
$this->output->show_message($msg, $params['type'] ?: 'error');
}
}
/**
* Displays an error message on storage fatal errors
*/
public function storage_fatal_error()
{
$err_code = $this->storage->get_error_code();
switch ($err_code) {
// Not all are really fatal, but these should catch
// connection/authentication errors the best we can
case rcube_imap_generic::ERROR_NO:
case rcube_imap_generic::ERROR_BAD:
case rcube_imap_generic::ERROR_BYE:
$this->display_server_error();
}
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*/
public function html_editor($mode = '')
{
$spellcheck = intval($this->config->get('enable_spellcheck'));
$spelldict = intval($this->config->get('spellcheck_dictionary'));
$disabled_plugins = array();
$disabled_buttons = array();
$extra_plugins = array();
$extra_buttons = array();
if (!$spellcheck) {
$disabled_plugins[] = 'spellchecker';
}
$hook = $this->plugins->exec_hook('html_editor', array(
'mode' => $mode,
'disabled_plugins' => $disabled_plugins,
'disabled_buttons' => $disabled_buttons,
'extra_plugins' => $extra_plugins,
'extra_buttons' => $extra_buttons,
));
if ($hook['abort']) {
return;
}
$lang_codes = array($_SESSION['language']);
$assets_dir = $this->config->get('assets_dir') ?: INSTALL_PATH;
$skin_path = $this->output->get_skin_path();
if ($pos = strpos($_SESSION['language'], '_')) {
$lang_codes[] = substr($_SESSION['language'], 0, $pos);
}
foreach ($lang_codes as $code) {
if (file_exists("$assets_dir/program/js/tinymce/langs/$code.js")) {
$lang = $code;
break;
}
}
if (empty($lang)) {
$lang = 'en';
}
$config = array(
'mode' => $mode,
'lang' => $lang,
'skin_path' => $skin_path,
'spellcheck' => $spellcheck, // deprecated
'spelldict' => $spelldict,
'content_css' => 'program/resources/tinymce/content.css',
'disabled_plugins' => $hook['disabled_plugins'],
'disabled_buttons' => $hook['disabled_buttons'],
'extra_plugins' => $hook['extra_plugins'],
'extra_buttons' => $hook['extra_buttons'],
);
if ($path = $this->config->get('editor_css_location')) {
if ($path = $this->find_asset($skin_path . $path)) {
$config['content_css'] = $path;
}
}
$this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia');
$this->output->set_env('editor_config', $config);
if ($path = $this->config->get('media_browser_css_location', 'program/resources/tinymce/browser.css')) {
if ($path != 'none' && ($path = $this->find_asset($path))) {
$this->output->include_css($path);
}
}
$this->output->include_script('tinymce/tinymce.min.js');
$this->output->include_script('editor.js');
}
/**
* File upload progress handler.
*
* @deprecated We're using HTML5 upload progress
*/
public function upload_progress()
{
// NOOP
$this->output->send();
}
/**
* Initializes file uploading interface.
*
* @param int $max_size Optional maximum file size in bytes
*
* @return string Human-readable file size limit
*/
public function upload_init($max_size = null)
{
// find max filesize value
$max_filesize = rcube_utils::max_upload_size();
if ($max_size && $max_size < $max_filesize) {
$max_filesize = $max_size;
}
$max_filesize_txt = $this->show_bytes($max_filesize);
$this->output->set_env('max_filesize', $max_filesize);
$this->output->set_env('filesizeerror', $this->gettext(array(
'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize_txt))));
if ($max_filecount = ini_get('max_file_uploads')) {
$this->output->set_env('max_filecount', $max_filecount);
$this->output->set_env('filecounterror', $this->gettext(array(
'name' => 'filecounterror', 'vars' => array('count' => $max_filecount))));
}
$this->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B');
return $max_filesize_txt;
}
/**
* Upload form object
*
* @param array $attrib Object attributes
* @param string $name Form object name
* @param string $action Form action name
* @param array $input_attr File input attributes
* @param int $max_size Maximum upload size
*
* @return string HTML output
*/
public function upload_form($attrib, $name, $action, $input_attr = array(), $max_size = null)
{
// Get filesize, enable upload progress bar
$max_filesize = $this->upload_init($max_size);
$hint = html::div('hint', $this->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))));
if ($attrib['mode'] == 'hint') {
return $hint;
}
// set defaults
$attrib += array('id' => 'rcmUploadbox', 'buttons' => 'yes');
$event = rcmail_output::JS_OBJECT_NAME . ".command('$action', this.form)";
$form_id = $attrib['id'] . 'Frm';
// Default attributes of file input and form
$input_attr += array(
'id' => $attrib['id'] . 'Input',
'type' => 'file',
'name' => '_attachments[]',
);
$form_attr = array(
'id' => $form_id,
'name' => $name,
'method' => 'post',
'enctype' => 'multipart/form-data'
);
if ($attrib['mode'] == 'smart') {
unset($attrib['buttons']);
$form_attr['class'] = 'smart-upload';
$input_attr = array_merge($input_attr, array(
// #5854: Chrome does not execute onchange when selecting the same file.
// To fix this we reset the input using null value.
'onchange' => "$event; this.value=null",
'class' => 'smart-upload',
'tabindex' => '-1',
));
}
$input = new html_inputfield($input_attr);
$content = $attrib['prefix'] . $input->show();
if ($attrib['mode'] != 'smart') {
$content = html::div(null, $content . $hint);
}
if (rcube_utils::get_boolean($attrib['buttons'])) {
$button = new html_inputfield(array('type' => 'button'));
$content .= html::div('buttons',
$button->show($this->gettext('close'), array('class' => 'button', 'onclick' => "$('#{$attrib['id']}').hide()")) . ' ' .
$button->show($this->gettext('upload'), array('class' => 'button mainaction', 'onclick' => $event))
);
}
$this->output->add_gui_object($name, $form_id);
return html::div($attrib, $this->output->form_tag($form_attr, $content));
}
/**
* Outputs uploaded file content (with image thumbnails support
*
* @param array $file Upload file data
*/
public function display_uploaded_file($file)
{
if (empty($file)) {
return;
}
$file = $this->plugins->exec_hook('attachment_display', $file);
if ($file['status']) {
if (empty($file['size'])) {
$file['size'] = $file['data'] ? strlen($file['data']) : @filesize($file['path']);
}
// generate image thumbnail for file browser in HTML editor
if (!empty($_GET['_thumbnail'])) {
$thumbnail_size = 80;
$mimetype = $file['mimetype'];
$file_ident = $file['id'] . ':' . $file['mimetype'] . ':' . $file['size'];
$thumb_name = 'thumb' . md5($file_ident . ':' . $this->user->ID . ':' . $thumbnail_size);
$cache_file = rcube_utils::temp_filename($thumb_name, false, false);
// render thumbnail image if not done yet
if (!is_file($cache_file)) {
if (!$file['path']) {
$orig_name = $filename = $cache_file . '.tmp';
file_put_contents($orig_name, $file['data']);
}
else {
$filename = $file['path'];
}
$image = new rcube_image($filename);
if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) {
$mimetype = 'image/' . $imgtype;
if ($orig_name) {
unlink($orig_name);
}
}
}
if (is_file($cache_file)) {
// cache for 1h
$this->output->future_expire_header(3600);
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($cache_file));
readfile($cache_file);
exit;
}
}
header('Content-Type: ' . $file['mimetype']);
header('Content-Length: ' . $file['size']);
if ($file['data']) {
echo $file['data'];
}
else if ($file['path']) {
readfile($file['path']);
}
}
}
/**
* Initializes client-side autocompletion.
*/
public function autocomplete_init()
{
static $init;
if ($init) {
return;
}
$init = 1;
if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
$book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
if (count($book_types) > 1) {
$this->output->set_env('autocomplete_threads', $threads);
$this->output->set_env('autocomplete_sources', $book_types);
}
}
$this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
$this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
$this->output->add_label('autocompletechars', 'autocompletemore');
}
/**
* Returns supported font-family specifications
*
* @param string $font Font name
*
* @param string|array Font-family specification array or string (if $font is used)
*/
public static function font_defs($font = null)
{
$fonts = array(
'Andale Mono' => '"Andale Mono",Times,monospace',
'Arial' => 'Arial,Helvetica,sans-serif',
'Arial Black' => '"Arial Black","Avant Garde",sans-serif',
'Book Antiqua' => '"Book Antiqua",Palatino,serif',
'Courier New' => '"Courier New",Courier,monospace',
'Georgia' => 'Georgia,Palatino,serif',
'Helvetica' => 'Helvetica,Arial,sans-serif',
'Impact' => 'Impact,Chicago,sans-serif',
'Tahoma' => 'Tahoma,Arial,Helvetica,sans-serif',
'Terminal' => 'Terminal,Monaco,monospace',
'Times New Roman' => '"Times New Roman",Times,serif',
'Trebuchet MS' => '"Trebuchet MS",Geneva,sans-serif',
'Verdana' => 'Verdana,Geneva,sans-serif',
);
if ($font) {
return $fonts[$font];
}
return $fonts;
}
/**
* Create a human readable string for a number of bytes
*
* @param int $bytes Number of bytes
* @param string &$unit Size unit
*
* @return string Byte string
*/
public function show_bytes($bytes, &$unit = null)
{
// Plugins may want to display different units
$plugin = $this->plugins->exec_hook('show_bytes', array('bytes' => $bytes));
$unit = $plugin['unit'];
if ($plugin['result'] !== null) {
return $plugin['result'];
}
if ($bytes >= 1073741824) {
$unit = 'GB';
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . $this->gettext($unit);
}
else if ($bytes >= 1048576) {
$unit = 'MB';
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . $this->gettext($unit);
}
else if ($bytes >= 1024) {
$unit = 'KB';
$str = sprintf("%d ", round($bytes/1024)) . $this->gettext($unit);
}
else {
$unit = 'B';
$str = sprintf('%d ', $bytes) . $this->gettext($unit);
}
return $str;
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_part $part Message part
*
* @return string Part size (and unit)
*/
public function message_part_size($part)
{
if (isset($part->d_parameters['size'])) {
$size = $this->show_bytes((int)$part->d_parameters['size']);
}
else {
$size = $part->size;
if ($size === 0) {
$part->exact_size = true;
}
if ($part->encoding == 'base64') {
$size = $size / 1.33;
}
$size = $this->show_bytes($size);
}
if (!$part->exact_size) {
$size = '~' . $size;
}
return $size;
}
/**
* Returns message UID(s) and IMAP folder(s) from GET/POST data
*
* @param string $uids UID value to decode
* @param string $mbox Default mailbox value (if not encoded in UIDs)
* @param bool $is_multifolder Will be set to True if multi-folder request
* @param int $mode Request mode. Default: rcube_utils::INPUT_GPC.
*
* @return array List of message UIDs per folder
*/
public static function get_uids($uids = null, $mbox = null, &$is_multifolder = false, $mode = 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', $mode ?: rcube_utils::INPUT_GPC);
$_mbox = $mbox ?: (string) rcube_utils::get_input_value('_mbox', $mode ?: rcube_utils::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) {
$is_multifolder = true;
// 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;
}
else {
$is_multifolder = true;
}
if ($uid == '*') {
$result[$mbox] = $uid;
}
else if (preg_match('/^[0-9:.]+$/', $uid)) {
$result[$mbox][] = $uid;
}
}
}
return $result;
}
/**
* Get resource file content (with assets_dir support)
*
* @param string $name File name
*
* @return string File content
*/
public function get_resource_content($name)
{
if (!strpos($name, '/')) {
$name = "program/resources/$name";
}
$assets_dir = $this->config->get('assets_dir');
if ($assets_dir) {
$path = slashify($assets_dir) . $name;
if (@file_exists($path)) {
$name = $path;
}
}
return file_get_contents($name, false);
}
/**
* Converts HTML content into plain text
*
* @param string $html HTML content
* @param array $options Conversion parameters (width, links, charset)
*
* @return string Plain text
*/
public function html2text($html, $options = array())
{
$default_options = array(
'links' => true,
'width' => 75,
'body' => $html,
'charset' => RCUBE_CHARSET,
);
$options = array_merge($default_options, (array) $options);
// Plugins may want to modify HTML in another/additional way
$options = $this->plugins->exec_hook('html2text', $options);
// Convert to text
if (!$options['abort']) {
$converter = new rcube_html2text($options['body'],
false, $options['links'], $options['width'], $options['charset']);
$options['body'] = rtrim($converter->get_text());
}
return $options['body'];
}
/**
* Connect to the mail storage server with stored session data
*
* @return bool True on success, False on error
*/
public function storage_connect()
{
$storage = $this->get_storage();
if ($_SESSION['storage_host'] && !$storage->is_connected()) {
$host = $_SESSION['storage_host'];
$user = $_SESSION['username'];
$port = $_SESSION['storage_port'];
$ssl = $_SESSION['storage_ssl'];
$pass = $this->decrypt($_SESSION['password']);
if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
if (is_object($this->output)) {
$this->output->show_message('storageerror', 'error');
}
}
else {
$this->set_storage_prop();
}
}
return $storage->is_connected();
}
}
diff --git a/tests/Browser/Addressbook/Addressbook.php b/tests/Browser/Addressbook/Addressbook.php
new file mode 100644
index 000000000..a73a542c0
--- /dev/null
+++ b/tests/Browser/Addressbook/Addressbook.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Tests\Browser\Addressbook;
+
+class Addressbook extends \Tests\Browser\DuskTestCase
+{
+ public function testAddressbook()
+ {
+ $this->browse(function ($browser) {
+ $this->go('addressbook');
+
+ // check task
+ $this->assertEnvEquals('task', 'addressbook');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('qsearchbox', $objects);
+ $this->assertContains('folderlist', $objects);
+ $this->assertContains('contactslist', $objects);
+ $this->assertContains('countdisplay', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/Addressbook/Import.php b/tests/Browser/Addressbook/Import.php
new file mode 100644
index 000000000..4b136ccbf
--- /dev/null
+++ b/tests/Browser/Addressbook/Import.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Tests\Browser\Addressbook;
+
+class Import extends \Tests\Browser\DuskTestCase
+{
+ public function testImport()
+ {
+ $this->browse(function ($browser) {
+ $this->go('addressbook', 'import');
+
+ // check task and action
+ $this->assertEnvEquals('task', 'addressbook');
+ $this->assertEnvEquals('action', 'import');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('importform', $objects);
+ });
+ }
+
+ public function testImport2()
+ {
+ $this->browse(function ($browser) {
+ $this->go('addressbook', 'import');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('importform', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/DuskTestCase.php b/tests/Browser/DuskTestCase.php
new file mode 100644
index 000000000..0381a0b9c
--- /dev/null
+++ b/tests/Browser/DuskTestCase.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Tests\Browser;
+
+use PHPUnit\Framework\TestCase;
+use Facebook\WebDriver\Chrome\ChromeOptions;
+use Facebook\WebDriver\Remote\RemoteWebDriver;
+use Facebook\WebDriver\Remote\DesiredCapabilities;
+use Laravel\Dusk\Browser;
+use Laravel\Dusk\Chrome\SupportsChrome;
+use Laravel\Dusk\Concerns\ProvidesBrowser;
+use Symfony\Component\Finder\Finder;
+use Symfony\Component\Process\Process;
+
+abstract class DuskTestCase extends TestCase
+{
+ use ProvidesBrowser,
+ SupportsChrome;
+
+ protected $app;
+ protected static $phpProcess;
+
+
+ /**
+ * Prepare for Dusk test execution.
+ *
+ * @beforeClass
+ * @return void
+ */
+ public static function prepare()
+ {
+ static::startWebServer();
+ static::startChromeDriver();
+ }
+
+ /**
+ * Create the RemoteWebDriver instance.
+ *
+ * @return \Facebook\WebDriver\Remote\RemoteWebDriver
+ */
+ protected function driver()
+ {
+ $options = (new ChromeOptions())->addArguments([
+ '--disable-gpu',
+ '--headless',
+ '--window-size=1280,720',
+ ]);
+
+ return RemoteWebDriver::create(
+ 'http://localhost:9515',
+ DesiredCapabilities::chrome()->setCapability(
+ ChromeOptions::CAPABILITY,
+ $options
+ )
+ );
+ }
+
+ /**
+ * Set up the test run
+ *
+ * @return void
+ */
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->app = \rcmail::get_instance();
+
+ Browser::$baseUrl = 'http://localhost:8000';
+ Browser::$storeScreenshotsAt = __DIR__ . '/screenshots';
+ Browser::$storeConsoleLogAt = __DIR__ . '/console';
+
+ // Purge screenshots from the last test run
+ $pattern = sprintf('failure-%s_%s-*',
+ str_replace("\\", '_', get_class($this)),
+ $this->getName(false)
+ );
+
+ try {
+ $files = Finder::create()->files()->in(__DIR__ . '/screenshots')->name($pattern);
+ foreach ($files as $file) {
+ @unlink($file->getRealPath());
+ }
+ }
+ catch (\Symfony\Component\Finder\Exception\DirectoryNotFoundException $e) {
+ // ignore missing screenshots directory
+ }
+ }
+
+ /**
+ * Assert specified rcmail.env value
+ */
+ protected function assertEnvEquals($key, $expected)
+ {
+ $this->assertEquals($expected, $this->getEnv($key));
+ }
+
+ /**
+ * Get content of rcmail.env entry
+ */
+ protected function getEnv($key)
+ {
+ $this->browse(function (Browser $browser) use ($key, &$result) {
+ $result = $browser->script("return rcmail.env['$key']");
+ $result = $result[0];
+ });
+
+ return $result;
+ }
+
+ /**
+ * Get HTML IDs of defined buttons for specified Roundcube action
+ */
+ protected function getButtons($action)
+ {
+ $this->browse(function (Browser $browser) use ($action, &$buttons) {
+ $buttons = $browser->script("return rcmail.buttons['$action']");
+ $buttons = $buttons[0];
+ });
+
+ if (is_array($buttons)) {
+ foreach ($buttons as $idx => $button) {
+ $buttons[$idx] = $button['id'];
+ }
+ }
+
+ return (array) $buttons;
+ }
+
+ /**
+ * Return names of defined gui_objects
+ */
+ protected function getObjects()
+ {
+ $this->browse(function (Browser $browser) use (&$objects) {
+ $objects = $browser->script("var i, r = []; for (i in rcmail.gui_objects) r.push(i); return r");
+ $objects = $objects[0];
+ });
+
+ return (array) $objects;
+ }
+
+ /**
+ * Log in the test user
+ */
+ protected function doLogin()
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->type('_user', TESTS_USER);
+ $browser->type('_pass', TESTS_PASS);
+ $browser->click('button[type="submit"]');
+
+ // wait after successful login
+ //$browser->waitForReload();
+ $browser->waitUntil('!rcmail.busy');
+ });
+ }
+
+ /**
+ * Visit specified task/action with logon if needed
+ */
+ protected function go($task = 'mail', $action = null, $login = true)
+ {
+ $this->browse(function (Browser $browser) use ($task, $action, $login) {
+ $browser->visit("/?_task=$task&_action=$action");
+
+ // check if we have a valid session
+ if ($login && $this->getEnv('task') == 'login') {
+ $this->doLogin();
+ }
+ });
+ }
+
+ /**
+ * Starts PHP server.
+ */
+ protected static function startWebServer()
+ {
+ $path = realpath(__DIR__ . '/../../public_html');
+ $cmd = ['php', '-S', 'localhost:8000'];
+
+ static::$phpProcess = new Process($cmd, null, []);
+ static::$phpProcess->setWorkingDirectory($path);
+ static::$phpProcess->start();
+
+ static::afterClass(function () {
+ static::$phpProcess->stop();
+ });
+ }
+}
diff --git a/tests/Browser/Login.php b/tests/Browser/Login.php
new file mode 100644
index 000000000..dcc20ad3c
--- /dev/null
+++ b/tests/Browser/Login.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Tests\Browser;
+
+class Login extends DuskTestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ \bootstrap::init_db();
+ \bootstrap::init_imap();
+ }
+
+ public function testLogin()
+ {
+ // first test, we're already on the login page
+ $this->browse(function ($browser) {
+ $browser->visit('/');
+
+ $browser->assertTitleContains($this->app->config->get('product_name'));
+
+ // task should be set to 'login'
+ $this->assertEnvEquals('task', 'login');
+
+ $browser->assertVisible('#rcmloginuser');
+ $browser->assertVisible('#rcmloginpwd');
+
+ // test valid login
+ $this->go('mail');
+
+ // task should be set to 'mail' now
+ $this->assertEnvEquals('task', 'mail');
+ });
+ }
+}
diff --git a/tests/Browser/Logout.php b/tests/Browser/Logout.php
new file mode 100644
index 000000000..4e563e264
--- /dev/null
+++ b/tests/Browser/Logout.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Tests\Browser;
+
+class Logout extends DuskTestCase
+{
+ public function testLogout()
+ {
+ $this->browse(function ($browser) {
+ $this->go('settings');
+
+ // wait for the menu and then click the Logout button
+ $browser->waitFor('#taskmenu');
+ $browser->click('#taskmenu a.logout');
+
+ // task should be set to 'login'
+ $this->assertEnvEquals('task', 'login');
+
+ // form should exist
+ $browser->assertVisible('input[name="_user"]');
+ $browser->assertVisible('input[name="_pass"]');
+ $browser->assertMissing('#taskmenu');
+ });
+ }
+}
diff --git a/tests/Browser/Mail/CheckRecent.php b/tests/Browser/Mail/CheckRecent.php
new file mode 100644
index 000000000..4af02034e
--- /dev/null
+++ b/tests/Browser/Mail/CheckRecent.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Tests\Browser\Mail;
+
+class CheckRecent extends \Tests\Browser\DuskTestCase
+{
+ public function testCheckRecent()
+ {
+ $this->browse(function ($browser) {
+ $this->go('mail');
+
+ // TODO
+ });
+ }
+}
diff --git a/tests/Browser/Mail/Compose.php b/tests/Browser/Mail/Compose.php
new file mode 100644
index 000000000..5d9d988e7
--- /dev/null
+++ b/tests/Browser/Mail/Compose.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Tests\Browser\Mail;
+
+class Compose extends \Tests\Browser\DuskTestCase
+{
+ public function testCompose()
+ {
+ $this->browse(function ($browser) {
+ $this->go('mail', 'compose');
+
+ // check task and action
+ $this->assertEnvEquals('task', 'mail');
+ $this->assertEnvEquals('action', 'compose');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('qsearchbox', $objects);
+ $this->assertContains('addressbookslist', $objects);
+ $this->assertContains('contactslist', $objects);
+ $this->assertContains('messageform', $objects);
+ $this->assertContains('attachmentlist', $objects);
+ $this->assertContains('filedrop', $objects);
+ $this->assertContains('uploadform', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/Mail/Getunread.php b/tests/Browser/Mail/Getunread.php
new file mode 100644
index 000000000..e6300680c
--- /dev/null
+++ b/tests/Browser/Mail/Getunread.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Tests\Browser\Mail;
+
+class Getunread extends \Tests\Browser\DuskTestCase
+{
+ protected $msgcount = 0;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ \bootstrap::init_imap();
+ \bootstrap::purge_mailbox('INBOX');
+
+ // import email messages
+ foreach (glob(TESTS_DIR . 'data/mail/list_*.eml') as $f) {
+ \bootstrap::import_message($f, 'INBOX');
+ $this->msgcount++;
+ }
+ }
+
+ public function testGetunread()
+ {
+ $this->browse(function ($browser) {
+ $this->go('mail');
+
+ // Folders list state
+ $browser->waitFor('.folderlist li.inbox.unread');
+
+ $this->assertEquals(strval($this->msgcount), $browser->text('.folderlist li.inbox span.unreadcount'));
+
+ // Messages list state
+ $this->assertCount($this->msgcount, $browser->elements('#messagelist tr.unread'));
+ });
+ }
+}
diff --git a/tests/Browser/Mail/List.php b/tests/Browser/Mail/List.php
new file mode 100644
index 000000000..134f0bc3f
--- /dev/null
+++ b/tests/Browser/Mail/List.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Tests\Browser\Mail;
+
+class MailList extends \Tests\Browser\DuskTestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ \bootstrap::init_imap();
+ \bootstrap::purge_mailbox('INBOX');
+
+ // import email messages
+ foreach (glob(TESTS_DIR . 'data/mail/list_00.eml') as $f) {
+ \bootstrap::import_message($f, 'INBOX');
+ }
+ }
+
+ public function testList()
+ {
+ $this->browse(function ($browser) {
+ $this->go('mail');
+
+ $this->assertCount(1, $browser->elements('#messagelist tbody tr'));
+
+ // check message list
+ $browser->assertVisible('#messagelist tbody tr:first-child.unread');
+
+ $this->assertEquals('Lines', $browser->text('#messagelist tbody tr:first-child span.subject'));
+
+ //$browser->assertVisible('#messagelist tbody tr:first-child span.msgicon.unread');
+ });
+ }
+}
diff --git a/tests/Browser/Mail/Mail.php b/tests/Browser/Mail/Mail.php
new file mode 100644
index 000000000..1e767c2b8
--- /dev/null
+++ b/tests/Browser/Mail/Mail.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Tests\Browser\Mail;
+
+class Mail extends \Tests\Browser\DuskTestCase
+{
+ public function testMail()
+ {
+ $this->browse(function ($browser) {
+ $this->go('mail');
+
+ // check task
+ $this->assertEnvEquals('task', 'mail');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('qsearchbox', $objects);
+ $this->assertContains('mailboxlist', $objects);
+ $this->assertContains('messagelist', $objects);
+ $this->assertContains('quotadisplay', $objects);
+ $this->assertContains('search_filter', $objects);
+ $this->assertContains('countdisplay', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/README.md b/tests/Browser/README.md
new file mode 100644
index 000000000..56f5d10ca
--- /dev/null
+++ b/tests/Browser/README.md
@@ -0,0 +1,60 @@
+In-Browser Tests
+================
+
+The idea of these testing suite is to make it as simple as possible to execute
+the tests. So, you don't have to run any additional services, nor download
+and install anything manually.
+
+The tests are using [Laravel Dusk][laravel-dusk] and Chrome WebDriver.
+PHP server is used to serve Roundcube instance on tests run.
+
+
+INSTALLATION
+------------
+
+Installation:
+
+1. Add `"laravel/dusk": "~5.7.0"` to your composer.json file and run `composer update`.
+2. Install Chrome WebDriver for the version of Chrome/Chromium in your system. Yes,
+ you have to have Chrome/Chromium installed.
+```
+php tests/Browser/install.php [version]`
+```
+3. Configure the test account and Roundcube instance.
+
+Create a config file named `config-test.inc.php` in the Roundcube config dir.
+That file should provide specific `db_dsnw` and
+`default_host` values for testing purposes as well as the credentials of a
+valid IMAP user account used for running the tests with.
+
+Add these config options used by the Browser tests:
+
+```php
+ // Unit tests settings
+ $config['tests_username'] = 'roundcube.test@example.org';
+ $config['tests_password'] = '<test-account-password>';
+```
+
+WARNING
+-------
+Please note that the configured IMAP account as well as the Roundcube database
+configred in `db_dsnw` will be wiped and filled with test data in every test
+run. Under no circumstances you should use credentials of a production database
+or email account!
+
+Please, keep the file as simple as possible, i.e. containing only database
+and imap/smtp settings needed for the test user authentication. We would
+want to test default configuration. Especially only Elastic skin is supported.
+
+
+EXECUTING THE TESTS
+-------------------
+
+To run the test suite call `phpunit` from the tests/Browser directory:
+
+```
+ cd <roundcube-dir>/tests/Browser
+ phpunit # or ../../vendor/bin/phpunit
+```
+
+[laravel-dusk]: https://github.com/laravel/dusk
diff --git a/tests/Browser/Settings/About.php b/tests/Browser/Settings/About.php
new file mode 100644
index 000000000..f687a9d1e
--- /dev/null
+++ b/tests/Browser/Settings/About.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Tests\Browser\Settings;
+
+class About extends \Tests\Browser\DuskTestCase
+{
+ public function testAbout()
+ {
+ $this->browse(function ($browser) {
+ $this->go('settings', 'about');
+
+ // check task and action
+ $this->assertEnvEquals('task', 'settings');
+ $this->assertEnvEquals('action', 'about');
+
+ $browser->assertVisible('#pluginlist');
+ });
+ }
+}
diff --git a/tests/Browser/Settings/Folders.php b/tests/Browser/Settings/Folders.php
new file mode 100644
index 000000000..82c0b7d2e
--- /dev/null
+++ b/tests/Browser/Settings/Folders.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Tests\Browser\Settings;
+
+class Folders extends \Tests\Browser\DuskTestCase
+{
+ public function testFolders()
+ {
+ $this->browse(function ($browser) {
+ $this->go('settings', 'folders');
+
+ // task should be set to 'settings' and action to 'folders'
+ $this->assertEnvEquals('task', 'settings');
+ $this->assertEnvEquals('action', 'folders');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('quotadisplay', $objects);
+ $this->assertContains('subscriptionlist', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/Settings/Identities.php b/tests/Browser/Settings/Identities.php
new file mode 100644
index 000000000..d885f9edb
--- /dev/null
+++ b/tests/Browser/Settings/Identities.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Tests\Browser\Settings;
+
+class Identities extends \Tests\Browser\DuskTestCase
+{
+ public function testIdentities()
+ {
+ $this->browse(function ($browser) {
+ $this->go('settings', 'identities');
+
+ // check task and action
+ $this->assertEnvEquals('task', 'settings');
+ $this->assertEnvEquals('action', 'identities');
+
+ $objects = $this->getObjects();
+
+ // these objects should be there always
+ $this->assertContains('identitieslist', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/Settings/Settings.php b/tests/Browser/Settings/Settings.php
new file mode 100644
index 000000000..cfef01ceb
--- /dev/null
+++ b/tests/Browser/Settings/Settings.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Tests\Browser\Settings;
+
+class Settings extends \Tests\Browser\DuskTestCase
+{
+ public function testSettings()
+ {
+ $this->browse(function ($browser) {
+ $this->go('settings');
+
+ // task should be set to 'settings'
+ $this->assertEnvEquals('task', 'settings');
+
+ $objects = $this->getObjects();
+
+ $this->assertContains('sectionslist', $objects);
+ });
+ }
+}
diff --git a/tests/Browser/bootstrap.php b/tests/Browser/bootstrap.php
new file mode 100644
index 000000000..f8803c420
--- /dev/null
+++ b/tests/Browser/bootstrap.php
@@ -0,0 +1,174 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Environment initialization script for functional tests |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+if (php_sapi_name() != 'cli')
+ die("Not in shell mode (php-cli)");
+
+if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
+
+require_once(INSTALL_PATH . 'program/include/iniset.php');
+
+$rcmail = rcmail::get_instance(0, 'test');
+
+define('TESTS_DIR', realpath(__DIR__) . '/');
+define('TESTS_USER', $rcmail->config->get('tests_username'));
+define('TESTS_PASS', $rcmail->config->get('tests_password'));
+
+require_once(__DIR__ . '/DuskTestCase.php');
+
+
+/**
+ * Utilities for test environment setup
+ */
+class bootstrap
+{
+ static $imap_ready = null;
+
+ /**
+ * Wipe and re-initialize database
+ */
+ public static function init_db()
+ {
+ $rcmail = rcmail::get_instance();
+ $dsn = rcube_db::parse_dsn($rcmail->config->get('db_dsnw'));
+
+ if ($dsn['phptype'] == 'mysql' || $dsn['phptype'] == 'mysqli') {
+ // drop all existing tables first
+ $db = $rcmail->get_dbh();
+ $db->query("SET FOREIGN_KEY_CHECKS=0");
+ $sql_res = $db->query("SHOW TABLES");
+ while ($sql_arr = $db->fetch_array($sql_res)) {
+ $table = reset($sql_arr);
+ $db->query("DROP TABLE $table");
+ }
+
+ // init database with schema
+ system(sprintf('cat %s %s | mysql -h %s -u %s --password=%s %s',
+ realpath(INSTALL_PATH . '/SQL/mysql.initial.sql'),
+ realpath(TESTS_DIR . 'data/mysql.sql'),
+ escapeshellarg($dsn['hostspec']),
+ escapeshellarg($dsn['username']),
+ escapeshellarg($dsn['password']),
+ escapeshellarg($dsn['database'])
+ ));
+ }
+ else if ($dsn['phptype'] == 'sqlite') {
+ // delete database file -- will be re-initialized on first access
+ system(sprintf('rm -f %s', escapeshellarg($dsn['database'])));
+ }
+ }
+
+ /**
+ * Wipe the configured IMAP account and fill with test data
+ */
+ public static function init_imap()
+ {
+ if (!TESTS_USER) {
+ return false;
+ }
+ else if (self::$imap_ready !== null) {
+ return self::$imap_ready;
+ }
+
+ self::connect_imap(TESTS_USER, TESTS_PASS);
+ self::purge_mailbox('INBOX');
+ self::ensure_mailbox('Archive', true);
+
+ return self::$imap_ready;
+ }
+
+ /**
+ * Authenticate to IMAP with the given credentials
+ */
+ public static function connect_imap($username, $password, $host = null)
+ {
+ $rcmail = rcmail::get_instance();
+ $imap = $rcmail->get_storage();
+
+ if ($imap->is_connected()) {
+ $imap->close();
+ self::$imap_ready = false;
+ }
+
+ $imap_host = $host ?: $rcmail->config->get('default_host');
+ $a_host = parse_url($imap_host);
+ if ($a_host['host']) {
+ $imap_host = $a_host['host'];
+ $imap_ssl = isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'));
+ $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+ }
+ else {
+ $imap_port = 143;
+ $imap_ssl = false;
+ }
+
+ if (!$imap->connect($imap_host, $username, $password, $imap_port, $imap_ssl)) {
+ die("IMAP error: unable to authenticate with user " . TESTS_USER);
+ }
+
+ self::$imap_ready = true;
+ }
+
+ /**
+ * Import the given file into IMAP
+ */
+ public static function import_message($filename, $mailbox = 'INBOX')
+ {
+ if (!self::init_imap()) {
+ die(__METHOD__ . ': IMAP connection unavailable');
+ }
+
+ $imap = rcmail::get_instance()->get_storage();
+ $imap->save_message($mailbox, file_get_contents($filename));
+ }
+
+ /**
+ * Delete all messages from the given mailbox
+ */
+ public static function purge_mailbox($mailbox)
+ {
+ if (!self::init_imap()) {
+ die(__METHOD__ . ': IMAP connection unavailable');
+ }
+
+ $imap = rcmail::get_instance()->get_storage();
+ $imap->delete_message('*', $mailbox);
+ }
+
+ /**
+ * Make sure the given mailbox exists in IMAP
+ */
+ public static function ensure_mailbox($mailbox, $empty = false)
+ {
+ if (!self::init_imap()) {
+ die(__METHOD__ . ': IMAP connection unavailable');
+ }
+
+ $imap = rcmail::get_instance()->get_storage();
+
+ $folders = $imap->list_folders();
+ if (!in_array($mailbox, $folders)) {
+ $imap->create_folder($mailbox, true);
+ }
+ else if ($empty) {
+ $imap->delete_message('*', $mailbox);
+ }
+ }
+}
diff --git a/tests/Selenium/data/mail/list_00.eml b/tests/Browser/data/mail/list_00.eml
similarity index 100%
rename from tests/Selenium/data/mail/list_00.eml
rename to tests/Browser/data/mail/list_00.eml
diff --git a/tests/Selenium/data/mysql.sql b/tests/Browser/data/mysql.sql
similarity index 100%
rename from tests/Selenium/data/mysql.sql
rename to tests/Browser/data/mysql.sql
diff --git a/tests/Browser/install.php b/tests/Browser/install.php
new file mode 100644
index 000000000..4fba9022d
--- /dev/null
+++ b/tests/Browser/install.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Chrome WebDriver download tool |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+if (php_sapi_name() != 'cli')
+ die("Not in shell mode (php-cli)");
+
+if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
+
+require_once(INSTALL_PATH . 'program/include/iniset.php');
+
+class Installer extends Laravel\Dusk\Console\ChromeDriverCommand
+{
+ /**
+ * Execute the console command.
+ *
+ * @param string $version
+ *
+ * @return void
+ */
+ public function install($version = null)
+ {
+ $version = trim($this->getUrl(sprintf($this->versionUrl, $version ?: $this->latestVersion())));
+ $currentOS = Laravel\Dusk\OperatingSystem::id();
+
+ foreach ($this->slugs as $os => $slug) {
+ if ($os === $currentOS) {
+ $archive = $this->download($version, $slug);
+ $binary = $this->extract($archive);
+
+ $this->rename($binary, $os);
+ }
+ }
+
+ echo "ChromeDriver binary successfully installed for version $version.\n";
+ }
+
+ /**
+ * Get the contents of a URL
+ *
+ * @param string $url URL
+ *
+ * @return string|bool
+ */
+ protected function getUrl(string $url)
+ {
+ return file_get_contents($url);
+ }
+}
+
+$installer = new Installer;
+$installer->install($argv[1]);
diff --git a/tests/Selenium/phpunit.xml b/tests/Browser/phpunit.xml
similarity index 80%
rename from tests/Selenium/phpunit.xml
rename to tests/Browser/phpunit.xml
index fe0c7016c..eb6ea316c 100644
--- a/tests/Selenium/phpunit.xml
+++ b/tests/Browser/phpunit.xml
@@ -1,29 +1,27 @@
<phpunit backupGlobals="false"
bootstrap="bootstrap.php"
colors="true">
<testsuites>
- <testsuite name="Mail">
- <file>Login.php</file><!-- Login.php test must be first -->
- <file>Mail/Mail.php</file>
- <file>Mail/CheckRecent.php</file>
- <file>Mail/Compose.php</file>
- <file>Mail/Getunread.php</file>
- <file>Mail/List.php</file>
- <file>Logout.php</file><!-- Logout.php test must be last -->
+ <testsuite name="Logon">
+ <file>Login.php</file>
+ <file>Logout.php</file>
</testsuite>
<testsuite name="Addressbook">
- <file>Login.php</file>
<file>Addressbook/Addressbook.php</file>
<file>Addressbook/Import.php</file>
- <file>Logout.php</file>
</testsuite>
<testsuite name="Settings">
- <file>Login.php</file>
<file>Settings/About.php</file>
<file>Settings/Folders.php</file>
<file>Settings/Identities.php</file>
<file>Settings/Settings.php</file>
- <file>Logout.php</file>
+ </testsuite>
+ <testsuite name="Mail">
+ <file>Mail/Mail.php</file>
+ <file>Mail/CheckRecent.php</file>
+ <file>Mail/Compose.php</file>
+ <file>Mail/Getunread.php</file>
+ <file>Mail/List.php</file>
</testsuite>
</testsuites>
</phpunit>
diff --git a/tests/Selenium/Addressbook/Addressbook.php b/tests/Selenium/Addressbook/Addressbook.php
deleted file mode 100644
index 9a22b6e13..000000000
--- a/tests/Selenium/Addressbook/Addressbook.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-class Selenium_Addressbook_Addressbook extends Selenium_Test
-{
- public function testAddressbook()
- {
- $this->go('addressbook');
-
- // check task
- $env = $this->get_env();
- $this->assertEquals('addressbook', $env['task']);
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('qsearchbox', $objects);
- $this->assertContains('folderlist', $objects);
- $this->assertContains('contactslist', $objects);
- $this->assertContains('countdisplay', $objects);
- }
-}
diff --git a/tests/Selenium/Addressbook/Import.php b/tests/Selenium/Addressbook/Import.php
deleted file mode 100644
index 13d81740f..000000000
--- a/tests/Selenium/Addressbook/Import.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-class Selenium_Addressbook_Import extends Selenium_Test
-{
- public function testImport()
- {
- $this->go('addressbook', 'import');
-
- // check task and action
- $env = $this->get_env();
- $this->assertEquals('addressbook', $env['task']);
- $this->assertEquals('import', $env['action']);
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('importform', $objects);
- }
-
- public function testImport2()
- {
- $this->go('addressbook', 'import');
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('importform', $objects);
- }
-}
diff --git a/tests/Selenium/Login.php b/tests/Selenium/Login.php
deleted file mode 100644
index 6910b43e6..000000000
--- a/tests/Selenium/Login.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-class Selenium_Login extends Selenium_Test
-{
- protected function setUp()
- {
- bootstrap::init_db();
- bootstrap::init_imap();
- parent::setUp();
- }
-
- public function testLogin()
- {
- // first test, we're already on the login page
- $this->url(TESTS_URL);
-
- // task should be set to 'login'
- $env = $this->get_env();
- $this->assertEquals('login', $env['task']);
-
- // test valid login
- $this->login();
-
- // task should be set to 'mail' now
- $env = $this->get_env();
- $this->assertEquals('mail', $env['task']);
- }
-}
diff --git a/tests/Selenium/Logout.php b/tests/Selenium/Logout.php
deleted file mode 100644
index 95eeda57c..000000000
--- a/tests/Selenium/Logout.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-class Selenium_Logout extends Selenium_Test
-{
- public function testLogout()
- {
- $this->go('mail');
-
- $this->click_button('logout');
-
- sleep(TESTS_SLEEP);
-
- // task should be set to 'login'
- $env = $this->get_env();
- $this->assertEquals('login', $env['task']);
-
- // form should exist
- $user_input = $this->byCssSelector('form input[name="_user"]');
- }
-}
diff --git a/tests/Selenium/Mail/CheckRecent.php b/tests/Selenium/Mail/CheckRecent.php
deleted file mode 100644
index 865421c2d..000000000
--- a/tests/Selenium/Mail/CheckRecent.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-class Selenium_Mail_CheckRecent extends Selenium_Test
-{
- public function testCheckRecent()
- {
- $this->go('mail');
-
- $res = $this->ajaxResponse('check-recent', "rcmail.command('checkmail')");
-
- $this->assertEquals('check-recent', $res['action']);
- $this->assertRegExp('/this\.set_unread_count/', $res['exec']);
- }
-}
diff --git a/tests/Selenium/Mail/Compose.php b/tests/Selenium/Mail/Compose.php
deleted file mode 100644
index e707ef17d..000000000
--- a/tests/Selenium/Mail/Compose.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-class Selenium_Mail_Compose extends Selenium_Test
-{
- public function testCompose()
- {
- $this->go('mail', 'compose');
-
- // check task and action
- $env = $this->get_env();
- $this->assertEquals('mail', $env['task']);
- $this->assertEquals('compose', $env['action']);
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('qsearchbox', $objects);
- $this->assertContains('addressbookslist', $objects);
- $this->assertContains('contactslist', $objects);
- $this->assertContains('messageform', $objects);
- $this->assertContains('attachmentlist', $objects);
- $this->assertContains('filedrop', $objects);
- $this->assertContains('uploadform', $objects);
- }
-}
diff --git a/tests/Selenium/Mail/Getunread.php b/tests/Selenium/Mail/Getunread.php
deleted file mode 100644
index c18ddc0dd..000000000
--- a/tests/Selenium/Mail/Getunread.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-class Selenium_Mail_Getunread extends Selenium_Test
-{
- protected $msgcount = 0;
-
- protected function setUp()
- {
- parent::setUp();
-
- bootstrap::init_imap();
- bootstrap::purge_mailbox('INBOX');
-
- // import email messages
- foreach (glob(TESTS_DIR . 'Selenium/data/mail/list_*.eml') as $f) {
- bootstrap::import_message($f, 'INBOX');
- $this->msgcount++;
- }
- }
-
- public function testGetunread()
- {
- $this->go('mail');
-
- $res = $this->ajaxResponse('getunread', "rcmail.http_request('getunread')");
- $this->assertEquals('getunread', $res['action']);
-
- $env = $this->get_env();
- $this->assertEquals($env['unread_counts']['INBOX'], $this->msgcount);
-
- $li = $this->byCssSelector('.folderlist li.inbox');
- $this->assertHasClass('unread', $li);
-
- $badge = $this->byCssSelector('.folderlist li.inbox span.unreadcount');
- $this->assertEquals(strval($this->msgcount), $this->getText($badge));
- }
-}
diff --git a/tests/Selenium/Mail/List.php b/tests/Selenium/Mail/List.php
deleted file mode 100644
index dc2857777..000000000
--- a/tests/Selenium/Mail/List.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-class Selenium_Mail_List extends Selenium_Test
-{
- protected function setUp()
- {
- parent::setUp();
-
- bootstrap::init_imap();
- bootstrap::purge_mailbox('INBOX');
-
- // import email messages
- foreach (glob(TESTS_DIR . 'Selenium/data/mail/list_00.eml') as $f) {
- bootstrap::import_message($f, 'INBOX');
- }
- }
-
- public function testList()
- {
- $this->go('mail');
-
- $res = $this->ajaxResponse('list', "rcmail.command('list')");
-
- $this->assertEquals('list', $res['action']);
- $this->assertRegExp('/this\.set_pagetitle/', $res['exec']);
- $this->assertRegExp('/this\.set_unread_count/', $res['exec']);
- $this->assertRegExp('/this\.set_rowcount/', $res['exec']);
- $this->assertRegExp('/this\.set_message_coltypes/', $res['exec']);
-
- $this->assertContains('current_page', $res['env']);
- $this->assertContains('exists', $res['env']);
- $this->assertContains('pagecount', $res['env']);
- $this->assertContains('pagesize', $res['env']);
- $this->assertContains('messagecount', $res['env']);
- $this->assertContains('mailbox', $res['env']);
-
- $this->assertEquals($res['env']['mailbox'], 'INBOX');
- $this->assertEquals($res['env']['messagecount'], 1);
-
- // check message list
- $row = $this->byCssSelector('.messagelist tbody tr:first-child');
- $this->assertHasClass('unread', $row);
-
- $subject = $this->byCssSelector('.messagelist tbody tr:first-child td.subject');
- $this->assertEquals('Lines', $this->getText($subject));
-
- $icon = $this->byCssSelector('.messagelist tbody tr:first-child td.status span');
- $this->assertHasClass('unread', $icon);
- }
-}
diff --git a/tests/Selenium/Mail/Mail.php b/tests/Selenium/Mail/Mail.php
deleted file mode 100644
index 98413787b..000000000
--- a/tests/Selenium/Mail/Mail.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-class Selenium_Mail_Mail extends Selenium_Test
-{
- public function testMail()
- {
- $this->go('mail');
-
- // check task
- $env = $this->get_env();
- $this->assertEquals('mail', $env['task']);
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('qsearchbox', $objects);
- $this->assertContains('mailboxlist', $objects);
- $this->assertContains('messagelist', $objects);
- $this->assertContains('quotadisplay', $objects);
- $this->assertContains('search_filter', $objects);
- $this->assertContains('countdisplay', $objects);
- }
-}
diff --git a/tests/Selenium/README.md b/tests/Selenium/README.md
deleted file mode 100644
index 5610fae71..000000000
--- a/tests/Selenium/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-Running Selenium Tests
-======================
-
-In order to run the Selenium-based web tests, some configuration for the
-Roundcube test instance need to be created. Along with the default config for a
-given Roundcube instance, you should provide a config specifically for running
-tests. To do so, create a config file named `config-test.inc.php` in the
-regular Roundcube config dir. That should provide specific `db_dsnw` and
-`default_host` values for testing purposes as well as the credentials of a
-valid IMAP user account used for running the tests with.
-
-Add these config options used by the Selenium tests:
-
-```php
- // Unit tests settings
- $config['tests_username'] = 'roundcube.test@example.org';
- $config['tests_password'] = '<test-account-password>';
- $config['tests_url'] = 'http://localhost/roundcube/index-test.php';
-```
-
-The `tests_url` should point to Roundcube's index-test.php file accessible by
-the Selenium web browser.
-
-WARNING
--------
-Please note that the configured IMAP account as well as the Roundcube database
-configred in `db_dsnw` will be wiped and filled with test data in every test
-run. Under no circumstances you should use credentials of a production database
-or email account!
-
-
-Run the tests
--------------
-
-First you need to start a Selenium server. We recommend to use the
-[Selenium Standalone Server][selenium-server] but the tests will also run on a
-Selenium Grid. The tests are based in [PHPUnit_Selenium][phpunit] which can be
-installed through [PEAR][pear-phpunit].
-
-To start the test suite call `phpunit` from the Selenium directory:
-
-```
- cd <roundcube-dir>/tests/Selenium
- phpunit
-```
-
-[phpunit]: http://phpunit.de/manual/4.0/en/selenium.html
-[pear-phpunit]: http://pear.phpunit.de/
-[selenium-server]: http://docs.seleniumhq.org/download/
diff --git a/tests/Selenium/Settings/About.php b/tests/Selenium/Settings/About.php
deleted file mode 100644
index 4cd49431a..000000000
--- a/tests/Selenium/Settings/About.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-class Selenium_Settings_About extends Selenium_Test
-{
- public function testAbout()
- {
- $this->url(TESTS_URL . '?_task=settings&_action=about');
- sleep(TESTS_SLEEP);
-
- // check task and action
- $env = $this->get_env();
- $this->assertEquals('settings', $env['task']);
- $this->assertEquals('about', $env['action']);
- }
-}
diff --git a/tests/Selenium/Settings/Folders.php b/tests/Selenium/Settings/Folders.php
deleted file mode 100644
index fa64e45d6..000000000
--- a/tests/Selenium/Settings/Folders.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-class Selenium_Settings_Folders extends Selenium_Test
-{
- public function testFolders()
- {
- $this->go('settings', 'folders');
-
- // task should be set to 'settings' and action to 'folders'
- $env = $this->get_env();
- $this->assertEquals('settings', $env['task']);
- $this->assertEquals('folders', $env['action']);
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('quotadisplay', $objects);
- $this->assertContains('subscriptionlist', $objects);
- }
-}
diff --git a/tests/Selenium/Settings/Identities.php b/tests/Selenium/Settings/Identities.php
deleted file mode 100644
index 869018b09..000000000
--- a/tests/Selenium/Settings/Identities.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-class Selenium_Settings_Identities extends Selenium_Test
-{
- public function testIdentities()
- {
- $this->go('settings', 'identities');
-
- // check task and action
- $env = $this->get_env();
- $this->assertEquals('settings', $env['task']);
- $this->assertEquals('identities', $env['action']);
-
- $objects = $this->get_objects();
-
- // these objects should be there always
- $this->assertContains('identitieslist', $objects);
- }
-}
diff --git a/tests/Selenium/Settings/Settings.php b/tests/Selenium/Settings/Settings.php
deleted file mode 100644
index 08d8339f1..000000000
--- a/tests/Selenium/Settings/Settings.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-class Selenium_Settings_Settings extends Selenium_Test
-{
- public function testSettings()
- {
- $this->go('settings');
-
- // task should be set to 'settings'
- $env = $this->get_env();
- $this->assertEquals('settings', $env['task']);
-
- $objects = $this->get_objects();
-
- $this->assertContains('sectionslist', $objects);
- }
-}
diff --git a/tests/Selenium/bootstrap.php b/tests/Selenium/bootstrap.php
deleted file mode 100644
index 47e53757f..000000000
--- a/tests/Selenium/bootstrap.php
+++ /dev/null
@@ -1,348 +0,0 @@
-<?php
-
-/*
- +-----------------------------------------------------------------------+
- | This file is part of the Roundcube Webmail client |
- | |
- | Copyright (C) The Roundcube Dev Team |
- | |
- | Licensed under the GNU General Public License version 3 or |
- | any later version with exceptions for skins & plugins. |
- | See the README file for a full license statement. |
- | |
- | PURPOSE: |
- | Environment initialization script for unit tests |
- +-----------------------------------------------------------------------+
- | Author: Thomas Bruederli <roundcube@gmail.com> |
- | Author: Aleksander Machniak <alec@alec.pl> |
- +-----------------------------------------------------------------------+
-*/
-
-if (php_sapi_name() != 'cli')
- die("Not in shell mode (php-cli)");
-
-if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
-
-define('TESTS_DIR', realpath(__DIR__ . '/../') . '/');
-
-if (@is_dir(TESTS_DIR . 'config')) {
- define('RCUBE_CONFIG_DIR', TESTS_DIR . 'config');
-}
-
-require_once(INSTALL_PATH . 'program/include/iniset.php');
-
-// Extend include path so some plugin test won't fail
-$include_path = ini_get('include_path') . PATH_SEPARATOR . TESTS_DIR . '..';
-if (set_include_path($include_path) === false) {
- die("Fatal error: ini_set/set_include_path does not work.");
-}
-
-$rcmail = rcmail::get_instance(0, 'test');
-
-define('TESTS_URL', $rcmail->config->get('tests_url'));
-define('TESTS_BROWSER', $rcmail->config->get('tests_browser', 'firefox'));
-define('TESTS_USER', $rcmail->config->get('tests_username'));
-define('TESTS_PASS', $rcmail->config->get('tests_password'));
-define('TESTS_SLEEP', $rcmail->config->get('tests_sleep', 5));
-
-PHPUnit_Extensions_Selenium2TestCase::shareSession(true);
-
-
-/**
- * satisfy PHPUnit
- */
-class bootstrap
-{
- static $imap_ready = null;
-
- /**
- * Wipe and re-initialize (mysql) database
- */
- public static function init_db()
- {
- $rcmail = rcmail::get_instance();
- $dsn = rcube_db::parse_dsn($rcmail->config->get('db_dsnw'));
-
- if ($dsn['phptype'] == 'mysql' || $dsn['phptype'] == 'mysqli') {
- // drop all existing tables first
- $db = $rcmail->get_dbh();
- $db->query("SET FOREIGN_KEY_CHECKS=0");
- $sql_res = $db->query("SHOW TABLES");
- while ($sql_arr = $db->fetch_array($sql_res)) {
- $table = reset($sql_arr);
- $db->query("DROP TABLE $table");
- }
-
- // init database with schema
- system(sprintf('cat %s %s | mysql -h %s -u %s --password=%s %s',
- realpath(INSTALL_PATH . '/SQL/mysql.initial.sql'),
- realpath(TESTS_DIR . 'Selenium/data/mysql.sql'),
- escapeshellarg($dsn['hostspec']),
- escapeshellarg($dsn['username']),
- escapeshellarg($dsn['password']),
- escapeshellarg($dsn['database'])
- ));
- }
- else if ($dsn['phptype'] == 'sqlite') {
- // delete database file -- will be re-initialized on first access
- system(sprintf('rm -f %s', escapeshellarg($dsn['database'])));
- }
- }
-
- /**
- * Wipe the configured IMAP account and fill with test data
- */
- public static function init_imap()
- {
- if (!TESTS_USER) {
- return false;
- }
- else if (self::$imap_ready !== null) {
- return self::$imap_ready;
- }
-
- self::connect_imap(TESTS_USER, TESTS_PASS);
- self::purge_mailbox('INBOX');
- self::ensure_mailbox('Archive', true);
-
- return self::$imap_ready;
- }
-
- /**
- * Authenticate to IMAP with the given credentials
- */
- public static function connect_imap($username, $password, $host = null)
- {
- $rcmail = rcmail::get_instance();
- $imap = $rcmail->get_storage();
-
- if ($imap->is_connected()) {
- $imap->close();
- self::$imap_ready = false;
- }
-
- $imap_host = $host ?: $rcmail->config->get('default_host');
- $a_host = parse_url($imap_host);
- if ($a_host['host']) {
- $imap_host = $a_host['host'];
- $imap_ssl = isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'));
- $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
- }
- else {
- $imap_port = 143;
- $imap_ssl = false;
- }
-
- if (!$imap->connect($imap_host, $username, $password, $imap_port, $imap_ssl)) {
- die("IMAP error: unable to authenticate with user " . TESTS_USER);
- }
-
- self::$imap_ready = true;
- }
-
- /**
- * Import the given file into IMAP
- */
- public static function import_message($filename, $mailbox = 'INBOX')
- {
- if (!self::init_imap()) {
- die(__METHOD__ . ': IMAP connection unavailable');
- }
-
- $imap = rcmail::get_instance()->get_storage();
- $imap->save_message($mailbox, file_get_contents($filename));
- }
-
- /**
- * Delete all messages from the given mailbox
- */
- public static function purge_mailbox($mailbox)
- {
- if (!self::init_imap()) {
- die(__METHOD__ . ': IMAP connection unavailable');
- }
-
- $imap = rcmail::get_instance()->get_storage();
- $imap->delete_message('*', $mailbox);
- }
-
- /**
- * Make sure the given mailbox exists in IMAP
- */
- public static function ensure_mailbox($mailbox, $empty = false)
- {
- if (!self::init_imap()) {
- die(__METHOD__ . ': IMAP connection unavailable');
- }
-
- $imap = rcmail::get_instance()->get_storage();
-
- $folders = $imap->list_folders();
- if (!in_array($mailbox, $folders)) {
- $imap->create_folder($mailbox, true);
- }
- else if ($empty) {
- $imap->delete_message('*', $mailbox);
- }
- }
-}
-
-// @TODO: make sure mailbox has some content (always the same) or is empty
-// @TODO: plugins: enable all?
-
-/**
- * Base class for all tests in this directory
- */
-class Selenium_Test extends PHPUnit_Extensions_Selenium2TestCase
-{
- protected $login_data = null;
-
- protected function setUp()
- {
- $this->setBrowser(TESTS_BROWSER);
- $this->login_data = array(TESTS_USER, TESTS_PASS);
-
- // Set root to our index.html, for better performance
- // See https://github.com/sebastianbergmann/phpunit-selenium/issues/217
- $baseurl = preg_replace('!/index(-.+)?\.php^!', '', TESTS_URL);
- $this->setBrowserUrl($baseurl . '/tests/Selenium');
- }
-
- protected function login($username = null, $password = null)
- {
- if (!empty($username)) {
- $this->login_data = array($username, $password);
- }
-
- $this->go('mail', null, true);
- }
-
- protected function do_login()
- {
- $user_input = $this->byCssSelector('form input[name="_user"]');
- $pass_input = $this->byCssSelector('form input[name="_pass"]');
- $submit = $this->byCssSelector('form input[type="submit"]');
-
- $user_input->value($this->login_data[0]);
- $pass_input->value($this->login_data[1]);
-
- // submit login form
- $submit->click();
-
- // wait after successful login
- sleep(TESTS_SLEEP);
- }
-
- protected function go($task = 'mail', $action = null, $login = true)
- {
- $this->url(TESTS_URL . '?_task=' . $task);
-
- // wait for interface load (initial ajax requests, etc.)
- sleep(TESTS_SLEEP);
-
- // check if we have a valid session
- $env = $this->get_env();
- if ($login && $env['task'] == 'login') {
- $this->do_login();
- }
-
- if ($action) {
- $this->click_button($action);
- sleep(TESTS_SLEEP);
- }
- }
-
- protected function get_env()
- {
- return $this->execute(array(
- 'script' => 'return window.rcmail ? rcmail.env : {};',
- 'args' => array(),
- ));
- }
-
- protected function get_buttons($action)
- {
- $buttons = $this->execute(array(
- 'script' => "return rcmail.buttons['$action'];",
- 'args' => array(),
- ));
-
- if (is_array($buttons)) {
- foreach ($buttons as $idx => $button) {
- $buttons[$idx] = $button['id'];
- }
- }
-
- return (array) $buttons;
- }
-
- protected function get_objects()
- {
- return $this->execute(array(
- 'script' => "var i,r = []; for (i in rcmail.gui_objects) r.push(i); return r;",
- 'args' => array(),
- ));
- }
-
- protected function click_button($action)
- {
- $buttons = $this->get_buttons($action);
- $id = array_shift($buttons);
-
- // this doesn't work for me
- $this->byId($id)->click();
- }
-
- protected function ajaxResponse($action, $script = '', $button = false)
- {
- if (!$script && !$button) {
- $script = "rcmail.command('$action')";
- }
-
- $script =
- "if (!window.test_ajax_response) {
- window.test_ajax_response_object = {};
- function test_ajax_response(response)
- {
- if (response.response && response.response.action) {
- window.test_ajax_response_object[response.response.action] = response.response;
- }
- }
- rcmail.addEventListener('responsebefore', test_ajax_response);
- }
- window.test_ajax_response_object['$action'] = null;
- $script;
- ";
-
- // run request
- $this->execute(array(
- 'script' => $script,
- 'args' => array(),
- ));
-
- if ($button) {
- $this->click_button($action);
- }
-
- // wait
- sleep(TESTS_SLEEP);
-
- // get response
- $response = $this->execute(array(
- 'script' => "return window.test_ajax_response_object ? test_ajax_response_object['$action'] : {};",
- 'args' => array(),
- ));
-
- return $response;
- }
-
- protected function getText($element)
- {
- return $element->text() ?: $element->attribute('textContent');
- }
-
- protected function assertHasClass($classname, $element)
- {
- $this->assertContains($classname, $element->attribute('class'));
- }
-}
diff --git a/tests/Selenium/index.html b/tests/Selenium/index.html
deleted file mode 100644
index 7aa65f829..000000000
--- a/tests/Selenium/index.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<html>
-<head>
- <title>Roundcube Webmail Tests</title>
-</head>
-<body>
-Testing...
-</body>
-</html>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Jan 31, 2:16 PM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426330
Default Alt Text
(143 KB)
Attached To
Mode
R3 roundcubemail
Attached
Detach File
Event Timeline
Log In to Comment