Page MenuHomePhorge

No OneTemporary

Size
282 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/program/actions/contacts/export.php b/program/actions/contacts/export.php
index 4bf216759..028cbf551 100644
--- a/program/actions/contacts/export.php
+++ b/program/actions/contacts/export.php
@@ -1,192 +1,192 @@
<?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: |
| Export the selected address book as vCard file |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_export extends rcmail_action_contacts_index
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->request_security_check(rcube_utils::INPUT_GET);
$sort_col = $rcmail->config->get('addressbook_sort_col', 'name');
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) {
$search = (array) $_SESSION['search'][$_REQUEST['_search']];
$records = [];
// Get records from all sources
foreach ($search as $s => $set) {
$source = $rcmail->get_address_book($s);
// reset page
$source->set_page(1);
$source->set_pagesize(99999);
$source->set_search_set($set);
// get records
$result = $source->list_records();
while ($record = $result->next()) {
// because vcard_map is per-source we need to create vcard here
self::prepare_for_export($record, $source);
$record['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($record, $sort_col);
$records[$key] = $record;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
$result->records = array_values($records);
}
// selected contacts
else if (!empty($_REQUEST['_cid'])) {
$records = [];
// Selected contact IDs (with multi-source support)
$cids = self::get_cids();
foreach ($cids as $s => $ids) {
$source = $rcmail->get_address_book($s);
// reset page and page size (#6103)
$source->set_page(1);
$source->set_pagesize(count($ids));
$result = $source->search('ID', $ids, 1, true, true);
while ($record = $result->next()) {
// because vcard_map is per-source we need to create vcard here
self::prepare_for_export($record, $source);
$record['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($record, $sort_col);
$records[$key] = $record;
}
}
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
$result->records = array_values($records);
}
// selected directory/group
else {
$CONTACTS = self::contact_source(null, true);
// get contacts for this user
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(99999);
$result = $CONTACTS->list_records(null, 0, true);
}
// Give plugins a possibility to implement other output formats or modify the result
$plugin = $rcmail->plugins->exec_hook('addressbook_export', ['result' => $result]);
$result = $plugin['result'];
if ($plugin['abort']) {
- exit;
+ $rcmail->output->sendExit();
}
// send downlaod headers
- header('Content-Type: text/vcard; charset=' . RCUBE_CHARSET);
- header('Content-Disposition: attachment; filename="contacts.vcf"');
+ $rcmail->output->header('Content-Type: text/vcard; charset=' . RCUBE_CHARSET);
+ $rcmail->output->header('Content-Disposition: attachment; filename="contacts.vcf"');
while ($result && ($row = $result->next())) {
if (!empty($CONTACTS)) {
self::prepare_for_export($row, $CONTACTS);
}
// fix folding and end-of-line chars
$row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']);
$row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']);
echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol;
}
- exit;
+ $rcmail->output->sendExit();
}
/**
* Copy contact record properties into a vcard object
*/
public static function prepare_for_export(&$record, $source = null)
{
$groups = $source && $source->groups && $source->export_groups ? $source->get_record_groups($record['ID']) : null;
$fieldmap = $source ? $source->vcard_map : null;
if (empty($record['vcard'])) {
$vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap);
$vcard->reset();
foreach ($record as $key => $values) {
list($field, $section) = explode(':', $key);
// avoid unwanted casting of DateTime objects to an array
// (same as in rcube_contacts::convert_save_data())
if (is_object($values) && is_a($values, 'DateTime')) {
$values = array($values);
}
foreach ((array) $values as $value) {
if (is_array($value) || is_a($value, 'DateTime') || @strlen($value)) {
$vcard->set($field, $value, strtoupper($section));
}
}
}
// append group names
if ($groups) {
$vcard->set('groups', join(',', $groups), null);
}
$record['vcard'] = $vcard->export();
}
// patch categories to alread existing vcard block
else if (!empty($record['vcard'])) {
$vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap);
// unset CATEGORIES entry, it might be not up-to-date (#1490277)
$vcard->set('groups', null);
$record['vcard'] = $vcard->export();
if (!empty($groups)) {
$vgroups = 'CATEGORIES:' . rcube_vcard::vcard_quote($groups, ',') . rcube_vcard::$eol;
$record['vcard'] = str_replace('END:VCARD', $vgroups . 'END:VCARD', $record['vcard']);
}
}
}
}
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 2e1d49561..c6911ae9c 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,1940 +1,1940 @@
<?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','oauth','dummy');
/**
* Current task.
*
* @var string
*/
public $task;
/**
* Current action.
*
* @var string
*/
public $action = '';
public $comm_path = './';
public $filename = '';
public $default_skin;
public $login_error;
public $oauth;
private $address_books = [];
private $action_map = [];
private $action_args = [];
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(isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null));
// 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 && empty($this->action)
&& (empty($_SESSION['task']) || $_SESSION['task'] != $this->task)
) {
$this->session->remove('page');
// set current task to session
$_SESSION['task'] = $this->task;
}
}
// init output class (not in CLI mode)
if (!empty($_REQUEST['_remote'])) {
$this->json_init();
}
else if (!empty($_SERVER['REMOTE_ADDR'])) {
$this->load_gui(!empty($_REQUEST['_framed']));
}
// load oauth manager
$this->oauth = rcmail_oauth::get_instance();
// 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';
}
// Re-initialize plugins if task is changing
if (!empty($this->task) && $this->task != $task) {
$this->plugins->init($this, $task);
}
$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);
$session_lang = isset($_SESSION['language']) ? $_SESSION['language'] : null;
$lang = $this->language_prop($this->config->get('language', $session_lang));
$_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');
}
}
/**
* Handle the request. All request pre-checks are NOT done here.
*/
public function action_handler()
{
// we're ready, user is authenticated and the request is safe
$plugin = $this->plugins->exec_hook('ready', ['task' => $this->task, 'action' => $this->action]);
$this->set_task($plugin['task']);
$this->action = $plugin['action'];
// handle special actions
if ($this->action == 'keep-alive') {
$this->output->reset();
$this->plugins->exec_hook('keep_alive', []);
$this->output->send();
}
$task = $this->action == 'save-pref' ? 'utils' : $this->task;
$task = $task == 'addressbook' ? 'contacts' : $task;
$task_class = "rcmail_action_{$task}_index";
// execute the action index handler
if (class_exists($task_class)) {
$task_handler = new $task_class;
$task_handler->run();
}
// allow 5 "redirects" to another action
$redirects = 0;
while ($redirects < 5) {
// execute a plugin action
if (preg_match('/^plugin\./', $this->action)) {
$this->plugins->exec_action($this->action);
break;
}
// execute action registered to a plugin task
if ($this->plugins->is_plugin_task($this->task)) {
if (!$this->action) $this->action = 'index';
$this->plugins->exec_action("{$this->task}.{$this->action}");
break;
}
$action = !empty($this->action) ? $this->action : 'index';
// handle deprecated action names
if (!empty($task_handler) && !empty($task_handler::$aliases[$action])) {
$action = $task_handler::$aliases[$action];
}
$action = str_replace('-', '_', $action);
$class = "rcmail_action_{$task}_{$action}";
// Run the action (except the index)
if ($class != $task_class && class_exists($class)) {
$handler = new $class;
if (!$handler->checks()) {
break;
}
$handler->run($this->action_args);
$redirects++;
}
else {
break;
}
}
if ($this->action == 'refresh') {
$last = intval(rcube_utils::get_input_value('_last', rcube_utils::INPUT_GPC));
$this->plugins->exec_hook('refresh', ['last' => $last]);
}
// parse main template (default)
$this->output->send($this->task);
// if we arrive here, something went wrong
$error = ['code' => 404, 'line' => __LINE__, 'file' => __FILE__, 'message' => "Invalid request"];
rcmail::raise_error($error, true, true);
}
/**
* Return instance of the internal address book class
*
* @param string $id Address book identifier. It accepts also special values:
* - rcube_addressbook::TYPE_CONTACT (or 'sql') for the SQL addressbook
* - rcube_addressbook::TYPE_DEFAULT for the 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');
$default = false;
// 'sql' is the alias for '0' used by autocomplete
if ($id == 'sql') {
$id = rcube_addressbook::TYPE_CONTACT;
}
else if ($id == rcube_addressbook::TYPE_DEFAULT || $id == -1) { // -1 for BC
$id = $this->config->get('default_addressbook');
$default = true;
}
$id = (string) $id;
// use existing instance
if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
$contacts = $this->address_books[$id];
}
else if ($id && !empty($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 === (string) rcube_addressbook::TYPE_CONTACT) {
$contacts = new rcube_contacts($this->db, $this->get_user_id());
}
else if ($id === (string) rcube_addressbook::TYPE_RECIPIENT || $id === (string) rcube_addressbook::TYPE_TRUSTED_SENDER) {
$contacts = new rcube_addresses($this->db, $this->get_user_id(), (int) $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) {
+ if (!empty($plugin['instance']) && $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');
$list = array();
// SQL-based (built-in) address book
if ($abook_type === 'sql') {
$list[rcube_addressbook::TYPE_CONTACT] = array(
'id' => (string) rcube_addressbook::TYPE_CONTACT,
'name' => $this->gettext('personaladrbook'),
'groups' => true,
'readonly' => false,
'undelete' => $this->config->get('undo_timeout') > 0,
);
}
// 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'],
);
}
}
$collected_recipients = $this->config->get('collected_recipients');
$collected_senders = $this->config->get('collected_senders');
if ($collected_recipients === (string) rcube_addressbook::TYPE_RECIPIENT) {
$list[rcube_addressbook::TYPE_RECIPIENT] = array(
'id' => (string) rcube_addressbook::TYPE_RECIPIENT,
'name' => $this->gettext('collectedrecipients'),
'groups' => false,
'readonly' => true,
'undelete' => false,
'deletable' => true,
);
}
if ($collected_senders === (string) rcube_addressbook::TYPE_TRUSTED_SENDER) {
$list[rcube_addressbook::TYPE_TRUSTED_SENDER] = array(
'id' => (string) rcube_addressbook::TYPE_TRUSTED_SENDER,
'name' => $this->gettext('trustedsenders'),
'groups' => false,
'readonly' => true,
'undelete' => false,
'deletable' => true,
);
}
// 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) {
// 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']) {
+ else if ($skip_hidden && !empty($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();
}
return $this->output;
}
/**
* Create session object and start the session.
*/
public function session_init()
{
parent::session_init();
// set initial session vars
if (empty($_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;
}
$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 username input validity
if (!$this->login_input_checks($username, $password)) {
$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);
$ssl = false;
if (!empty($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 (empty($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;
}
}
/**
* Validate username input
*
* @param string $username User name
* @param string $password User password
*
* @return bool True if valid, False otherwise
*/
private function login_input_checks($username, $password)
{
$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);
if ($username_maxlen && strlen($username) > $username_maxlen) {
return false;
}
if ($password_maxlen && strlen($password) > $password_maxlen) {
return false;
}
if ($username_filter) {
$is_email = strtolower($username_filter) == 'email';
if ($is_email && !rcube_utils::check_email($username, false)) {
return false;
}
if (!$is_email && !preg_match($username_filter, $username)) {
return false;
}
}
return true;
}
/**
* 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 a 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));
}
$task = $this->task;
if (!empty($p['_task'])) {
$task = $p['_task'];
}
else if (!empty($p['task'])) {
$task = $p['task'];
}
unset($p['task'], $p['_task']);
$pre = array('_task' => $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 = '';
if (!empty($_SERVER['REDIRECT_SCRIPT_URL'])) {
$base_path = $_SERVER['REDIRECT_SCRIPT_URL'];
}
else if (!empty($_SERVER['SCRIPT_NAME'])) {
$base_path = $_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();
}
}
$this->address_books = [];
// 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 : '') . (isset($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);
}
}
/**
* 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
* @param array $args Arguments to be passed to the next action
*/
public function overwrite_action($action, $args = [])
{
$this->action = $action;
$this->action_args = array_merge($this->action_args, $args);
$this->output->set_env('action', $action);
}
/**
* Set environment variables for specified config options
*
* @param array $options List of configuration option names
*
* @deprecated since 1.5-beta, use rcmail_action::set_env_config()
*/
public function set_env_config($options)
{
rcmail_action::set_env_config($options);
}
/**
* Insert a contact to specified addressbook.
*
* @param array $contact Contact data
* @param rcube_addressbook $source The addressbook object
* @param string $error Filled with an error message/label on error
*
* @return int|bool Contact ID on success, False otherwise
*/
public function contact_create($contact, $source, &$error = null)
{
$contact['email'] = rcube_utils::idn_to_utf8($contact['email']);
$contact = $this->plugins->exec_hook('contact_displayname', $contact);
if (empty($contact['name'])) {
$contact['name'] = rcube_addressbook::compose_display_name($contact);
}
// validate the contact
if (!$source->validate($contact, true)) {
$error = $source->get_error();
return false;
}
$plugin = $this->plugins->exec_hook('contact_create', array(
'record' => $contact,
'source' => $this->get_address_book_id($source),
));
$contact = $plugin['record'];
if (!empty($plugin['abort'])) {
$error = $plugin['message'];
return $plugin['result'];
}
return $source->insert($contact);
}
/**
* Find an email address in user addressbook(s)
*
* @param string $email Email address
* @param int $type Addressbook type (see rcube_addressbook::TYPE_* consts)
*
* @return bool True if the address exists in specified addressbook(s), False otherwise
*/
public function contact_exists($email, $type)
{
if (empty($email) || !is_string($email) || !strpos($email, '@')) {
return false;
}
// TODO: Support TYPE_READONLY filter
$sources = [];
if ($type & rcube_addressbook::TYPE_WRITEABLE) {
foreach ($this->get_address_sources(true, true) as $book) {
$sources[] = $book['id'];
}
}
if ($type & rcube_addressbook::TYPE_DEFAULT) {
if ($default = $this->get_address_book(rcube_addressbook::TYPE_DEFAULT, true)) {
$book_id = $this->get_address_book_id($default);
if (!in_array($book_id, $sources)) {
$sources[] = $book_id;
}
}
}
if ($type & rcube_addressbook::TYPE_RECIPIENT) {
$collected_recipients = $this->config->get('collected_recipients');
if (strlen($collected_recipients) && !in_array($collected_recipients, $sources)) {
array_unshift($sources, $collected_recipients);
}
}
if ($type & rcube_addressbook::TYPE_TRUSTED_SENDER) {
$collected_senders = $this->config->get('collected_senders');
if (strlen($collected_senders) && !in_array($collected_senders, $sources)) {
array_unshift($sources, $collected_senders);
}
}
$plugin = $this->plugins->exec_hook('contact_exists', array(
'email' => $email,
'type' => $type,
'sources' => $sources,
));
if (!empty($plugin['abort'])) {
return $plugin['result'];
}
foreach ($plugin['sources'] as $source) {
$contacts = $this->get_address_book($source);
if (!$contacts) {
continue;
}
$result = $contacts->search('email', $email, rcube_addressbook::SEARCH_STRICT, false);
if ($result->count) {
return true;
}
}
return false;
}
/**
* 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
* @deprecated since 1.5-beta, use rcmail_action::table_output()
*/
public function table_output($attrib, $table_data, $show_cols, $id_col)
{
return rcmail_action::table_output($attrib, $table_data, $show_cols, $id_col);
}
/**
* 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 (isset($stz)) {
date_default_timezone_set($stz);
}
return !empty($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 (!empty($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 (isset($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
* @deprecated since 1.5-beta, use rcmail_action::folder_list()
*/
public function folder_list($attrib)
{
return rcmail_action::folder_list($attrib);
}
/**
* Return folders list as html_select object
*
* @param array $p Named parameters
*
* @return html_select HTML drop-down object
* @deprecated since 1.5-beta, use rcmail_action::folder_selector()
*/
public function folder_selector($p = [])
{
return rcmail_action::folder_selector($p);
}
/**
* Returns class name for the given folder if it is a special folder
* (including shared/other users namespace roots).
*
* @param string $folder_id IMAP Folder name
*
* @return string|null CSS class name
* @deprecated since 1.5-beta, use rcmail_action::folder_classname()
*/
public function folder_classname($folder_id)
{
return rcmail_action::folder_classname($folder_id);
}
/**
* 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
* @deprecated since 1.5-beta, use rcmail_action::localize_foldername()
*/
public function localize_foldername($name, $with_path = false, $path_remove = false)
{
return rcmail_action::localize_foldername($name, $with_path, $path_remove);
}
/**
* Localize folder path
*
* @deprecated since 1.5-beta, use rcmail_action::localize_folderpath()
*/
public function localize_folderpath($path)
{
return rcmail_action::localize_folderpath($path);
}
/**
* Return HTML for quota indicator object
*
* @param array $attrib Named parameters
*
* @return string HTML code for the quota indicator object
* @deprecated since 1.5-beta, use rcmail_action::quota_display()
*/
public static function quota_display($attrib)
{
return rcmail_action::quota_display($attrib);
}
/**
* Return (parsed) quota information
*
* @param array $attrib Named parameters
* @param array $folder Current folder
*
* @return array Quota information
* @deprecated since 1.5-beta, use rcmail_action::quota_content()
*/
public function quota_content($attrib = null, $folder = null)
{
return rcmail_action::quota_content($attrib, $folder);
}
/**
* 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)
*
* @deprecated since 1.5-beta, use rcmail_action::display_server_error()
*/
public function display_server_error($fallback = null, $fallback_args = null, $suffix = '', $params = [])
{
rcmail_action::display_server_error($fallback, $fallback_args, $suffix, $params);
}
/**
* Displays an error message on storage fatal errors
*
* @deprecated since 1.5-beta, use rcmail_action::storage_fatal_error()
*/
public function storage_fatal_error()
{
rcmail_action::storage_fatal_error();
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*
* @deprecated since 1.5-beta, use rcmail_action::html_editor()
*/
public function html_editor($mode = '')
{
rcmail_action::html_editor($mode);
}
/**
* 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
* @deprecated since 1.5-beta, use rcmail_action::upload_init()
*/
public function upload_init($max_size = null)
{
return rcmail_action::upload_init($max_size);
}
/**
* 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
* @deprecated since 1.5-beta, use rcmail_action::upload_form()
*/
public function upload_form($attrib, $name, $action, $input_attr = [], $max_size = null)
{
return rcmail_action::upload_form($attrib, $name, $action, $input_attr, $max_size);
}
/**
* Outputs uploaded file content (with image thumbnails support
*
* @param array $file Upload file data
*
* @deprecated since 1.5-beta, use rcmail_action::display_uploaded_file()
*/
public function display_uploaded_file($file)
{
rcmail_action::display_uploaded_file($file);
}
/**
* Initializes client-side autocompletion.
*
* @deprecated since 1.5-beta, use rcmail_action::autocomplete_init()
*/
public function autocomplete_init()
{
rcmail_action::autocomplete_init();
}
/**
* Returns supported font-family specifications
*
* @param string $font Font name
*
* @return string|array Font-family specification array or string (if $font is used)
* @deprecated since 1.5-beta, use rcmail_action::autocomplete_init()
*/
public static function font_defs($font = null)
{
return rcmail_action::font_defs($font);
}
/**
* 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
* @deprecated since 1.5-beta, use rcmail_action::show_bytes()
*/
public function show_bytes($bytes, &$unit = null)
{
return rcmail_action::show_bytes($bytes, $unit);
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_part $part Message part
*
* @return string Part size (and unit)
* @deprecated since 1.5-beta, use rcmail_action::message_part_size()
*/
public function message_part_size($part)
{
return rcmail_action::message_part_size($part);
}
/**
* 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
* @deprecated since 1.5-beta, use rcmail_action::get_uids()
*/
public static function get_uids($uids = null, $mbox = null, &$is_multifolder = false, $mode = null)
{
return rcmail_action::get_uids($uids, $mbox, $is_multifolder, $mode);
}
/**
* Get resource file content (with assets_dir support)
*
* @param string $name File name
*
* @return string File content
* @deprecated since 1.5-beta, use rcmail_action::get_resource_content()
*/
public function get_resource_content($name)
{
return rcmail_action::get_resource_content($name);
}
/**
* 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/program/include/rcmail_output.php b/program/include/rcmail_output.php
index 1c8677f7f..6385d9317 100644
--- a/program/include/rcmail_output.php
+++ b/program/include/rcmail_output.php
@@ -1,144 +1,144 @@
<?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. |
| |
| CONTENTS: |
| Abstract class for output generation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Webmail
* @subpackage View
*/
abstract class rcmail_output extends rcube_output
{
const JS_OBJECT_NAME = 'rcmail';
const BLANK_GIF = 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7';
public $type = 'html';
public $ajax_call = false;
public $framed = false;
protected $pagetitle = '';
protected $object_handlers = array();
protected $devel_mode = false;
/**
* Object constructor
*/
public function __construct()
{
parent::__construct();
$this->devel_mode = (bool) $this->config->get('devel_mode');
}
/**
* Setter for page title
*
* @param string $title Page title
*/
public function set_pagetitle($title)
{
$this->pagetitle = $title;
}
/**
* Getter for the current skin path property
*/
public function get_skin_path()
{
return $this->config->get('skin_path');
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
parent::reset();
$this->object_handlers = array();
$this->pagetitle = '';
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
abstract function command();
/**
* Add a localized label to the client environment
*/
abstract function add_label();
/**
* Register a template object handler
*
* @param string $name Object name
* @param string $func Function name to call
*
* @return void
*/
public function add_handler($name, $func)
{
$this->object_handlers[$name] = $func;
}
/**
* Register a list of template object handlers
*
* @param array $handlers Hash array with object=>handler pairs
*
* @return void
*/
public function add_handlers($handlers)
{
$this->object_handlers = array_merge($this->object_handlers, $handlers);
}
/**
* A wrapper for header() function, so it can be replaced for automated tests
*
* @param string $header The header string
* @param bool $replace Replace previously set header?
*/
public function header($header, $replace = true)
{
header($header, $replace);
}
/**
* A helper to send output to the browser and exit
*
* @param string $body The output body
* @param array $headers Headers
*/
- public function sendExit($body, $headers = [])
+ public function sendExit($body = '', $headers = [])
{
foreach ($headers as $header) {
header($header);
}
print $body;
exit;
}
}
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index 99bd5ff55..25452a9e5 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2626 +1,2626 @@
<?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: |
| Class to handle HTML page output using a skin template. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create HTML page output using a skin template
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_html extends rcmail_output
{
public $type = 'html';
protected $message;
protected $template_name;
protected $objects = array();
protected $js_env = array();
protected $js_labels = array();
protected $js_commands = array();
protected $skin_paths = array();
protected $skin_name = '';
protected $scripts_path = '';
protected $script_files = array();
protected $css_files = array();
protected $scripts = array();
protected $meta_tags = array();
protected $link_tags = array('shortcut icon' => '');
protected $header = '';
protected $footer = '';
protected $body = '';
protected $base_path = '';
protected $assets_path;
protected $assets_dir = RCUBE_INSTALL_PATH;
protected $devel_mode = false;
protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
// deprecated names of templates used before 0.5
protected $deprecated_templates = array(
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
);
// deprecated names of template objects used before 1.4
protected $deprecated_template_objects = array(
'addressframe' => 'contentframe',
'messagecontentframe' => 'contentframe',
'prefsframe' => 'contentframe',
'folderframe' => 'contentframe',
'identityframe' => 'contentframe',
'responseframe' => 'contentframe',
'keyframe' => 'contentframe',
'filterframe' => 'contentframe',
);
/**
* Constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = $this->config->get('devel_mode');
$this->set_env('task', $task);
$this->set_env('standard_windows', (bool) $this->config->get('standard_windows'));
$this->set_env('locale', !empty($_SESSION['language']) ? $_SESSION['language'] : 'en_US');
$this->set_env('devel_mode', $this->devel_mode);
// Version number e.g. 1.4.2 will be 10402
$version = explode('.', preg_replace('/[^0-9.].*/', '', RCMAIL_VERSION));
$this->set_env('rcversion', $version[0] * 10000 + $version[1] * 100 + (isset($version[2]) ? $version[2] : 0));
// add cookie info
$this->set_env('cookie_domain', ini_get('session.cookie_domain'));
$this->set_env('cookie_path', ini_get('session.cookie_path'));
$this->set_env('cookie_secure', filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN));
// Easy way to change skin via GET argument, for developers
if ($this->devel_mode && !empty($_GET['skin']) && preg_match('/^[a-z0-9-_]+$/i', $_GET['skin'])) {
if ($this->check_skin($_GET['skin'])) {
$this->set_skin($_GET['skin']);
$this->app->user->save_prefs(array('skin' => $_GET['skin']));
}
}
// load and setup the skin
$this->set_skin($this->config->get('skin'));
$this->set_assets_path($this->config->get('assets_path'), $this->config->get('assets_dir'));
if (!empty($_REQUEST['_extwin']))
$this->set_env('extwin', 1);
if ($this->framed || $framed)
$this->set_env('framed', 1);
$lic = <<<EOF
/*
@licstart The following is the entire license notice for the
JavaScript code in this page.
Copyright (C) The Roundcube Dev Team
The JavaScript code in this page is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
The code is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU GPL for more details.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
EOF;
// add common javascripts
$this->add_script($lic, 'head_top');
$this->add_script('var '.self::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
// don't wait for page onload. Call init at the bottom of the page (delayed)
$this->add_script(self::JS_OBJECT_NAME.'.init();', 'docready');
$this->scripts_path = 'program/js/';
$this->include_script('jquery.min.js');
$this->include_script('common.js');
$this->include_script('app.js');
// register common UI objects
$this->add_handlers(array(
'loginform' => array($this, 'login_form'),
'preloader' => array($this, 'preloader'),
'username' => array($this, 'current_username'),
'message' => array($this, 'message_container'),
'charsetselector' => array($this, 'charset_selector'),
'aboutcontent' => array($this, 'about_content'),
));
// set blankpage (watermark) url
$blankpage = $this->config->get('blankpage_url', '/watermark.html');
$this->set_env('blankpage', $blankpage);
}
/**
* Set environment variable
*
* @param string $name Property name
* @param mixed $value Property value
* @param boolean $addtojs True if this property should be added
* to client environment
*/
public function set_env($name, $value, $addtojs = true)
{
$this->env[$name] = $value;
if ($addtojs || isset($this->js_env[$name])) {
$this->js_env[$name] = $value;
}
}
/**
* Parse and set assets path
*
* @param string $path Assets path URL (relative or absolute)
* @param string $fs_dif Assets path in filesystem
*/
public function set_assets_path($path, $fs_dir = null)
{
if (empty($path)) {
return;
}
$path = rtrim($path, '/') . '/';
// handle relative assets path
if (!preg_match('|^https?://|', $path) && $path[0] != '/') {
// save the path to search for asset files later
$this->assets_dir = $path;
$base = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
$base = rtrim($base, '/');
// remove url token if exists
if ($len = intval($this->config->get('use_secure_urls'))) {
$_base = explode('/', $base);
$last = count($_base) - 1;
$length = $len > 1 ? $len : 16; // as in rcube::get_secure_url_token()
// we can't use real token here because it
// does not exists in unauthenticated state,
// hope this will not produce false-positive matches
if ($last > -1 && preg_match('/^[a-f0-9]{' . $length . '}$/', $_base[$last])) {
$path = '../' . $path;
}
}
}
// set filesystem path for assets
if ($fs_dir) {
if ($fs_dir[0] != '/') {
$fs_dir = realpath(RCUBE_INSTALL_PATH . $fs_dir);
}
// ensure the path ends with a slash
$this->assets_dir = rtrim($fs_dir, '/') . '/';
}
$this->assets_path = $path;
$this->set_env('assets_path', $path);
}
/**
* Getter for the current page title
*
* @param bool $full Prepend title with product/user name
*
* @return string The page title
*/
protected function get_pagetitle($full = true)
{
if (!empty($this->pagetitle)) {
$title = $this->pagetitle;
}
else if ($this->env['task'] == 'login') {
$title = $this->app->gettext(array(
'name' => 'welcome',
'vars' => array('product' => $this->config->get('product_name')
)));
}
else {
$title = ucfirst($this->env['task']);
}
if ($full) {
if ($this->devel_mode && !empty($_SESSION['username'])) {
$title = $_SESSION['username'] . ' :: ' . $title;
}
else if ($prod_name = $this->config->get('product_name')) {
$title = $prod_name . ' :: ' . $title;
}
}
return $title;
}
/**
* Getter for the current skin path property
*/
public function get_skin_path()
{
return $this->skin_paths[0];
}
/**
* Set skin
*
* @param string $skin Skin name
*/
public function set_skin($skin)
{
if (!$this->check_skin($skin)) {
// If the skin does not exist (could be removed or invalid),
// fallback to the skin set in the system configuration (#7271)
$skin = $this->config->system_skin;
}
$skin_path = 'skins/' . $skin;
$this->config->set('skin_path', $skin_path);
$this->base_path = $skin_path;
// register skin path(s)
$this->skin_paths = array();
$this->skins = array();
$this->load_skin($skin_path);
$this->skin_name = $skin;
$this->set_env('skin', $skin);
}
/**
* Check skin validity/existence
*
* @param string $skin Skin name
*
* @return bool True if the skin exist and is readable, False otherwise
*/
public function check_skin($skin)
{
// Sanity check to prevent from path traversal vulnerability (#1490620)
if (strpos($skin, '/') !== false || strpos($skin, "\\") !== false) {
rcube::raise_error(array(
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Invalid skin name'
), true, false);
return false;
}
$skins_allowed = $this->config->get('skins_allowed');
if (!empty($skins_allowed) && !in_array($skin, (array) $skins_allowed)) {
return false;
}
$path = RCUBE_INSTALL_PATH . 'skins/';
return !empty($skin) && is_dir($path . $skin) && is_readable($path . $skin);
}
/**
* Helper method to recursively read skin meta files and register search paths
*/
private function load_skin($skin_path)
{
$this->skin_paths[] = $skin_path;
// read meta file and check for dependencies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
$meta['path'] = $skin_path;
$path_elements = explode('/', $skin_path);
$skin_id = end($path_elements);
if (empty($meta['name'])) {
$meta['name'] = $skin_id;
}
$this->skins[$skin_id] = $meta;
// Keep skin config for ajax requests (#6613)
$_SESSION['skin_config'] = array();
if (!empty($meta['extends'])) {
$path = RCUBE_INSTALL_PATH . 'skins/';
if (is_dir($path . $meta['extends']) && is_readable($path . $meta['extends'])) {
$_SESSION['skin_config'] = $this->load_skin('skins/' . $meta['extends']);
}
}
if (!empty($meta['config'])) {
foreach ($meta['config'] as $key => $value) {
$this->config->set($key, $value, true);
$_SESSION['skin_config'][$key] = $value;
}
$value = array_merge((array) $this->config->get('dont_override'), array_keys($meta['config']));
$this->config->set('dont_override', $value, true);
}
if (!empty($meta['localization'])) {
$locdir = $meta['localization'] === true ? 'localization' : $meta['localization'];
if ($texts = $this->app->read_localization(RCUBE_INSTALL_PATH . $skin_path . '/' . $locdir)) {
$this->app->load_language($_SESSION['language'], $texts);
}
}
// Use array_merge() here to allow for global default and extended skins
if (!empty($meta['meta'])) {
$this->meta_tags = array_merge($this->meta_tags, (array) $meta['meta']);
}
if (!empty($meta['links'])) {
$this->link_tags = array_merge($this->link_tags, (array) $meta['links']);
}
return $_SESSION['skin_config'];
}
/**
* Check if a specific template exists
*
* @param string $name Template name
*
* @return boolean True if template exists
*/
public function template_exists($name)
{
foreach ($this->skin_paths as $skin_path) {
$filename = RCUBE_INSTALL_PATH . $skin_path . '/templates/' . $name . '.html';
if ((is_file($filename) && is_readable($filename))
|| ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name]))
) {
return true;
}
}
return false;
}
/**
* Find the given file in the current skin path stack
*
* @param string $file File name/path to resolve (starting with /)
* @param string &$skin_path Reference to the base path of the matching skin
* @param string $add_path Additional path to search in
* @param bool $minified Fallback to a minified version of the file
*
* @return mixed Relative path to the requested file or False if not found
*/
public function get_skin_file($file, &$skin_path = null, $add_path = null, $minified = false)
{
$skin_paths = $this->skin_paths;
if ($add_path) {
array_unshift($skin_paths, $add_path);
$skin_paths = array_unique($skin_paths);
}
if ($skin_path = $this->find_file_path($file, $skin_paths)) {
return $skin_path . $file;
}
if ($minified && preg_match('/(?<!\.min)\.(js|css)$/', $file)) {
$file = preg_replace('/\.(js|css)$/', '.min.\\1', $file);
if ($skin_path = $this->find_file_path($file, $skin_paths)) {
return $skin_path . $file;
}
}
return false;
}
/**
* Find path of the asset file
*/
protected function find_file_path($file, $skin_paths)
{
foreach ($skin_paths as $skin_path) {
if ($this->assets_dir != RCUBE_INSTALL_PATH) {
if (realpath($this->assets_dir . $skin_path . $file)) {
return $skin_path;
}
}
if (realpath(RCUBE_INSTALL_PATH . $skin_path . $file)) {
return $skin_path;
}
}
}
/**
* Register a GUI object to the client script
*
* @param string $obj Object name
* @param string $id Object ID
*/
public function add_gui_object($obj, $id)
{
$this->add_script(self::JS_OBJECT_NAME.".gui_object('$obj', '$id');");
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
public function command()
{
$cmd = func_get_args();
if (strpos($cmd[0], 'plugin.') !== false)
$this->js_commands[] = array('triggerEvent', $cmd[0], $cmd[1]);
else
$this->js_commands[] = $cmd;
}
/**
* Add a localized label to the client environment
*/
public function add_label()
{
$args = func_get_args();
if (count($args) == 1 && is_array($args[0])) {
$args = $args[0];
}
foreach ($args as $name) {
$this->js_labels[$name] = $this->app->gettext($name);
}
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message display time in seconds
*
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
{
if ($override || !$this->message) {
if ($this->app->text_exists($message)) {
if (!empty($vars)) {
$vars = array_map(array('rcube','Q'), $vars);
}
$msgtext = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
else {
$msgtext = $message;
}
$this->message = $message;
$this->command('display_message', $msgtext, $type, $timeout * 1000);
}
}
/**
* Delete all stored env variables and commands
*
* @param bool $all Reset all env variables (including internal)
*/
public function reset($all = false)
{
$framed = $this->framed;
- $task = $this->env['task'];
- $env = $all ? null : array_intersect_key($this->env, array('extwin'=>1, 'framed'=>1));
+ $task = isset($this->env['task']) ? $this->env['task'] : '';
+ $env = $all ? null : array_intersect_key($this->env, array('extwin' => 1, 'framed' => 1));
// keep jQuery-UI files
$css_files = $script_files = array();
foreach ($this->css_files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$css_files[] = $file;
}
}
foreach ($this->script_files as $position => $files) {
foreach ($files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$script_files[$position][] = $file;
}
}
}
parent::reset();
// let some env variables survive
$this->env = $this->js_env = $env;
$this->framed = $framed || !empty($this->env['framed']);
$this->js_labels = array();
$this->js_commands = array();
$this->scripts = array();
$this->header = '';
$this->footer = '';
$this->body = '';
$this->css_files = array();
$this->script_files = array();
// load defaults
if (!$all) {
$this->__construct();
}
// Note: we merge jQuery-UI scripts after jQuery...
$this->css_files = array_merge($this->css_files, $css_files);
$this->script_files = array_merge_recursive($this->script_files, $script_files);
$this->set_env('orig_task', $task);
}
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
* @param bool $secure Redirect to secure location (see rcmail::url())
*/
public function redirect($p = array(), $delay = 1, $secure = false)
{
if (!empty($this->env['extwin'])) {
$p['extwin'] = 1;
}
$location = $this->app->url($p, false, false, $secure);
$this->header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin template.
*
* @param string $templ Template name
* @param boolean $exit True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
if ($templ != 'iframe') {
// prevent from endless loops
if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {
rcube::raise_error(array('code' => 505, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => 'Recursion alert: ignoring output->send()'), true, false);
return;
}
$this->parse($templ, false);
}
else {
$this->framed = true;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
if (!empty($this->script_files)) {
$this->set_env('request_token', $this->app->get_request_token());
}
// Fix assets path on blankpage
if (!empty($this->js_env['blankpage'])) {
$this->js_env['blankpage'] = $this->asset_url($this->abs_url($this->js_env['blankpage'], true));
}
$commands = $this->get_js_commands($framed);
// if all js commands go to parent window we can ignore all
// script files and skip rcube_webmail initialization (#1489792)
// but not on error pages where skins may need jQuery, etc.
if ($framed && empty($this->js_env['server_error'])) {
$this->scripts = array();
$this->script_files = array();
$this->header = '';
$this->footer = '';
}
// write all javascript commands
if (!empty($commands)) {
$this->add_script($commands, 'head_top');
}
$this->page_headers();
// call super method
$this->_write($template);
}
/**
* Send common page headers
* For now it only (re)sets X-Frame-Options when needed
*/
public function page_headers()
{
if (headers_sent()) {
return;
}
// allow (legal) iframe content to be loaded
$framed = $this->framed || !empty($this->env['framed']);
if ($framed && ($xopt = $this->app->config->get('x_frame_options', 'sameorigin'))) {
if (strtolower($xopt) === 'deny') {
$this->header('X-Frame-Options: sameorigin', true);
}
}
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string $name Template name
* @param boolean $exit Exit script
* @param boolean $write Don't write to stdout, return parsed content instead
*
* @link http://php.net/manual/en/function.exit.php
*/
function parse($name = 'main', $exit = true, $write = true)
{
$plugin = false;
$realname = $name;
$skin_dir = '';
$plugin_skin_paths = array();
$this->template_name = $realname;
$temp = explode('.', $name, 2);
if (count($temp) > 1) {
$plugin = $temp[0];
$name = $temp[1];
$skin_dir = $plugin . '/skins/' . $this->config->get('skin');
// apply skin search escalation list to plugin directory
foreach ($this->skin_paths as $skin_path) {
$plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path;
}
// prepend plugin skin paths to search list
$this->skin_paths = array_merge($plugin_skin_paths, $this->skin_paths);
}
// find skin template
$path = false;
foreach ($this->skin_paths as $skin_path) {
// when requesting a plugin template ignore global skin path(s)
if ($plugin && strpos($skin_path, $this->app->plugins->url) !== 0) {
continue;
}
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$name.html";
// fallback to deprecated template names
if (!is_readable($path) && ($dname = $this->deprecated_templates[$realname])) {
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$dname.html";
if (is_readable($path)) {
rcube::raise_error(array(
'code' => 502, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Using deprecated template '$dname' in $skin_path/templates. Please rename to '$realname'"
), true, false);
}
}
if (is_readable($path)) {
$this->config->set('skin_path', $skin_path);
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$skin_dir = preg_replace('!^plugins/!', '', $skin_path);
break;
}
else {
$path = false;
}
}
// read template file
if (!$path || ($templ = @file_get_contents($path)) === false) {
rcube::raise_error(array(
'code' => 404,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$realname
), true, $write);
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
return false;
}
// replace all path references to plugins/... with the configured plugins dir
// and /this/ to the current plugin skin directory
if ($plugin) {
$templ = preg_replace(
array('/\bplugins\//', '/(["\']?)\/this\//'),
array($this->app->plugins->url, '\\1'.$this->app->plugins->url.$skin_dir.'/'),
$templ
);
}
// parse for specialtags
$output = $this->parse_conditions($templ);
$output = $this->parse_xml($output);
// trigger generic hook where plugins can put additional content to the page
$hook = $this->app->plugins->exec_hook("render_page", array(
'template' => $realname, 'content' => $output, 'write' => $write));
// save some memory
$output = $hook['content'];
unset($hook['content']);
// remove plugin skin paths from current context
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
if (!$write) {
return $this->postrender($output);
}
$this->write(trim($output));
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*/
protected function get_js_commands(&$framed = null)
{
$out = '';
$parent_commands = 0;
$parent_prefix = '';
$top_commands = array();
// these should be always on top,
// e.g. hide_message() below depends on env.framed
if (!$this->framed && !empty($this->js_env)) {
$top_commands[] = array('set_env', $this->js_env);
}
if (!empty($this->js_labels)) {
$top_commands[] = array('add_label', $this->js_labels);
}
// unlock interface after iframe load
$unlock = isset($_REQUEST['_unlock']) ? preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']) : 0;
if ($this->framed) {
$top_commands[] = array('iframe_loaded', $unlock);
}
else if ($unlock) {
$top_commands[] = array('hide_message', $unlock);
}
$commands = array_merge($top_commands, $this->js_commands);
foreach ($commands as $i => $args) {
$method = array_shift($args);
$parent = $this->framed || preg_match('/^parent\./', $method);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg, $this->devel_mode);
}
if ($parent) {
$parent_commands++;
$method = preg_replace('/^parent\./', '', $method);
$parent_prefix = 'if (window.parent && parent.' . self::JS_OBJECT_NAME . ') parent.';
$method = $parent_prefix . self::JS_OBJECT_NAME . '.' . $method;
}
else {
$method = self::JS_OBJECT_NAME . '.' . $method;
}
$out .= sprintf("%s(%s);\n", $method, implode(',', $args));
}
$framed = $parent_prefix && $parent_commands == count($commands);
// make the output more compact if all commands go to parent window
if ($framed) {
$out = "if (window.parent && parent." . self::JS_OBJECT_NAME . ") {\n"
. str_replace($parent_prefix, "\tparent.", $out)
. "}\n";
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string $str Input string
* @param bool $search_path True if URL should be resolved using the current skin path stack
*
* @return string URL
*/
public function abs_url($str, $search_path = false)
{
if ($str[0] == '/') {
if ($search_path && ($file_url = $this->get_skin_file($str))) {
return $file_url;
}
return $this->base_path . $str;
}
return $str;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
$args = [
'code' => $code,
'message' => $message,
];
$page = new rcmail_action_utils_error;
$page->run($args);
}
/**
* Modify path by adding URL prefix if configured
*
* @param string $path Asset path
* @param bool $abs_url Pass to self::abs_url() first
*
* @return string Asset path
*/
public function asset_url($path, $abs_url = false)
{
// iframe content can't be in a different domain
// @TODO: check if assests are on a different domain
if ($abs_url) {
$path = $this->abs_url($path, true);
}
if (!$this->assets_path || in_array($path[0], array('?', '/', '.')) || strpos($path, '://')) {
return $path;
}
return $this->assets_path . $path;
}
/***** Template parsing methods *****/
/**
* Replace all strings ($varname)
* with the content of the according global variable.
*/
protected function parse_with_globals($input)
{
$GLOBALS['__version'] = html::quote(RCMAIL_VERSION);
$GLOBALS['__comm_path'] = html::quote($this->app->comm_path);
$GLOBALS['__skin_path'] = html::quote($this->base_path);
return preg_replace_callback('/\$(__[a-z0-9_\-]+)/',
array($this, 'globals_callback'), $input);
}
/**
* Callback function for preg_replace_callback() in parse_with_globals()
*/
protected function globals_callback($matches)
{
return $GLOBALS[$matches[1]];
}
/**
* Correct absolute paths in images and other tags (add cache busters)
*/
protected function fix_paths($output)
{
return preg_replace_callback(
'!(src|href|background|data-src-[a-z]+)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
array($this, 'file_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_paths()
*
* @return string Parsed string
*/
protected function file_callback($matches)
{
$file = $matches[3];
$file = preg_replace('!^/this/!', '/', $file);
// correct absolute paths
if ($file[0] == '/') {
$this->get_skin_file($file, $skin_path, $this->base_path);
$file = ($skin_path ?: $this->base_path) . $file;
}
// add file modification timestamp
if (preg_match('/\.(js|css|less|ico|png|svg|jpeg)$/', $file)) {
$file = $this->file_mod($file);
}
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Correct paths of asset files according to assets_path
*/
protected function fix_assets_paths($output)
{
return preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.?=-]+)(["\'\s>])!i',
array($this, 'assets_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_assets_paths()
*
* @return string Parsed string
*/
protected function assets_callback($matches)
{
$file = $this->asset_url($matches[3]);
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Modify file by adding mtime indicator
*/
protected function file_mod($file)
{
$fs = false;
$ext = substr($file, strrpos($file, '.') + 1);
// use minified file if exists (not in development mode)
if (!$this->devel_mode && !preg_match('/\.min\.' . $ext . '$/', $file)) {
$minified_file = substr($file, 0, strlen($ext) * -1) . 'min.' . $ext;
if ($fs = @filemtime($this->assets_dir . $minified_file)) {
return $minified_file . '?s=' . $fs;
}
}
if ($fs = @filemtime($this->assets_dir . $file)) {
$file .= '?s=' . $fs;
}
return $file;
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input Template content
*
* @return string
* @uses rcmail_output_html::parse_xml()
* @since 0.1-rc1
*/
public function just_parse($input)
{
$input = $this->parse_conditions($input);
$input = $this->parse_xml($input);
$input = $this->postrender($input);
return $input;
}
/**
* Parse for conditional tags
*/
protected function parse_conditions($input)
{
while (preg_match('/<roundcube:if\s+[^>]+>(((?!<roundcube:(if|endif)).)*)<roundcube:endif[^>]*>/is', $input, $conditions)) {
$result = $this->eval_condition($conditions[0]);
$input = str_replace($conditions[0], $result, $input);
}
return $input;
}
/**
* Process & evaluate conditional tags
*/
protected function eval_condition($input)
{
$matches = preg_split('/<roundcube:(if|elseif|else|endif)\s*([^>]*)>\n?/is', $input, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($matches && count($matches) == 4) {
if (preg_match('/^(else|endif)$/i', $matches[1])) {
return $matches[0] . $this->eval_condition($matches[3]);
}
$attrib = html::parse_attrib_string($matches[2]);
if (isset($attrib['condition'])) {
$condmet = $this->check_condition($attrib['condition']);
$condparts = preg_split('/<roundcube:((elseif|else|endif)[^>]*)>\n?/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE);
if ($condmet) {
$result = $condparts[0];
if ($condparts[2] != 'endif') {
$result .= preg_replace('/.*<roundcube:endif[^>]*>\n?/Uis', '', $condparts[3], 1);
}
else {
$result .= $condparts[3];
}
}
else {
$result = "<roundcube:$condparts[1]>" . $condparts[3];
}
return $matches[0] . $this->eval_condition($result);
}
rcube::raise_error(array(
'code' => 500, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @param string $condition Condition statement
*
* @return boolean True if condition is met, False if not
* @todo Extend this to allow real conditions, not just "set"
*/
protected function check_condition($condition)
{
return $this->eval_expression($condition);
}
/**
* Inserts hidden field with CSRF-prevention-token into POST forms
*/
protected function alter_form_tag($matches)
{
$out = $matches[0];
$attrib = html::parse_attrib_string($matches[1]);
if (strtolower($attrib['method']) == 'post') {
$hidden = new html_hiddenfield(array('name' => '_token', 'value' => $this->app->get_request_token()));
$out .= "\n" . $hidden->show();
}
return $out;
}
/**
* Parse & evaluate a given expression and return its result.
*
* @param string $expression Expression statement
*
* @return mixed Expression result
*/
protected function eval_expression($expression)
{
$expression = preg_replace(
array(
'/session:([a-z0-9_]+)/i',
'/config:([a-z0-9_]+)(:([a-z0-9_]+))?/i',
'/env:([a-z0-9_]+)/i',
'/request:([a-z0-9_]+)/i',
'/cookie:([a-z0-9_]+)/i',
'/browser:([a-z0-9_]+)/i',
'/template:name/i',
),
array(
"(isset(\$_SESSION['\\1']) ? \$_SESSION['\\1'] : null)",
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"(isset(\$this->env['\\1']) ? \$this->env['\\1'] : null)",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"(isset(\$_COOKIE['\\1']) ? \$_COOKIE['\\1'] : null)",
"(isset(\$this->browser->{'\\1'}) ? \$this->browser->{'\\1'} : null)",
"'{$this->template_name}'",
),
$expression
);
// Note: We used create_function() before but it's deprecated in PHP 7.2
// and really it was just a wrapper on eval().
return eval("return ($expression);");
}
/**
* Parse variable strings
*
* @param string $type Variable type (env, config etc)
* @param string $name Variable name
*
* @return mixed Variable value
*/
protected function parse_variable($type, $name)
{
$value = '';
switch ($type) {
case 'env':
$value = $this->env[$name];
break;
case 'config':
$value = $this->config->get($name);
if (is_array($value) && $value[$_SESSION['storage_host']]) {
$value = $value[$_SESSION['storage_host']];
}
break;
case 'request':
$value = rcube_utils::get_input_value($name, rcube_utils::INPUT_GPC);
break;
case 'session':
$value = $_SESSION[$name];
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name], ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
return $value;
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string $input Input string to parse
*
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
protected function parse_xml($input)
{
return preg_replace_callback('/<roundcube:([-_a-z]+)\s+((?:[^>]|\\\\>)+)(?<!\\\\)>/Ui', array($this, 'xml_command'), $input);
}
/**
* Callback function for parsing an xml command tag
* and turn it into real html content
*
* @param array $matches Matches array of preg_replace_callback
*
* @return string Tag/Object content
*/
protected function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = html::parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// localize title and summary attributes
if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
$attrib['title'] = $this->app->gettext($attrib['title']);
}
if ($command != 'button' && !empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) {
$attrib['summary'] = $this->app->gettext($attrib['summary']);
}
// execute command
switch ($command) {
// return a button
case 'button':
if (!empty($attrib['name']) || !empty($attrib['command'])) {
return $this->button($attrib);
}
break;
// frame
case 'frame':
return $this->frame($attrib);
break;
// show a label
case 'label':
if (!empty($attrib['expression'])) {
$attrib['name'] = $this->eval_expression($attrib['expression']);
}
if (!empty($attrib['name']) || !empty($attrib['command'])) {
$vars = $attrib + array('product' => $this->config->get('product_name'));
unset($vars['name'], $vars['command']);
$label = $this->app->gettext($attrib + array('vars' => $vars));
$quoting = null;
if (!empty($attrib['quoting'])) {
$quoting = strtolower($attrib['quoting']);
}
else if (isset($attrib['html'])) {
$quoting = rcube_utils::get_boolean((string) $attrib['html']) ? 'no' : '';
}
// 'noshow' can be used in skins to define new labels
if (!empty($attrib['noshow'])) {
return '';
}
switch ($quoting) {
case 'no':
case 'raw':
break;
case 'javascript':
case 'js':
$label = rcube::JQ($label);
break;
default:
$label = html::quote($label);
break;
}
return $label;
}
break;
case 'add_label':
$this->add_label($attrib['name']);
break;
// include a file
case 'include':
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
break;
}
if ($attrib['file'][0] != '/') {
$attrib['file'] = '/templates/' . $attrib['file'];
}
$old_base_path = $this->base_path;
$include = '';
$attr_skin_path = !empty($attrib['skinpath']) ? $attrib['skinpath'] : null;
if (!empty($attrib['skin_path'])) {
$attr_skin_path = $attrib['skin_path'];
}
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attr_skin_path)) {
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$path = realpath(RCUBE_INSTALL_PATH . $path);
}
if (is_readable($path)) {
$allow_php = $this->config->get('skin_include_php');
$include = $allow_php ? $this->include_php($path) : file_get_contents($path);
$include = $this->parse_conditions($include);
$include = $this->parse_xml($include);
$include = $this->fix_paths($include);
}
$this->base_path = $old_base_path;
return $include;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib);
return $hook['content'];
// define a container block
case 'container':
if ($attrib['name'] && $attrib['id']) {
$this->command('gui_container', $attrib['name'], $attrib['id']);
// let plugins insert some content here
$hook = $this->app->plugins->exec_hook("template_container", $attrib);
return $hook['content'];
}
break;
// return code for a specific application object
case 'object':
$object = strtolower($attrib['name']);
$content = '';
$handler = null;
// correct deprecated object names
if (!empty($this->deprecated_template_objects[$object])) {
$object = $this->deprecated_template_objects[$object];
}
if (!empty($this->object_handlers[$object])) {
$handler = $this->object_handlers[$object];
}
// execute object handler function
if (is_callable($handler)) {
$this->prepare_object_attribs($attrib);
// We assume that objects with src attribute are internal (in most
// cases this is a watermark frame). We need this to make sure assets_path
// is added to the internal assets paths
$external = empty($attrib['src']);
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"')));
// 'type' attribute added in 1.4 was renamed 'logo-type' in 1.5
// check both for backwards compatibility
$logo_type = !empty($attrib['logo-type']) ? $attrib['logo-type'] : null;
$logo_match = !empty($attrib['logo-match']) ? $attrib['logo-match'] : null;
if (!empty($attrib['type']) && empty($logo_type)) {
$logo_type = $attrib['type'];
}
if (($template_logo = $this->get_template_logo($logo_type, $logo_match)) !== null) {
$attrib['src'] = $template_logo;
}
$additional_logos = array();
$logo_types = (array) $this->config->get('additional_logo_types');
foreach ($logo_types as $type) {
if (($template_logo = $this->get_template_logo($type)) !== null) {
$additional_logos[$type] = $this->abs_url($template_logo);
}
}
if (!empty($additional_logos)) {
$this->set_env('additional_logos', $additional_logos);
}
if (!empty($attrib['src'])) {
$content = html::img($attrib);
}
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle(false));
}
else if ($object == 'pagetitle') {
// Deprecated, <title> will be added automatically
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'contentframe') {
if (empty($attrib['id'])) {
$attrib['id'] = 'rcm' . $this->env['task'] . 'frame';
}
// parse variables
if (preg_match('/^(config|env):([a-z0-9_]+)$/i', $attrib['src'], $matches)) {
$attrib['src'] = $this->parse_variable($matches[1], $matches[2]);
}
$content = $this->frame($attrib, true);
}
else if ($object == 'meta' || $object == 'links') {
if ($object == 'meta') {
$source = 'meta_tags';
$tag = 'meta';
$key = 'name';
$param = 'content';
}
else {
$source = 'link_tags';
$tag = 'link';
$key = 'rel';
$param = 'href';
}
foreach ($this->$source as $name => $vars) {
// $vars can be in many forms:
// - string
// - array('key' => 'val')
// - array(string, string)
// - array(array(), string)
// - array(array('key' => 'val'), array('key' => 'val'))
// normalise this for processing by checking for string array keys
$vars = is_array($vars) ? (count(array_filter(array_keys($vars), 'is_string')) > 0 ? array($vars) : $vars) : array($vars);
foreach ($vars as $args) {
// skip unset headers e.g. when extending a skin and removing a header defined in the parent
if ($args === false) {
continue;
}
$args = is_array($args) ? $args : array($param => $args);
// special handling for favicon
if ($object == 'links' && $name == 'shortcut icon' && empty($args[$param])) {
if ($href = $this->get_template_logo('favicon')) {
$args[$param] = $href;
}
else if ($href = $this->config->get('favicon', '/images/favicon.ico')) {
$args[$param] = $href;
}
}
$content .= html::tag($tag, array($key => $name, 'nl' => true) + $args);
}
}
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
if (strlen($hook['content']) && !empty($external)) {
$object_id = uniqid('TEMPLOBJECT:', true);
$this->objects[$object_id] = $hook['content'];
$hook['content'] = $object_id;
}
return $hook['content'];
// return <link> element
case 'link':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
unset($attrib['condition']);
return html::tag('link', $attrib);
// return code for a specified eval expression
case 'exp':
return html::quote($this->eval_expression($attrib['expression']));
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$value = $this->parse_variable($var[0], $var[1]);
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
case 'form':
return $this->form_tag($attrib);
}
return '';
}
/**
* Prepares template object attributes
*
* @param array &$attribs Attributes
*/
protected function prepare_object_attribs(&$attribs)
{
foreach ($attribs as $key => &$value) {
if (strpos($key, 'data-label-') === 0) {
// Localize data-label-* attributes
$value = $this->app->gettext($value);
}
elseif ($key[0] == ':') {
// Evaluate attributes with expressions and remove special character from attribute name
$attribs[substr($key, 1)] = $this->eval_expression($value);
unset($attribs[$key]);
}
}
}
/**
* Include a specific file and return it's contents
*
* @param string $file File path
*
* @return string Contents of the processed file
*/
protected function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Put objects' content back into template output
*/
protected function postrender($output)
{
// insert objects' contents
foreach ($this->objects as $key => $val) {
$output = str_replace($key, $val, $output, $count);
if ($count) {
$this->objects[$key] = null;
}
}
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
return $output;
}
/**
* Create and register a button
*
* @param array $attrib Named button attributes
*
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $s_button_count = 100;
static $disabled_actions = null;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (empty($attrib['command']) && empty($attrib['name']) && empty($attrib['href'])) {
return '';
}
$command = !empty($attrib['command']) ? $attrib['command'] : null;
$action = $command ?: (!empty($attrib['name']) ? $attrib['name'] : null);
if (!empty($attrib['task'])) {
$command = $attrib['task'] . '.' . $command;
$element = $attrib['task'] . '.' . $action;
}
else {
$element = (!empty($this->env['task']) ? $this->env['task'] . '.' : '') . $action;
}
if ($disabled_actions === null) {
$disabled_actions = (array) $this->config->get('disabled_actions');
}
// remove buttons for disabled actions
if (in_array($element, $disabled_actions) || in_array($action, $disabled_actions)) {
return '';
}
// try to find out the button type
if (!empty($attrib['type'])) {
$attrib['type'] = strtolower($attrib['type']);
if (strpos($attrib['type'], '-menuitem')) {
$attrib['type'] = substr($attrib['type'], 0, -9);
$menuitem = true;
}
}
else if (!empty($attrib['image']) || !empty($attrib['imagepas']) || !empty($attrib['imageact'])) {
$attrib['type'] = 'image';
}
else {
$attrib['type'] = 'button';
}
if (empty($attrib['image'])) {
if (!empty($attrib['imagepas'])) {
$attrib['image'] = $attrib['imagepas'];
}
else if (!empty($attrib['imageact'])) {
$attrib['image'] = $attrib['imageact'];
}
}
if (empty($attrib['id'])) {
// ensure auto generated IDs are unique between main window and content frame
// Elastic skin duplicates buttons between the two on smaller screens (#7618)
$prefix = ($this->framed || !empty($this->env['framed'])) ? 'frm' : '';
$attrib['id'] = sprintf('rcmbtn%s%d', $prefix, $s_button_count++);
}
// get localized text for labels and titles
$domain = !empty($attrib['domain']) ? $attrib['domain'] : null;
if (!empty($attrib['title'])) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $domain));
}
if (!empty($attrib['label'])) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $domain));
}
if (!empty($attrib['alt'])) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $domain));
}
// set accessibility attributes
if (empty($attrib['role'])) {
$attrib['role'] = 'button';
}
if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
if (array_key_exists('tabindex', $attrib)) {
$attrib['data-tabindex'] = $attrib['tabindex'];
}
$attrib['tabindex'] = '-1'; // disable button by default
$attrib['aria-disabled'] = 'true';
}
// set title to alt attribute for IE browsers
if ($this->browser->ie && empty($attrib['title']) && !empty($attrib['alt'])) {
$attrib['title'] = $attrib['alt'];
}
// add empty alt attribute for XHTML compatibility
if (!isset($attrib['alt'])) {
$attrib['alt'] = '';
}
// register button in the system
if (!empty($attrib['command'])) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
self::JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
!empty($attrib['imageact']) ? $this->abs_url($attrib['imageact']) : (!empty($attrib['classact']) ? $attrib['classact'] : ''),
!empty($attrib['imagesel']) ? $this->abs_url($attrib['imagesel']) : (!empty($attrib['classsel']) ? $attrib['classsel'] : ''),
!empty($attrib['imageover']) ? $this->abs_url($attrib['imageover']) : ''
));
// make valid href to specific buttons
if (in_array($attrib['command'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('task' => $attrib['command']));
$attrib['onclick'] = sprintf("return %s.command('switch-task','%s',this,event)", self::JS_OBJECT_NAME, $attrib['command']);
}
else if (!empty($attrib['task']) && in_array($attrib['task'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task']));
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command']));
}
else if (($attrib['command'] == 'permaurl' || $attrib['command'] == 'extwin') && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (empty($attrib['href'])) {
$attrib['href'] = '#';
}
if (!empty($attrib['task'])) {
if (!empty($attrib['classact'])) {
$attrib['class'] = $attrib['classact'];
}
}
else if ($command && empty($attrib['onclick'])) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
self::JS_OBJECT_NAME,
$command,
!empty($attrib['prop']) ? $attrib['prop'] : ''
);
}
$out = '';
$btn_content = null;
$link_attrib = array();
// generate image tag
if ($attrib['type'] == 'image') {
$attrib_str = html::attrib_string(
$attrib,
array(
'style', 'class', 'id', 'width', 'height', 'border', 'hspace',
'vspace', 'align', 'alt', 'tabindex', 'title'
)
);
$btn_content = sprintf('<img src="%s"%s />', $this->abs_url($attrib['image']), $attrib_str);
if (!empty($attrib['label'])) {
$btn_content .= ' '.$attrib['label'];
}
$link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'target');
}
else if ($attrib['type'] == 'link') {
$btn_content = isset($attrib['content']) ? $attrib['content'] : (!empty($attrib['label']) ? $attrib['label'] : $attrib['command']);
$link_attrib = array_merge(html::$common_attrib, array('href', 'onclick', 'tabindex', 'target', 'rel'));
if (!empty($attrib['innerclass'])) {
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if (!empty($attrib['label'])) {
$attrib['value'] = $attrib['label'];
}
if (!empty($attrib['command'])) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
else {
if (!empty($attrib['label'])) {
$attrib['value'] = $attrib['label'];
}
if (!empty($attrib['command'])) {
$attrib['disabled'] = 'disabled';
}
$content = isset($attrib['content']) ? $attrib['content'] : $attrib['label'];
$out = html::tag('button', $attrib, $content, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, $link_attrib);
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
if (!empty($attrib['wrapper'])) {
$out = html::tag($attrib['wrapper'], null, $out);
}
if (!empty($menuitem)) {
$class = !empty($attrib['menuitem-class']) ? ' class="' . $attrib['menuitem-class'] . '"' : '';
$out = '<li role="menuitem"' . $class . '>' . $out . '</li>';
}
return $out;
}
/**
* Link an external script file
*
* @param string $file File URL
* @param string $position Target position [head|head_bottom|foot]
*/
public function include_script($file, $position = 'head', $add_path = true)
{
if ($add_path && !preg_match('|^https?://|i', $file) && $file[0] != '/') {
$file = $this->file_mod($this->scripts_path . $file);
}
if (!isset($this->script_files[$position]) || !is_array($this->script_files[$position])) {
$this->script_files[$position] = array();
}
if (!in_array($file, $this->script_files[$position])) {
$this->script_files[$position][] = $file;
}
}
/**
* Add inline javascript code
*
* @param string $script JS code snippet
* @param string $position Target position [head|head_top|foot|docready]
*/
public function add_script($script, $position = 'head')
{
if (!isset($this->scripts[$position])) {
$this->scripts[$position] = rtrim($script);
}
else {
$this->scripts[$position] .= "\n" . rtrim($script);
}
}
/**
* Link an external css file
*
* @param string $file File URL
*/
public function include_css($file)
{
$this->css_files[] = $file;
}
/**
* Add HTML code to the page header
*
* @param string $str HTML code
*/
public function add_header($str)
{
$this->header .= "\n" . $str;
}
/**
* Add HTML code to the page footer
* To be added right befor </body>
*
* @param string $str HTML code
*/
public function add_footer($str)
{
$this->footer .= "\n" . $str;
}
/**
* Process template and write to stdOut
*
* @param string $output HTML output
*/
protected function _write($output = '')
{
$output = trim($output);
if (empty($output)) {
$output = html::doctype('html5') . "\n" . $this->default_template;
$is_empty = true;
}
$merge_script_files = function($output, $script) {
return $output . html::script($script);
};
$merge_scripts = function($output, $script) {
return $output . html::script(array(), $script);
};
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script("\$(function() {\n" . $this->scripts['docready'] . "\n});", 'foot');
}
$page_header = '';
$page_footer = '';
$meta = '';
// declare page language
if (!empty($_SESSION['language'])) {
$lang = substr($_SESSION['language'], 0, 2);
$output = preg_replace('/<html/', '<html lang="' . html::quote($lang) . '"', $output, 1);
if (!headers_sent()) {
$this->header('Content-Language: ' . $lang);
}
}
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
$this->header('Content-Type: text/html; charset=' . $this->charset);
}
$meta .= html::tag('meta', array(
'http-equiv' => 'content-type',
'content' => "text/html; charset={$this->charset}",
'nl' => true
));
}
// include page title (after charset specification)
$meta .= '<title>' . html::quote($this->get_pagetitle()) . "</title>\n";
$output = preg_replace('/(<head[^>]*>)\n*/i', "\\1\n{$meta}", $output, 1, $count);
if (!$count) {
$page_header .= $meta;
}
// include scripts into header/footer
if (!empty($this->script_files['head'])) {
$page_header .= array_reduce((array) $this->script_files['head'], $merge_script_files);
}
$page_header .= array_reduce(array($this->scripts['head_top'] . $this->scripts['head']), $merge_scripts);
$page_header .= $this->header . "\n";
if (!empty($this->script_files['head_bottom'])) {
$page_header .= array_reduce((array) $this->script_files['head_bottom'], $merge_script_files);
}
if (!empty($this->script_files['foot'])) {
$page_footer .= array_reduce((array) $this->script_files['foot'], $merge_script_files);
}
$page_footer .= $this->footer . "\n";
if (!empty($this->scripts['foot'])) {
$page_footer .= array_reduce((array) $this->scripts['foot'], $merge_scripts);
}
// find page header
if ($hpos = stripos($output, '</head>')) {
$page_header .= "\n";
}
else {
if (!is_numeric($hpos)) {
$hpos = stripos($output, '<body');
}
if (!is_numeric($hpos) && ($hpos = stripos($output, '<html'))) {
while ($output[$hpos] != '>') {
$hpos++;
}
$hpos++;
}
$page_header = "<head>\n$page_header\n</head>\n";
}
// add page hader
if ($hpos) {
$output = substr_replace($output, $page_header, $hpos, 0);
}
else {
$output = $page_header . $output;
}
// add page footer
if (($fpos = strripos($output, '</body>')) || ($fpos = strripos($output, '</html>'))) {
// for Elastic: put footer content before "footer scripts"
while (($npos = strripos($output, "\n", -strlen($output) + $fpos - 1))
&& $npos != $fpos
&& ($chunk = substr($output, $npos, $fpos - $npos)) !== ''
&& (trim($chunk) === '' || preg_match('/\s*<script[^>]+><\/script>\s*/', $chunk))
) {
$fpos = $npos;
}
$output = substr_replace($output, $page_footer."\n", $fpos, 0);
}
else {
$output .= "\n".$page_footer;
}
// add css files in head, before scripts, for speed up with parallel downloads
if (!empty($this->css_files) && empty($is_empty)
&& (($pos = stripos($output, '<script ')) || ($pos = stripos($output, '</head>')))
) {
$css = '';
foreach ($this->css_files as $file) {
$is_less = substr_compare($file, '.less', -5, 5, true) === 0;
$css .= html::tag('link', array(
'rel' => $is_less ? 'stylesheet/less' : 'stylesheet',
'type' => 'text/css',
'href' => $file,
'nl' => true,
));
}
$output = substr_replace($output, $css, $pos, 0);
}
$output = $this->parse_with_globals($this->fix_paths($output));
if ($this->assets_path) {
$output = $this->fix_assets_paths($output);
}
$output = $this->postrender($output);
// trigger hook with final HTML content to be sent
$hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
if (!$hook['abort']) {
if ($this->charset != RCUBE_CHARSET) {
echo rcube_charset::convert($hook['content'], RCUBE_CHARSET, $this->charset);
}
else {
echo $hook['content'];
}
}
}
/**
* Returns iframe object, registers some related env variables
*
* @param array $attrib HTML attributes
* @param boolean $is_contentframe Register this iframe as the 'contentframe' gui object
*
* @return string IFRAME element
*/
public function frame($attrib, $is_contentframe = false)
{
static $idcount = 0;
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmframe' . ++$idcount;
}
$attrib['name'] = $attrib['id'];
$attrib['src'] = !empty($attrib['src']) ? $this->abs_url($attrib['src'], true) : 'about:blank';
// register as 'contentframe' object
if ($is_contentframe || !empty($attrib['contentframe'])) {
$this->set_env('contentframe', !empty($attrib['contentframe']) ? $attrib['contentframe'] : $attrib['name']);
}
return html::iframe($attrib);
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array $attrib Named tag parameters
* @param string $content HTML content of the form
*
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
$hidden = '';
if (!empty($this->env['extwin'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_extwin', 'value' => '1'));
$hidden = $hiddenfield->show();
}
else if ($this->framed || !empty($this->env['framed'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
$hidden = $hiddenfield->show();
}
if (!$content) {
$attrib['noclose'] = true;
}
return html::tag('form',
$attrib + array('action' => $this->app->comm_path, 'method' => "get"),
$hidden . $content,
array('id','class','style','name','method','action','enctype','onsubmit')
);
}
/**
* Build a form tag with a unique request token
*
* @param array $attrib Named tag parameters including 'action' and 'task' values
* which will be put into hidden fields
* @param string $content Form content
*
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if (!empty($attrib['task'])) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if (!empty($attrib['action'])) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
// we already have a <form> tag
if (!empty($attrib['form'])) {
if ($this->framed || !empty($this->env['framed'])) {
$hidden->add(array('name' => '_framed', 'value' => '1'));
}
return $hidden->show() . $content;
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array $attrib Named tag parameters (currently not used)
*
* @return string HTML code for the gui object
*/
public function current_username($attrib)
{
static $username;
// alread fetched
if (!empty($username)) {
return $username;
}
// Current username is an e-mail address
if (strpos($_SESSION['username'], '@')) {
$username = $_SESSION['username'];
}
// get e-mail address from default identity
else if ($sql_arr = $this->app->user->get_identity()) {
$username = $sql_arr['email'];
}
else {
$username = $this->app->user->get_username();
}
$username = rcube_utils::idn_to_utf8($username);
return html::quote($username);
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
protected function login_form($attrib)
{
$default_host = $this->config->get('default_host');
$autocomplete = (int) $this->config->get('login_autocomplete');
$username_filter = $this->config->get('login_username_filter');
$_SESSION['temp'] = true;
// save original url
$url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING'])) {
$url = $_SERVER['QUERY_STRING'];
}
// Disable autocapitalization on iPad/iPhone (#1488609)
$attrib['autocapitalize'] = 'off';
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
// set atocomplete attribute
$user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');
if ($username_filter && strtolower($username_filter) == 'email') {
$user_attrib['type'] = 'email';
}
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
$input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required')
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'required' => 'required')
+ $attrib + $pass_attrib);
$input_host = null;
$hide_host = false;
if (is_array($default_host) && count($default_host) > 1) {
$input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost', 'class' => 'custom-select'));
foreach ($default_host as $key => $value) {
if (!is_array($value)) {
$input_host->add($value, (is_numeric($key) ? $value : $key));
}
else {
$input_host = null;
break;
}
}
}
else if (is_array($default_host) && ($host = key($default_host)) !== null) {
$hide_host = true;
$input_host = new html_hiddenfield(array(
'name' => '_host', 'id' => 'rcmloginhost', 'value' => is_numeric($host) ? $default_host[$host] : $host) + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost', 'class' => 'form-control')
+ $attrib + $host_attrib);
}
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', html::quote($this->app->gettext('username'))));
$table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
$table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
$table->add('input', $input_pass->show());
// add host selection row
if (is_object($input_host) && !$hide_host) {
$table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
$table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
if ($hide_host) {
$out .= $input_host->show();
}
if (rcube_utils::get_boolean($attrib['submit'])) {
$button_attr = array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit');
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// add oauth login button
if ($this->config->get('oauth_auth_uri') && $this->config->get('oauth_provider')) {
$link_attr = array('href' => $this->app->url(array('action' => 'oauth')), 'id' => 'rcmloginoauth', 'class' => 'button oauth ' . $this->config->get('oauth_provider'));
$out .= html::p('oauthlogin', html::a($link_attr, $this->app->gettext(array('name' => 'oauthlogin', 'vars' => array('provider' => $this->config->get('oauth_provider_name', 'OAuth'))))));
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
}
// include script for timezone detection
$this->include_script('jstz.min.js');
return $out;
}
/**
* GUI object 'preloader'
* Loads javascript code for images preloading
*
* @param array $attrib Named parameters
* @return void
*/
protected function preloader($attrib)
{
$images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
$images = array_map(array($this, 'abs_url'), $images);
$images = array_map(array($this, 'asset_url'), $images);
if (empty($images) || $_REQUEST['_task'] == 'logout') {
return;
}
$this->add_script('var images = ' . self::json_serialize($images, $this->devel_mode) .';
for (var i=0; i<images.length; i++) {
img = new Image();
img.src = images[i];
}', 'docready');
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
$attrib['class'] = trim((!empty($attrib['class']) ? $attrib['class'] : '') . ' no-bs');
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if (isset($attrib['type']) && $attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
if (empty($attrib['placeholder'])) {
$attrib['placeholder'] = $this->app->gettext('searchplaceholder');
}
$label = html::label(array('for' => $attrib['id'], 'class' => 'voice'), rcube::Q($this->app->gettext('arialabelsearchterms')));
$input_q = new html_inputfield($attrib);
$out = $label . $input_q->show();
$name = 'qsearchbox';
// Support for multiple searchforms on the same page
if (isset($attrib['gui-object']) && $attrib['gui-object'] !== false && $attrib['gui-object'] !== 'false') {
$name = $attrib['gui-object'];
}
$this->add_gui_object($name, $attrib['id']);
// add form tag around text field
if (empty($attrib['form']) && empty($attrib['no-form'])) {
$out = $this->form_tag(array(
'name' => !empty($attrib['form-name']) ? $attrib['form-name'] : 'rcmqsearchform',
'onsubmit' => sprintf(
"%s.command('%s'); return false",
self::JS_OBJECT_NAME,
!empty($attrib['command']) ? $attrib['command'] : 'search'
),
// 'style' => "display:inline"
), $out);
}
if (!empty($attrib['wrapper'])) {
$options_button = '';
$ariatag = !empty($attrib['ariatag']) ? $attrib['ariatag'] : 'h2';
$domain = !empty($attrib['label-domain']) ? $attrib['label-domain'] : null;
$options = !empty($attrib['options']) ? $attrib['options'] : null;
$header_label = $this->app->gettext('arialabel' . $attrib['label'], $domain);
$header_attrs = array(
'id' => 'aria-label-' . $attrib['label'],
'class' => 'voice'
);
$header = html::tag($ariatag, $header_attrs, rcube::Q($header_label));
if ($attrib['options']) {
$options_button = $this->button(array(
'type' => 'link',
'href' => '#search-filter',
'class' => 'button options',
'label' => 'options',
'title' => 'options',
'tabindex' => '0',
'innerclass' => 'inner',
'data-target' => $options
));
}
$search_button = $this->button(array(
'type' => 'link',
'href' => '#search',
'class' => 'button search',
'label' => $attrib['buttontitle'],
'title' => $attrib['buttontitle'],
'tabindex' => '0',
'innerclass' => 'inner',
));
$reset_button = $this->button(array(
'type' => 'link',
'command' => !empty($attrib['reset-command']) ? $attrib['reset-command'] : 'reset-search',
'class' => 'button reset',
'label' => 'resetsearch',
'title' => 'resetsearch',
'tabindex' => '0',
'innerclass' => 'inner',
));
$out = html::div(array(
'role' => 'search',
'aria-labelledby' => !empty($attrib['label']) ? 'aria-label-' . $attrib['label'] : null,
'class' => $attrib['wrapper'],
), "$header$out\n$reset_button\n$options_button\n$search_button");
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
protected function message_container($attrib)
{
if (isset($attrib['id']) === false) {
$attrib['id'] = 'rcmMessageContainer';
}
$this->add_gui_object('message', $attrib['id']);
return html::div($attrib, '');
}
/**
* GUI object 'charsetselector'
*
* @param array $attrib Named parameters for the select tag
*
* @return string HTML code for the gui object
*/
public function charset_selector($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_charset');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'name', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
$charsets = array(
'UTF-8' => 'UTF-8 ('.$this->app->gettext('unicode').')',
'US-ASCII' => 'ASCII ('.$this->app->gettext('english').')',
'ISO-8859-1' => 'ISO-8859-1 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-2' => 'ISO-8859-2 ('.$this->app->gettext('easterneuropean').')',
'ISO-8859-4' => 'ISO-8859-4 ('.$this->app->gettext('baltic').')',
'ISO-8859-5' => 'ISO-8859-5 ('.$this->app->gettext('cyrillic').')',
'ISO-8859-6' => 'ISO-8859-6 ('.$this->app->gettext('arabic').')',
'ISO-8859-7' => 'ISO-8859-7 ('.$this->app->gettext('greek').')',
'ISO-8859-8' => 'ISO-8859-8 ('.$this->app->gettext('hebrew').')',
'ISO-8859-9' => 'ISO-8859-9 ('.$this->app->gettext('turkish').')',
'ISO-8859-10' => 'ISO-8859-10 ('.$this->app->gettext('nordic').')',
'ISO-8859-11' => 'ISO-8859-11 ('.$this->app->gettext('thai').')',
'ISO-8859-13' => 'ISO-8859-13 ('.$this->app->gettext('baltic').')',
'ISO-8859-14' => 'ISO-8859-14 ('.$this->app->gettext('celtic').')',
'ISO-8859-15' => 'ISO-8859-15 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-16' => 'ISO-8859-16 ('.$this->app->gettext('southeasterneuropean').')',
'WINDOWS-1250' => 'Windows-1250 ('.$this->app->gettext('easterneuropean').')',
'WINDOWS-1251' => 'Windows-1251 ('.$this->app->gettext('cyrillic').')',
'WINDOWS-1252' => 'Windows-1252 ('.$this->app->gettext('westerneuropean').')',
'WINDOWS-1253' => 'Windows-1253 ('.$this->app->gettext('greek').')',
'WINDOWS-1254' => 'Windows-1254 ('.$this->app->gettext('turkish').')',
'WINDOWS-1255' => 'Windows-1255 ('.$this->app->gettext('hebrew').')',
'WINDOWS-1256' => 'Windows-1256 ('.$this->app->gettext('arabic').')',
'WINDOWS-1257' => 'Windows-1257 ('.$this->app->gettext('baltic').')',
'WINDOWS-1258' => 'Windows-1258 ('.$this->app->gettext('vietnamese').')',
'ISO-2022-JP' => 'ISO-2022-JP ('.$this->app->gettext('japanese').')',
'ISO-2022-KR' => 'ISO-2022-KR ('.$this->app->gettext('korean').')',
'ISO-2022-CN' => 'ISO-2022-CN ('.$this->app->gettext('chinese').')',
'EUC-JP' => 'EUC-JP ('.$this->app->gettext('japanese').')',
'EUC-KR' => 'EUC-KR ('.$this->app->gettext('korean').')',
'EUC-CN' => 'EUC-CN ('.$this->app->gettext('chinese').')',
'BIG5' => 'BIG5 ('.$this->app->gettext('chinese').')',
'GB2312' => 'GB2312 ('.$this->app->gettext('chinese').')',
'KOI8-R' => 'KOI8-R ('.$this->app->gettext('cyrillic').')',
);
if ($post = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST)) {
$set = $post;
}
else if (!empty($attrib['selected'])) {
$set = $attrib['selected'];
}
else {
$set = $this->get_charset();
}
$set = strtoupper($set);
if (!isset($charsets[$set]) && preg_match('/^[A-Z0-9-]+$/', $set)) {
$charsets[$set] = $set;
}
$select = new html_select($field_attrib);
$select->add(array_values($charsets), array_keys($charsets));
return $select->show($set);
}
/**
* Include content from config/about.<LANG>.html if available
*/
protected function about_content($attrib)
{
$content = '';
$filenames = array(
'about.' . $_SESSION['language'] . '.html',
'about.' . substr($_SESSION['language'], 0, 2) . '.html',
'about.html',
);
foreach ($filenames as $file) {
$fn = RCUBE_CONFIG_DIR . $file;
if (is_readable($fn)) {
$content = file_get_contents($fn);
$content = $this->parse_conditions($content);
$content = $this->parse_xml($content);
break;
}
}
return $content;
}
/**
* Get logo URL for current template based on skin_logo config option
*
* @param string $type Type of the logo to check for (e.g. 'print' or 'small')
* default is null (no special type)
* @param string $match (optional) 'all' = type, template or wildcard, 'template' = type or template
* Note: when type is specified matches are limited to type only unless $match is defined
*
* @return string image URL
*/
protected function get_template_logo($type = null, $match = null)
{
$template_logo = null;
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
$template_names = array(
$this->skin_name . ':' . $this->template_name . '[' . $type . ']',
$this->skin_name . ':' . $this->template_name,
$this->skin_name . ':*[' . $type . ']',
$this->skin_name . ':[' . $type . ']',
$this->skin_name . ':*',
'*:' . $this->template_name . '[' . $type . ']',
'*:' . $this->template_name,
'*:*[' . $type . ']',
'*:[' . $type . ']',
$this->template_name . '[' . $type . ']',
$this->template_name,
'*[' . $type . ']',
'[' . $type . ']',
'*',
);
if (empty($type)) {
// If no type provided then remove those options from the list
$template_names = preg_grep("/\]$/", $template_names, PREG_GREP_INVERT);
}
elseif ($match === null) {
// Type specified with no special matching requirements so remove all none type specific options from the list
$template_names = preg_grep("/\]$/", $template_names);
}
if ($match == 'template') {
// Match only specific type or template name
$template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
}
foreach ($template_names as $key) {
if (isset($logo[$key])) {
$template_logo = $logo[$key];
break;
}
}
}
else {
$template_logo = $logo;
}
}
return $template_logo;
}
}
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index 06ea296ea..2202b4b86 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -1,1806 +1,1808 @@
<?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: |
| Framework base class providing core functions and holding |
| instances of all 'global' objects like db- and storage-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Base class of the Roundcube Framework
* implemented as singleton
*
* @package Framework
* @subpackage Core
*/
class rcube
{
// Init options
const INIT_WITH_DB = 1;
const INIT_WITH_PLUGINS = 2;
// Request status
const REQUEST_VALID = 0;
const REQUEST_ERROR_URL = 1;
const REQUEST_ERROR_TOKEN = 2;
const DEBUG_LINE_LENGTH = 4096;
/** @var rcube_config Stores instance of rcube_config */
public $config;
/** @var rcube_db Instance of database class */
public $db;
/** @var Memcache Instance of Memcache class */
public $memcache;
/** @var Memcached Instance of Memcached class */
public $memcached;
/** @var Redis Instance of Redis class */
public $redis;
/** @var rcube_session Instance of rcube_session class */
public $session;
/** @var rcube_smtp Instance of rcube_smtp class */
public $smtp;
/** @var rcube_storage Instance of rcube_storage class */
public $storage;
/** @var rcube_output Instance of rcube_output class */
public $output;
/** @var rcube_plugin_api Instance of rcube_plugin_api */
public $plugins;
/** @var rcube_user Instance of rcube_user class */
public $user;
/** @var int Request status */
public $request_status = 0;
/** @var array Localization */
protected $texts;
/** @var rcube_cache[] Initialized cache objects */
protected $caches = array();
/** @var array Registered shutdown functions */
protected $shutdown_functions = array();
/** @var rcube Singleton instance of rcube */
static protected $instance;
/**
* This implements the 'singleton' design pattern
*
* @param int $mode Options to initialize with this instance. See rcube::INIT_WITH_* constants
* @param string $env Environment name to run (e.g. live, dev, test)
*
* @return rcube The one and only instance
*/
static function get_instance($mode = 0, $env = '')
{
if (!self::$instance) {
self::$instance = new rcube($env);
self::$instance->init($mode);
}
return self::$instance;
}
/**
* Private constructor
*
* @param string $env Environment name to run (e.g. live, dev, test)
*/
protected function __construct($env = '')
{
// load configuration
$this->config = new rcube_config($env);
$this->plugins = new rcube_dummy_plugin_api;
register_shutdown_function(array($this, 'shutdown'));
}
/**
* Initial startup function
*
* @param int $mode Options to initialize with this instance. See rcube::INIT_WITH_* constants
*/
protected function init($mode = 0)
{
// initialize syslog
if ($this->config->get('log_driver') == 'syslog') {
$syslog_id = $this->config->get('syslog_id', 'roundcube');
$syslog_facility = $this->config->get('syslog_facility', LOG_USER);
openlog($syslog_id, LOG_ODELAY, $syslog_facility);
}
// connect to database
if ($mode & self::INIT_WITH_DB) {
$this->get_dbh();
}
// create plugin API and load plugins
if ($mode & self::INIT_WITH_PLUGINS) {
$this->plugins = rcube_plugin_api::get_instance();
}
}
/**
* Get the current database connection
*
* @return rcube_db Database object
*/
public function get_dbh()
{
if (!$this->db) {
$this->db = rcube_db::factory(
$this->config->get('db_dsnw'),
$this->config->get('db_dsnr'),
$this->config->get('db_persistent')
);
$this->db->set_debug((bool)$this->config->get('sql_debug'));
}
return $this->db;
}
/**
* Get global handle for memcache access
*
* @return object Memcache
*/
public function get_memcache()
{
if (!isset($this->memcache)) {
$this->memcache = rcube_cache_memcache::engine();
}
return $this->memcache;
}
/**
* Get global handle for memcached access
*
* @return object Memcached
*/
public function get_memcached()
{
if (!isset($this->memcached)) {
$this->memcached = rcube_cache_memcached::engine();
}
return $this->memcached;
}
/**
* Get global handle for redis access
*
* @return object Redis
*/
public function get_redis()
{
if (!isset($this->redis)) {
$this->redis = rcube_cache_redis::engine();
}
return $this->redis;
}
/**
* Initialize and get user cache object
*
* @param string $name Cache identifier
* @param string $type Cache type ('db', 'apc', 'memcache', 'redis')
* @param string $ttl Expiration time for cache items
* @param bool $packed Enables/disables data serialization
* @param bool $indexed Use indexed cache
*
* @return rcube_cache User cache object
*/
public function get_cache($name, $type = 'db', $ttl = 0, $packed = true, $indexed = false)
{
if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) {
$this->caches[$name] = rcube_cache::factory($type, $userid, $name, $ttl, $packed, $indexed);
}
return $this->caches[$name];
}
/**
* Initialize and get shared cache object
*
* @param string $name Cache identifier
* @param bool $packed Enables/disables data serialization
*
* @return rcube_cache Shared cache object
*/
public function get_cache_shared($name, $packed = true)
{
$shared_name = "shared_$name";
if (!array_key_exists($shared_name, $this->caches)) {
$opt = strtolower($name) . '_cache';
$type = $this->config->get($opt);
$ttl = $this->config->get($opt . '_ttl');
if (!$type) {
// cache is disabled
return $this->caches[$shared_name] = null;
}
if ($ttl === null) {
$ttl = $this->config->get('shared_cache_ttl', '10d');
}
$this->caches[$shared_name] = rcube_cache::factory($type, null, $name, $ttl, $packed);
}
return $this->caches[$shared_name];
}
/**
* Create SMTP object and connect to server
*
* @param boolean $connect True if connection should be established
*/
public function smtp_init($connect = false)
{
$this->smtp = new rcube_smtp();
if ($connect) {
$this->smtp->connect();
}
}
/**
* Initialize and get storage object
*
* @return rcube_storage Storage object
*/
public function get_storage()
{
// already initialized
if (!is_object($this->storage)) {
$this->storage_init();
}
return $this->storage;
}
/**
* Initialize storage object
*/
public function storage_init()
{
// already initialized
if (is_object($this->storage)) {
return;
}
$driver = $this->config->get('storage_driver', 'imap');
$driver_class = "rcube_{$driver}";
if (!class_exists($driver_class)) {
self::raise_error(array(
'code' => 700, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Storage driver class ($driver) not found!"),
true, true);
}
// Initialize storage object
$this->storage = new $driver_class;
// for backward compat. (deprecated, will be removed)
$this->imap = $this->storage;
// set class options
$options = array(
'auth_type' => $this->config->get("{$driver}_auth_type", 'check'),
'auth_cid' => $this->config->get("{$driver}_auth_cid"),
'auth_pw' => $this->config->get("{$driver}_auth_pw"),
'debug' => (bool) $this->config->get("{$driver}_debug"),
'force_caps' => (bool) $this->config->get("{$driver}_force_caps"),
'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
'socket_options' => $this->config->get("{$driver}_conn_options"),
'timeout' => (int) $this->config->get("{$driver}_timeout"),
'skip_deleted' => (bool) $this->config->get('skip_deleted'),
'driver' => $driver,
);
if (!empty($_SESSION['storage_host'])) {
$options['language'] = $_SESSION['language'];
$options['host'] = $_SESSION['storage_host'];
$options['user'] = $_SESSION['username'];
$options['port'] = $_SESSION['storage_port'];
$options['ssl'] = $_SESSION['storage_ssl'];
$options['password'] = $this->decrypt($_SESSION['password']);
$_SESSION[$driver.'_host'] = $_SESSION['storage_host'];
}
$options = $this->plugins->exec_hook("storage_init", $options);
// for backward compat. (deprecated, to be removed)
$options = $this->plugins->exec_hook("imap_init", $options);
$this->storage->set_options($options);
$this->set_storage_prop();
// subscribe to 'storage_connected' hook for session logging
if ($this->config->get('imap_log_session', false)) {
$this->plugins->register_hook('storage_connected', array($this, 'storage_log_session'));
}
}
/**
* Set storage parameters.
*/
protected function set_storage_prop()
{
$storage = $this->get_storage();
// set pagesize from config
$pagesize = $this->config->get('mail_pagesize');
if (!$pagesize) {
$pagesize = $this->config->get('pagesize', 50);
}
$storage->set_pagesize($pagesize);
$storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
// enable caching of mail data
$driver = $this->config->get('storage_driver', 'imap');
$storage_cache = $this->config->get("{$driver}_cache");
$messages_cache = $this->config->get('messages_cache');
// for backward compatybility
if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
$storage_cache = 'db';
$messages_cache = true;
}
if ($storage_cache) {
$storage->set_caching($storage_cache);
}
if ($messages_cache) {
$storage->set_messages_caching(true);
}
}
/**
* Set special folders type association.
* This must be done AFTER connecting to the server!
*/
protected function set_special_folders()
{
$storage = $this->get_storage();
$folders = $storage->get_special_folders(true);
$prefs = array();
// check SPECIAL-USE flags on IMAP folders
foreach ($folders as $type => $folder) {
$idx = $type . '_mbox';
if ($folder !== $this->config->get($idx)) {
$prefs[$idx] = $folder;
}
}
// Some special folders differ, update user preferences
if (!empty($prefs) && $this->user) {
$this->user->save_prefs($prefs);
}
// create default folders (on login)
if ($this->config->get('create_default_folders')) {
$storage->create_default_folders();
}
}
/**
* Callback for IMAP connection events to log session identifiers
*
* @param array $args Callback arguments
*/
public function storage_log_session($args)
{
if (!empty($args['session']) && session_id()) {
$this->write_log('imap_session', $args['session']);
}
}
/**
* Create session object and start the session.
*/
public function session_init()
{
// Ignore in CLI mode or when session started (Installer?)
if (empty($_SERVER['REMOTE_ADDR']) || session_id()) {
return;
}
$storage = $this->config->get('session_storage', 'db');
$sess_name = $this->config->get('session_name');
$sess_domain = $this->config->get('session_domain');
$sess_path = $this->config->get('session_path');
$sess_samesite = $this->config->get('session_samesite');
$lifetime = $this->config->get('session_lifetime', 0) * 60;
$is_secure = $this->config->get('use_https') || rcube_utils::https_check();
// set session domain
if ($sess_domain) {
ini_set('session.cookie_domain', $sess_domain);
}
// set session path
if ($sess_path) {
ini_set('session.cookie_path', $sess_path);
}
// set session samesite attribute
// requires PHP >= 7.3.0, see https://wiki.php.net/rfc/same-site-cookie for more info
if (version_compare(PHP_VERSION, '7.3.0', '>=') && $sess_samesite) {
ini_set('session.cookie_samesite', $sess_samesite);
}
// set session garbage collecting time according to session_lifetime
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
// set session cookie lifetime so it never expires (#5961)
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_secure', $is_secure);
ini_set('session.name', $sess_name ?: 'roundcube_sessid');
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
// Make sure session garbage collector is enabled when using custom handlers (#6560)
// Note: Use session.gc_divisor to control accuracy
if ($storage != 'php' && !ini_get('session.gc_probability')) {
ini_set('session.gc_probability', 1);
}
// Start the session
$this->session = rcube_session::factory($this->config);
$this->session->register_gc_handler(array($this, 'gc'));
$this->session->start();
}
/**
* Garbage collector - cache/temp cleaner
*/
public function gc()
{
rcube_cache::gc();
$this->get_storage()->cache_gc();
$this->gc_temp();
}
/**
* Garbage collector function for temp files.
* Removes temporary files older than temp_dir_ttl.
*/
public function gc_temp()
{
$tmp = unslashify($this->config->get('temp_dir'));
// expire in 48 hours by default
$temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
$temp_dir_ttl = get_offset_sec($temp_dir_ttl);
if ($temp_dir_ttl < 6*3600) {
$temp_dir_ttl = 6*3600; // 6 hours sensible lower bound.
}
$expire = time() - $temp_dir_ttl;
if ($tmp && ($dir = opendir($tmp))) {
while (($fname = readdir($dir)) !== false) {
if (strpos($fname, RCUBE_TEMP_FILE_PREFIX) !== 0) {
continue;
}
if (@filemtime("$tmp/$fname") < $expire) {
@unlink("$tmp/$fname");
}
}
closedir($dir);
}
}
/**
* Runs garbage collector with probability based on
* session settings. This is intended for environments
* without a session.
*/
public function gc_run()
{
$probability = (int) ini_get('session.gc_probability');
$divisor = (int) ini_get('session.gc_divisor');
if ($divisor > 0 && $probability > 0) {
$random = mt_rand(1, $divisor);
if ($random <= $probability) {
$this->gc();
}
}
}
/**
* Get localized text in the desired language
*
* @param mixed $attrib Named parameters array or label name
* @param string $domain Label domain (plugin) name
*
* @return string Localized text
*/
public function gettext($attrib, $domain = null)
{
// load localization files if not done yet
if (empty($this->texts)) {
$this->load_language();
}
// extract attributes
if (is_string($attrib)) {
$attrib = array('name' => $attrib);
}
$name = (string) $attrib['name'];
// attrib contain text values: use them from now
$slang = !empty($_SESSION['language']) ? strtolower($_SESSION['language']) : 'en_us';
if (isset($attrib[$slang])) {
$this->texts[$name] = $attrib[$slang];
}
if ($slang != 'en_us' && isset($attrib['en_us'])) {
$this->texts[$name] = $attrib['en_us'];
}
// check for text with domain
if ($domain && isset($this->texts["$domain.$name"])) {
$text = $this->texts["$domain.$name"];
}
else if (isset($this->texts[$name])) {
$text = $this->texts[$name];
}
// text does not exist
if (!isset($text)) {
return "[$name]";
}
// replace vars in text
if (!empty($attrib['vars']) && is_array($attrib['vars'])) {
foreach ($attrib['vars'] as $var_key => $var_value) {
$text = str_replace($var_key[0] != '$' ? '$'.$var_key : $var_key, $var_value, $text);
}
}
// replace \n with real line break
$text = strtr($text, array('\n' => "\n"));
// case folding
if ((!empty($attrib['uppercase']) && strtolower($attrib['uppercase']) == 'first') || !empty($attrib['ucfirst'])) {
$case_mode = MB_CASE_TITLE;
}
else if (!empty($attrib['uppercase'])) {
$case_mode = MB_CASE_UPPER;
}
else if (!empty($attrib['lowercase'])) {
$case_mode = MB_CASE_LOWER;
}
if (isset($case_mode)) {
$text = mb_convert_case($text, $case_mode);
}
return $text;
}
/**
* Check if the given text label exists
*
* @param string $name Label name
* @param string $domain Label domain (plugin) name or '*' for all domains
* @param string &$ref_domain Sets domain name if label is found
*
* @return boolean True if text exists (either in the current language or in en_US)
*/
public function text_exists($name, $domain = null, &$ref_domain = null)
{
// load localization files if not done yet
if (empty($this->texts)) {
$this->load_language();
}
if (isset($this->texts[$name])) {
$ref_domain = '';
return true;
}
// any of loaded domains (plugins)
if ($domain == '*') {
foreach ($this->plugins->loaded_plugins() as $domain) {
if (isset($this->texts[$domain.'.'.$name])) {
$ref_domain = $domain;
return true;
}
}
}
// specified domain
else if ($domain && isset($this->texts[$domain.'.'.$name])) {
$ref_domain = $domain;
return true;
}
return false;
}
/**
* Load a localization package
*
* @param string $lang Language ID
* @param array $add Additional text labels/messages
* @param array $merge Additional text labels/messages to merge
*/
public function load_language($lang = null, $add = array(), $merge = array())
{
$sess_lang = !empty($_SESSION['language']) ? $_SESSION['language'] : 'en_US';
$lang = $this->language_prop($lang ?: $sess_lang);
// load localized texts
if (empty($this->texts) || $lang != $sess_lang) {
// get english labels (these should be complete)
$files = array(
RCUBE_LOCALIZATION_DIR . 'en_US/labels.inc',
RCUBE_LOCALIZATION_DIR . 'en_US/messages.inc',
);
// include user language files
if ($lang != 'en' && $lang != 'en_US' && is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
$files[] = RCUBE_LOCALIZATION_DIR . $lang . '/labels.inc';
$files[] = RCUBE_LOCALIZATION_DIR . $lang . '/messages.inc';
}
$this->texts = array();
foreach ($files as $file) {
$this->texts = self::read_localization_file($file, $this->texts);
}
$_SESSION['language'] = $lang;
}
// append additional texts (from plugin)
if (is_array($add) && !empty($add)) {
$this->texts += $add;
}
// merge additional texts (from plugin)
if (is_array($merge) && !empty($merge)) {
$this->texts = array_merge($this->texts, $merge);
}
}
/**
* Read localized texts from an additional location (plugins, skins).
* Then you can use the result as 2nd arg to load_language().
*
* @param string $dir Directory to search in
* @param string|null $lang Language code to read
*
* @return array Localization labels/messages
*/
public function read_localization($dir, $lang = null)
{
if ($lang == null) {
$lang = $_SESSION['language'];
}
$langs = array_unique(array('en_US', $lang));
$locdir = slashify($dir);
$texts = array();
// Language aliases used to find localization in similar lang, see below
$aliases = array(
'de_CH' => 'de_DE',
'es_AR' => 'es_ES',
'fa_AF' => 'fa_IR',
'nl_BE' => 'nl_NL',
'pt_BR' => 'pt_PT',
'zh_CN' => 'zh_TW',
);
foreach ($langs as $lng) {
$fpath = $locdir . $lng . '.inc';
$_texts = self::read_localization_file($fpath);
if (!empty($_texts)) {
$texts = array_merge($texts, $_texts);
}
// Fallback to a localization in similar language (#1488401)
else if ($lng != 'en_US') {
$alias = null;
if (!empty($aliases[$lng])) {
$alias = $aliases[$lng];
}
else if ($key = array_search($lng, $aliases)) {
$alias = $key;
}
if (!empty($alias)) {
$fpath = $locdir . $alias . '.inc';
$texts = self::read_localization_file($fpath, $texts);
}
}
}
return $texts;
}
/**
* Load localization file
*
* @param string $file File location
* @param array $texts Additional texts to merge with
*
* @return array Localization labels/messages
*/
public static function read_localization_file($file, $texts = array())
{
if (is_file($file) && is_readable($file)) {
$labels = [];
$messages = [];
// use buffering to handle empty lines/spaces after closing PHP tag
ob_start();
include $file;
ob_end_clean();
if (!empty($labels)) {
$texts = array_merge($texts, $labels);
}
if (!empty($messages)) {
$texts = array_merge($texts, $messages);
}
}
return $texts;
}
/**
* Check the given string and return a valid language code
*
* @param string $lang Language code
*
* @return string Valid language code
*/
protected function language_prop($lang)
{
static $rcube_languages, $rcube_language_aliases;
// user HTTP_ACCEPT_LANGUAGE if no language is specified
if (empty($lang) || $lang == 'auto') {
$accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
$lang = $accept_langs[0];
if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
$lang = $m[1] . '_' . strtoupper($m[2]);
}
}
if (empty($rcube_languages)) {
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
}
// check if we have an alias for that language
if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
$lang = $rcube_language_aliases[$lang];
}
// try the first two chars
else if (!isset($rcube_languages[$lang])) {
$short = substr($lang, 0, 2);
// check if we have an alias for the short language code
if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
$lang = $rcube_language_aliases[$short];
}
// expand 'nn' to 'nn_NN'
else if (!isset($rcube_languages[$short])) {
$lang = $short.'_'.strtoupper($short);
}
}
if (!isset($rcube_languages[$lang]) || !is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
$lang = 'en_US';
}
return $lang;
}
/**
* Read directory program/localization and return a list of available languages
*
* @return array List of available localizations
*/
public function list_languages()
{
static $sa_languages = array();
if (!count($sa_languages)) {
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
if ($dh = @opendir(RCUBE_LOCALIZATION_DIR)) {
while (($name = readdir($dh)) !== false) {
if ($name[0] == '.' || !is_dir(RCUBE_LOCALIZATION_DIR . $name)) {
continue;
}
if (isset($rcube_languages[$name])) {
$sa_languages[$name] = $rcube_languages[$name];
}
}
closedir($dh);
}
}
return $sa_languages;
}
/**
* Encrypt a string
*
* @param string $clear Clear text input
* @param string $key Encryption key to retrieve from the configuration, defaults to 'des_key'
* @param bool $base64 Whether or not to base64_encode() the result before returning
*
* @return string Encrypted text
*/
public function encrypt($clear, $key = 'des_key', $base64 = true)
{
if (!is_string($clear) || !strlen($clear)) {
return '';
}
$ckey = $this->config->get_crypto_key($key);
$method = $this->config->get_crypto_method();
$opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
$iv = rcube_utils::random_bytes(openssl_cipher_iv_length($method), true);
$cipher = openssl_encrypt($clear, $method, $ckey, $opts, $iv);
if ($cipher === false) {
self::raise_error(array(
'file' => __FILE__,
'line' => __LINE__,
'message' => "Failed to encrypt data with configured cipher method: $method!"
), true, false);
return false;
}
$cipher = $iv . $cipher;
return $base64 ? base64_encode($cipher) : $cipher;
}
/**
* Decrypt a string
*
* @param string $cipher Encrypted text
* @param string $key Encryption key to retrieve from the configuration, defaults to 'des_key'
* @param boo $base64 Whether or not input is base64-encoded
*
* @return string Decrypted text
*/
public function decrypt($cipher, $key = 'des_key', $base64 = true)
{
if (!$cipher) {
return '';
}
$cipher = $base64 ? base64_decode($cipher) : $cipher;
$ckey = $this->config->get_crypto_key($key);
$method = $this->config->get_crypto_method();
$opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
$iv_size = openssl_cipher_iv_length($method);
$iv = substr($cipher, 0, $iv_size);
// session corruption? (#1485970)
if (strlen($iv) < $iv_size) {
return '';
}
$cipher = substr($cipher, $iv_size);
$clear = openssl_decrypt($cipher, $method, $ckey, $opts, $iv);
return $clear;
}
/**
* Returns session token for secure URLs
*
* @param bool $generate Generate token if not exists in session yet
*
* @return string|bool Token string, False when disabled
*/
public function get_secure_url_token($generate = false)
{
if ($len = $this->config->get('use_secure_urls')) {
if (empty($_SESSION['secure_token']) && $generate) {
// generate x characters long token
$length = $len > 1 ? $len : 16;
$token = rcube_utils::random_bytes($length);
$plugin = $this->plugins->exec_hook('secure_token',
array('value' => $token, 'length' => $length));
$_SESSION['secure_token'] = $plugin['value'];
}
return $_SESSION['secure_token'];
}
return false;
}
/**
* Generate a unique token to be used in a form request
*
* @return string The request token
*/
public function get_request_token()
{
if (empty($_SESSION['request_token'])) {
$plugin = $this->plugins->exec_hook('request_token', array(
'value' => rcube_utils::random_bytes(32)));
$_SESSION['request_token'] = $plugin['value'];
}
return $_SESSION['request_token'];
}
/**
* Check if the current request contains a valid token.
* Empty requests aren't checked until use_secure_urls is set.
*
* @param int $mode Request method
*
* @return bool True if request token is valid false if not
*/
public function check_request($mode = rcube_utils::INPUT_POST)
{
// check secure token in URL if enabled
if ($token = $this->get_secure_url_token()) {
foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) {
if ($tok == $token) {
return true;
}
}
$this->request_status = self::REQUEST_ERROR_URL;
return false;
}
$sess_tok = $this->get_request_token();
// ajax requests
if (rcube_utils::request_header('X-Roundcube-Request') === $sess_tok) {
return true;
}
// skip empty requests
if (($mode == rcube_utils::INPUT_POST && empty($_POST))
|| ($mode == rcube_utils::INPUT_GET && empty($_GET))
) {
return true;
}
// default method of securing requests
- $token = rcube_utils::get_input_value('_token', $mode);
- $sess_id = $_COOKIE[ini_get('session.name')];
+ $token = rcube_utils::get_input_value('_token', $mode);
- if (empty($sess_id) || $token !== $sess_tok) {
+ if (empty($_COOKIE[ini_get('session.name')]) || $token !== $sess_tok) {
$this->request_status = self::REQUEST_ERROR_TOKEN;
return false;
}
return true;
}
/**
* 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
*
* @return string Valid application URL
*/
public function url($p)
{
// STUB: should be overloaded by the application
return '';
}
/**
* Function to be executed in script shutdown
* Registered with register_shutdown_function()
*/
public function shutdown()
{
foreach ($this->shutdown_functions as $function) {
call_user_func($function);
}
// write session data as soon as possible and before
// closing database connection, don't do this before
// registered shutdown functions, they may need the session
// Note: this will run registered gc handlers (ie. cache gc)
if (!empty($_SERVER['REMOTE_ADDR']) && is_object($this->session)) {
$this->session->write_close();
}
if (is_object($this->smtp)) {
$this->smtp->disconnect();
}
foreach ($this->caches as $cache) {
if (is_object($cache)) {
$cache->close();
}
}
if (is_object($this->storage)) {
$this->storage->close();
}
if ($this->config->get('log_driver') == 'syslog') {
closelog();
}
}
/**
* Registers shutdown function to be executed on shutdown.
* The functions will be executed before destroying any
* objects like smtp, imap, session, etc.
*
* @param callback $function Function callback
*/
public function add_shutdown_function($function)
{
$this->shutdown_functions[] = $function;
}
/**
* When you're going to sleep the script execution for a longer time
* it is good to close all external connections (sql, memcache, redis, SMTP, IMAP).
*
* No action is required on wake up, all connections will be
* re-established automatically.
*/
public function sleep()
{
foreach ($this->caches as $cache) {
if (is_object($cache)) {
$cache->close();
}
}
if ($this->storage) {
$this->storage->close();
}
if ($this->db) {
$this->db->closeConnection();
}
if ($this->memcache) {
$this->memcache->close();
}
if ($this->memcached) {
$this->memcached->quit();
}
if ($this->smtp) {
$this->smtp->disconnect();
}
if ($this->redis) {
$this->redis->close();
}
}
/**
* Quote a given string.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @param string $str A string to quote
* @param string $mode Replace mode for tags: show|remove|strict
* @param bool $newlines Convert newlines
*
* @return string HTML-quoted string
*/
public static function Q($str, $mode = 'strict', $newlines = true)
{
return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);
}
/**
* Quote a given string for javascript output.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @param string $str A string to quote
*
* @return string JS-quoted string
*/
public static function JQ($str)
{
return rcube_utils::rep_specialchars_output($str, 'js');
}
/**
* Quote a given string, remove new-line characters, use strict mode.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @param string $str A string to quote
*
* @return string HTML-quoted string
*/
public static function SQ($str)
{
return rcube_utils::rep_specialchars_output($str, 'html', 'strict', false);
}
/**
* Construct shell command, execute it and return output as string.
* Keywords {keyword} are replaced with arguments
*
* @param string $cmd Format string with {keywords} to be replaced
* @param mixed $values,... (zero, one or more arrays can be passed)
*
* @return string Output of command. Shell errors not detectable
*/
public static function exec(/* $cmd, $values1 = array(), ... */)
{
$args = func_get_args();
$cmd = array_shift($args);
$values = $replacements = array();
// merge values into one array
foreach ($args as $arg) {
$values += (array)$arg;
}
preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
foreach ($matches as $tags) {
list(, $tag, $option, $key) = $tags;
$parts = array();
if ($option) {
foreach ((array)$values["-$key"] as $key => $value) {
if ($value === true || $value === false || $value === null) {
$parts[] = $value ? $key : "";
}
else {
foreach ((array)$value as $val) {
$parts[] = "$key " . escapeshellarg($val);
}
}
}
}
else {
foreach ((array)$values[$key] as $value) {
$parts[] = escapeshellarg($value);
}
}
$replacements[$tag] = implode(' ', $parts);
}
// use strtr behaviour of going through source string once
$cmd = strtr($cmd, $replacements);
return (string) shell_exec($cmd);
}
/**
* Print or write debug messages
*
* @param mixed Debug message or data
*/
public static function console()
{
$args = func_get_args();
if (class_exists('rcube', false)) {
$rcube = self::get_instance();
$plugin = $rcube->plugins->exec_hook('console', array('args' => $args));
if ($plugin['abort']) {
return;
}
$args = $plugin['args'];
}
$msg = array();
foreach ($args as $arg) {
$msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
}
self::write_log('console', implode(";\n", $msg));
}
/**
* Append a line to a logfile in the logs directory.
* Date will be added automatically to the line.
*
* @param string $name Name of the log file
* @param mixed $line Line to append
*
* @return bool True on success, False on failure
*/
public static function write_log($name, $line)
{
if (!is_string($line)) {
$line = var_export($line, true);
}
$date_format = $log_driver = $session_key = null;
if (self::$instance) {
$date_format = self::$instance->config->get('log_date_format');
$log_driver = self::$instance->config->get('log_driver');
$session_key = intval(self::$instance->config->get('log_session_id', 8));
}
$date = rcube_utils::date_format($date_format);
// trigger logging hook
if (is_object(self::$instance) && is_object(self::$instance->plugins)) {
$log = self::$instance->plugins->exec_hook('write_log',
array('name' => $name, 'date' => $date, 'line' => $line));
$name = $log['name'];
$line = $log['line'];
$date = $log['date'];
if ($log['abort']) {
return true;
}
}
// add session ID to the log
if ($session_key > 0 && ($sess = session_id())) {
$line = '<' . substr($sess, 0, $session_key) . '> ' . $line;
}
if ($log_driver == 'syslog') {
$prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
return syslog($prio, $line);
}
// write message with file name when configured to log to STDOUT
if ($log_driver == 'stdout') {
$stdout = "php://stdout";
$line = "$name: $line\n";
return file_put_contents($stdout, $line, FILE_APPEND) !== false;
}
// log_driver == 'file' is assumed here
$line = sprintf("[%s]: %s\n", $date, $line);
// per-user logging is activated
if (self::$instance && self::$instance->config->get('per_user_logging')
&& self::$instance->get_user_id()
&& !in_array($name, array('userlogins', 'sendmail'))
) {
$log_dir = self::$instance->get_user_log_dir();
if (empty($log_dir) && $name !== 'errors') {
return false;
}
}
if (empty($log_dir)) {
if (!empty($log['dir'])) {
$log_dir = $log['dir'];
}
else if (self::$instance) {
$log_dir = self::$instance->config->get('log_dir');
}
}
if (empty($log_dir)) {
$log_dir = RCUBE_INSTALL_PATH . 'logs';
}
if (self::$instance) {
$name .= self::$instance->config->get('log_file_ext', '.log');
}
else {
$name .= '.log';
}
return file_put_contents("$log_dir/$name", $line, FILE_APPEND) !== false;
}
/**
* Throw system error (and show error page).
*
* @param array $arg Named parameters
* - code: Error code (required)
* - type: Error type [php|db|imap|javascript]
* - message: Error message
* - file: File where error occurred
* - line: Line where error occurred
* @param bool $log True to log the error
* @param bool $terminate Terminate script execution
*/
public static function raise_error($arg = array(), $log = false, $terminate = false)
{
// handle PHP exceptions
if ($arg instanceof Exception) {
$arg = array(
'code' => $arg->getCode(),
'line' => $arg->getLine(),
'file' => $arg->getFile(),
'message' => $arg->getMessage(),
);
}
else if ($arg instanceof PEAR_Error) {
$info = $arg->getUserInfo();
$arg = array(
'code' => $arg->getCode(),
'message' => $arg->getMessage() . ($info ? ': ' . $info : ''),
);
}
else if (is_string($arg)) {
$arg = array('message' => $arg);
}
if (empty($arg['code'])) {
$arg['code'] = 500;
}
$cli = php_sapi_name() == 'cli';
$arg['cli'] = $cli;
$arg['log'] = $log;
$arg['terminate'] = $terminate;
// send error to external error tracking tool
$arg = self::$instance->plugins->exec_hook('raise_error', $arg);
// installer
if (!$cli && class_exists('rcmail_install', false)) {
$rci = rcmail_install::get_instance();
$rci->raise_error($arg);
return;
}
if (($log || $terminate) && !$cli && $arg['message']) {
$arg['fatal'] = $terminate;
self::log_bug($arg);
}
if ($cli) {
fwrite(STDERR, 'ERROR: ' . trim($arg['message']) . "\n");
}
else if ($terminate && is_object(self::$instance->output)) {
self::$instance->output->raise_error($arg['code'], $arg['message']);
}
else if ($terminate) {
header("HTTP/1.0 500 Internal Error");
}
// terminate script
if ($terminate) {
+ if (defined('ROUNDCUBE_TEST_MODE') && ROUNDCUBE_TEST_MODE) {
+ throw new Exception('Error raised');
+ }
exit(1);
}
}
/**
* Log an error
*
* @param array $arg_arr Named parameters
* @see self::raise_error()
*/
public static function log_bug($arg_arr)
{
$program = strtoupper($arg_arr['type'] ?: 'php');
$uri = $_SERVER['REQUEST_URI'];
// write error to local log file
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$post_query = array();
foreach (array('_task', '_action') as $arg) {
if ($_POST[$arg] && !$_GET[$arg]) {
$post_query[$arg] = $_POST[$arg];
}
}
if (!empty($post_query)) {
$uri .= (strpos($uri, '?') != false ? '&' : '?')
. http_build_query($post_query, '', '&');
}
}
$log_entry = sprintf("%s Error: %s%s (%s %s)",
$program,
$arg_arr['message'],
$arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
$_SERVER['REQUEST_METHOD'],
$uri);
if (!self::write_log('errors', $log_entry)) {
// send error to PHPs error handler if write_log didn't succeed
trigger_error($arg_arr['message'], E_USER_WARNING);
}
}
/**
* Write debug info to the log
*
* @param string $engine Engine type - file name (memcache, apc, redis)
* @param string $data Data string to log
* @param bool $result Operation result
*/
public static function debug($engine, $data, $result = null)
{
static $debug_counter;
$line = '[' . (++$debug_counter[$engine]) . '] ' . $data;
if (($len = strlen($line)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$line = substr($line, 0, self::DEBUG_LINE_LENGTH) . "... [truncated $diff bytes]";
}
if ($result !== null) {
$line .= ' [' . ($result ? 'TRUE' : 'FALSE') . ']';
}
self::write_log($engine, $line);
}
/**
* Returns current time (with microseconds).
*
* @return float Current time in seconds since the Unix
*/
public static function timer()
{
return microtime(true);
}
/**
* Logs time difference according to provided timer
*
* @param float $timer Timer (self::timer() result)
* @param string $label Log line prefix
* @param string $dest Log file name
*
* @see self::timer()
*/
public static function print_timer($timer, $label = 'Timer', $dest = 'console')
{
static $print_count = 0;
$print_count++;
$now = self::timer();
$diff = $now - $timer;
if (empty($label)) {
$label = 'Timer '.$print_count;
}
self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
}
/**
* Setter for system user object
*
* @param rcube_user Current user instance
*/
public function set_user($user)
{
if (is_object($user)) {
$this->user = $user;
// overwrite config with user preferences
$this->config->set_user_prefs((array)$this->user->get_prefs());
}
}
/**
* Getter for logged user ID.
*
* @return mixed User identifier
*/
public function get_user_id()
{
if (is_object($this->user)) {
return $this->user->ID;
}
else if (isset($_SESSION['user_id'])) {
return $_SESSION['user_id'];
}
}
/**
* Getter for logged user name.
*
* @return string User name
*/
public function get_user_name()
{
if (is_object($this->user)) {
return $this->user->get_username();
}
else if (isset($_SESSION['username'])) {
return $_SESSION['username'];
}
}
/**
* Getter for logged user email (derived from user name not identity).
*
* @return string User email address
*/
public function get_user_email()
{
if (is_object($this->user)) {
return $this->user->get_username('mail');
}
}
/**
* Getter for logged user password.
*
* @return string User password
*/
public function get_user_password()
{
if (!empty($this->password)) {
return $this->password;
}
else if ($_SESSION['password']) {
return $this->decrypt($_SESSION['password']);
}
}
/**
* Get the per-user log directory
*
* @return string|false Per-user log directory if it exists and is writable, False otherwise
*/
protected function get_user_log_dir()
{
$log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
$user_name = $this->get_user_name();
$user_log_dir = $log_dir . '/' . $user_name;
return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
}
/**
* Getter for logged user language code.
*
* @return string User language code
*/
public function get_user_language()
{
if (is_object($this->user)) {
return $this->user->language;
}
else if (isset($_SESSION['language'])) {
return $_SESSION['language'];
}
}
/**
* Unique Message-ID generator.
*
* @param string $sender Optional sender e-mail address
*
* @return string Message-ID
*/
public function gen_message_id($sender = null)
{
$local_part = md5(uniqid('rcube'.mt_rand(), true));
$domain_part = '';
if ($sender && preg_match('/@([^\s]+\.[a-z0-9-]+)/', $sender, $m)) {
$domain_part = $m[1];
}
else {
$domain_part = $this->user->get_username('domain');
}
// Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
if (!preg_match('/\.[a-z0-9-]+$/i', $domain_part)) {
foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
$host = preg_replace('/:[0-9]+$/', '', $host);
if ($host && preg_match('/\.[a-z]+$/i', $host)) {
$domain_part = $host;
break;
}
}
}
return sprintf('<%s@%s>', $local_part, $domain_part);
}
/**
* Send the given message using the configured method.
*
* @param Mail_Mime &$message Reference to Mail_MIME object
* @param string $from Sender address string
* @param array|string $mailto Either a comma-separated list of recipients (RFC822 compliant),
* or an array of recipients, each RFC822 valid
* @param array|string &$error SMTP error array or (deprecated) string
* @param string &$body_file Location of file with saved message body,
* used when delay_file_io is enabled
* @param array $options SMTP options (e.g. DSN request)
* @param bool $disconnect Close SMTP connection ASAP
*
* @return bool Send status.
*/
public function deliver_message(&$message, $from, $mailto, &$error,
&$body_file = null, $options = null, $disconnect = false)
{
$plugin = $this->plugins->exec_hook('message_before_send', array(
'message' => $message,
'from' => $from,
'mailto' => $mailto,
'options' => $options,
));
if ($plugin['abort']) {
if (!empty($plugin['error'])) {
$error = $plugin['error'];
}
if (!empty($plugin['body_file'])) {
$body_file = $plugin['body_file'];
}
return isset($plugin['result']) ? $plugin['result'] : false;
}
$from = $plugin['from'];
$mailto = $plugin['mailto'];
$options = $plugin['options'];
$message = $plugin['message'];
$headers = $message->headers();
// generate list of recipients
$a_recipients = (array) $mailto;
if (strlen($headers['Cc'])) {
$a_recipients[] = $headers['Cc'];
}
if (strlen($headers['Bcc'])) {
$a_recipients[] = $headers['Bcc'];
}
// remove Bcc header and get the whole head of the message as string
$smtp_headers = $message->txtHeaders(array('Bcc' => null), true);
if ($message->getParam('delay_file_io')) {
// use common temp dir
$body_file = rcube_utils::temp_filename('msg');
$mime_result = $message->saveMessageBody($body_file);
if (is_a($mime_result, 'PEAR_Error')) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
$msg_body = fopen($body_file, 'r');
}
else {
$msg_body = $message->get();
}
// initialize SMTP connection
if (!is_object($this->smtp)) {
$this->smtp_init(true);
}
// send message
$sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options);
$response = $this->smtp->get_response();
$error = $this->smtp->get_error();
if (!$sent) {
self::raise_error(array('code' => 800, 'type' => 'smtp',
'line' => __LINE__, 'file' => __FILE__,
'message' => implode("\n", $response)), true, false);
// allow plugins to catch sending errors with the same parameters as in 'message_before_send'
$this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
}
else {
$this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body, 'message' => $message));
// remove MDN headers after sending
unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
if ($this->config->get('smtp_log')) {
// get all recipient addresses
$mailto = implode(',', $a_recipients);
$mailto = rcube_mime::decode_address_list($mailto, null, false, null, true);
self::write_log('sendmail', sprintf("User %s [%s]; Message %s for %s; %s",
$this->user->get_username(),
rcube_utils::remote_addr(),
$headers['Message-ID'],
implode(', ', $mailto),
!empty($response) ? implode('; ', $response) : ''));
}
}
if (is_resource($msg_body)) {
fclose($msg_body);
}
if ($disconnect) {
$this->smtp->disconnect();
}
// Add Bcc header back
if (!empty($headers['Bcc'])) {
$message->headers(array('Bcc' => $headers['Bcc']), true);
}
return $sent;
}
}
/**
* Lightweight plugin API class serving as a dummy if plugins are not enabled
*
* @package Framework
* @subpackage Core
*/
class rcube_dummy_plugin_api
{
/**
* Triggers a plugin hook.
*
* @param string $hook Hook name
* @param array $args Hook arguments
*
* @return array Hook arguments
* @see rcube_plugin_api::exec_hook()
*/
public function exec_hook($hook, $args = array())
{
return $args;
}
}
diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php
index 046f265eb..18be3778e 100644
--- a/program/lib/Roundcube/rcube_addressbook.php
+++ b/program/lib/Roundcube/rcube_addressbook.php
@@ -1,742 +1,747 @@
<?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: |
| Interface to the local address book database |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Abstract skeleton of an address book/repository
*
* @package Framework
* @subpackage Addressbook
*/
abstract class rcube_addressbook
{
// constants for error reporting
const ERROR_READ_ONLY = 1;
const ERROR_NO_CONNECTION = 2;
const ERROR_VALIDATE = 3;
const ERROR_SAVING = 4;
const ERROR_SEARCH = 5;
// search modes
const SEARCH_ALL = 0;
const SEARCH_STRICT = 1;
const SEARCH_PREFIX = 2;
const SEARCH_GROUPS = 4;
// contact types, note: some of these are used as addressbook source identifiers
const TYPE_CONTACT = 0;
const TYPE_RECIPIENT = 1;
const TYPE_TRUSTED_SENDER = 2;
const TYPE_DEFAULT = 4;
const TYPE_WRITEABLE = 8;
const TYPE_READONLY = 16;
// public properties (mandatory)
public $primary_key;
public $groups = false;
public $export_groups = true;
public $readonly = true;
public $searchonly = false;
public $undelete = false;
public $ready = false;
public $group_id = null;
public $list_page = 1;
public $page_size = 10;
public $sort_col = 'name';
public $sort_order = 'ASC';
public $date_cols = array();
public $coltypes = array(
'name' => array('limit'=>1),
'firstname' => array('limit'=>1),
'surname' => array('limit'=>1),
'email' => array('limit'=>1)
);
+ /**
+ * vCard additional fields mapping
+ */
+ public $vcard_map = [];
+
protected $error;
/**
* Returns addressbook name (e.g. for addressbooks listing)
*/
abstract function get_name();
/**
* Save a search string for future listings
*
* @param mixed $filter Search params to use in listing method, obtained by get_search_set()
*/
abstract function set_search_set($filter);
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
abstract function get_search_set();
/**
* Reset saved results and search parameters
*/
abstract function reset();
/**
* Refresh saved search set after data has changed
*
* @return mixed New search set
*/
function refresh_search()
{
return $this->get_search_set();
}
/**
* List the current set of contact records
*
* @param array $cols List of cols to show
* @param int $subset Only return this number of records, use negative values for tail
*
* @return rcube_result_set Indexed list of contact records, each a hash array
*/
abstract function list_records($cols=null, $subset=0);
/**
* Search records
*
* @param array $fields List of fields to search in
* @param string $value Search value
* @param int $mode Search mode. Sum of self::SEARCH_*.
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array());
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
abstract function count();
/**
* Return the last result set
*
* @return ?rcube_result_set Current result set or NULL if nothing selected yet
*/
abstract function get_result();
/**
* Get a specific contact record
*
* @param mixed $id Record identifier(s)
* @param boolean $assoc True to return record as associative array, otherwise a result set is returned
*
* @return rcube_result_set|array Result object with all record fields
*/
abstract function get_record($id, $assoc=false);
/**
* Returns the last error occurred (e.g. when updating/inserting failed)
*
* @return array Hash array with the following fields: type, message
*/
function get_error()
{
return $this->error;
}
/**
* Setter for errors for internal use
*
* @param int $type Error type (one of this class' error constants)
* @param string $message Error message (name of a text label)
*/
protected function set_error($type, $message)
{
$this->error = array('type' => $type, 'message' => $message);
}
/**
* Close connection to source
* Called on script shutdown
*/
function close() { }
/**
* Set internal list page
*
* @param number $page Page number to list
*/
function set_page($page)
{
$this->list_page = (int)$page;
}
/**
* Set internal page size
*
* @param number $size Number of messages to display on one page
*/
function set_pagesize($size)
{
$this->page_size = (int) $size;
}
/**
* Set internal sort settings
*
* @param string $sort_col Sort column
* @param string $sort_order Sort order
*/
function set_sort_order($sort_col, $sort_order = null)
{
if ($sort_col != null && in_array($sort_col, $this->coltypes)) {
$this->sort_col = $sort_col;
}
if ($sort_order != null) {
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
}
/**
* Check the given data before saving.
* If input isn't valid, the message to display can be fetched using get_error()
*
* @param array &$save_data Associative array with data to save
* @param boolean $autofix Attempt to fix/complete record automatically
*
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
$rcube = rcube::get_instance();
$valid = true;
// check validity of email addresses
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (strlen($email)) {
if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
$error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
$this->set_error(self::ERROR_VALIDATE, $error);
$valid = false;
break;
}
}
}
// allow plugins to do contact validation and auto-fixing
$plugin = $rcube->plugins->exec_hook('contact_validate', array(
'record' => $save_data,
'autofix' => $autofix,
'valid' => $valid,
));
if ($valid && !$plugin['valid']) {
$this->set_error(self::ERROR_VALIDATE, $plugin['error']);
}
if (is_array($plugin['record'])) {
$save_data = $plugin['record'];
}
return $plugin['valid'];
}
/**
* Create a new contact record
*
* @param array $save_data Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean $check True to check for duplicates first
*
* @return mixed The created record ID on success, False on error
*/
function insert($save_data, $check = false)
{
/* empty for read-only address books */
}
/**
* Create new contact records for every item in the record set
*
* @param rcube_result_set $recset Recordset to insert
* @param boolean $check True to check for duplicates first
*
* @return array List of created record IDs
*/
function insertMultiple($recset, $check = false)
{
$ids = array();
if ($recset instanceof rcube_result_set) {
while ($row = $recset->next()) {
if ($insert = $this->insert($row, $check))
$ids[] = $insert;
}
}
return $ids;
}
/**
* Update a specific contact record
*
* @param mixed $id Record identifier
* @param array $save_cols Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
*
* @return mixed On success if ID has been changed returns ID, otherwise True, False on error
*/
function update($id, $save_cols)
{
/* empty for read-only address books */
}
/**
* Mark one or more contact records as deleted
*
* @param array $ids Record identifiers
* @param bool $force Remove records irreversible (see self::undelete)
*/
function delete($ids, $force = true)
{
/* empty for read-only address books */
}
/**
* Unmark delete flag on contact record(s)
*
* @param array $ids Record identifiers
*/
function undelete($ids)
{
/* empty for read-only address books */
}
/**
* Mark all records in database as deleted
*
* @param bool $with_groups Remove also groups
*/
function delete_all($with_groups = false)
{
/* empty for read-only address books */
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($group_id) { }
/**
* List all active contact groups of this source
*
* @param string $search Optional search string to match group name
* @param int $mode Search mode. Sum of self::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
/* empty for address books don't supporting groups */
return array();
}
/**
* Get group properties such as name and email address(es)
*
* @param string $group_id Group identifier
*
* @return array Group properties as hash array
*/
function get_group($group_id)
{
/* empty for address books don't supporting groups */
}
/**
* Create a contact group with the given name
*
* @param string $name The group name
*
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Delete the given group and all linked group members
*
* @param string $group_id Group identifier
*
* @return boolean True on success, false if no data was changed
*/
function delete_group($group_id)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Rename a specific contact group
*
* @param string $group_id Group identifier
* @param string $newname New name to set for this group
* @param string &$newid New group identifier (if changed, otherwise don't set)
*
* @return boolean|string New name on success, false if no data was changed
*/
function rename_group($group_id, $newname, &$newid)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Add the given contact records the a certain group
*
* @param string $group_id Group identifier
* @param array|string $ids List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $ids)
{
/* empty for address books don't supporting groups */
return 0;
}
/**
* Remove the given contact records from a certain group
*
* @param string $group_id Group identifier
* @param array|string $ids List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $ids)
{
/* empty for address books don't supporting groups */
return 0;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array $id List of assigned groups as ID=>Name pairs
* @since 0.5-beta
*/
function get_record_groups($id)
{
/* empty for address books don't supporting groups */
return array();
}
/**
* Utility function to return all values of a certain data column
* either as flat list or grouped by subtype
*
* @param string $col Col name
* @param array $data Record data array as used for saving
* @param bool $flat True to return one array with all values,
* False for hash array with values grouped by type
*
* @return array List of column values
*/
public static function get_col_values($col, $data, $flat = false)
{
$out = array();
foreach ((array)$data as $c => $values) {
if ($c === $col || strpos($c, $col.':') === 0) {
if ($flat) {
$out = array_merge($out, (array)$values);
}
else {
list(, $type) = explode(':', $c);
if (isset($out[$type])) {
$out[$type] = array_merge((array) $out[$type], (array) $values);
}
else {
$out[$type] = (array) $values;
}
}
}
}
// remove duplicates
if ($flat && !empty($out)) {
$out = array_unique($out);
}
return $out;
}
/**
* Normalize the given string for fulltext search.
* Currently only optimized for Latin-1 characters; to be extended
*
* @param string $str Input string (UTF-8)
* @return string Normalized string
* @deprecated since 0.9-beta
*/
protected static function normalize_string($str)
{
return rcube_utils::normalize_string($str);
}
/**
* Compose a valid display name from the given structured contact data
*
* @param array $contact Hash array with contact data as key-value pairs
* @param bool $full_email Don't attempt to extract components from the email address
*
* @return string Display name
*/
public static function compose_display_name($contact, $full_email = false)
{
$contact = rcube::get_instance()->plugins->exec_hook('contact_displayname', $contact);
$fn = isset($contact['name']) ? $contact['name'] : '';
// default display name composition according to vcard standard
if (!$fn) {
$keys = ['prefix', 'firstname', 'middlename', 'surname', 'suffix'];
$fn = implode(' ', array_filter(array_intersect_key($contact, array_flip($keys))));
$fn = trim(preg_replace('/\s+/u', ' ', $fn));
}
// use email address part for name
$email = self::get_col_values('email', $contact, true);
$email = $email[0];
if ($email && (empty($fn) || $fn == $email)) {
// return full email
if ($full_email) {
return $email;
}
list($emailname) = explode('@', $email);
if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match)) {
$fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
}
else {
$fn = ucfirst($emailname);
}
}
return $fn;
}
/**
* Compose the name to display in the contacts list for the given contact record.
* This respects the settings parameter how to list conacts.
*
* @param array $contact Hash array with contact data as key-value pairs
*
* @return string List name
*/
public static function compose_list_name($contact)
{
static $compose_mode;
if (!isset($compose_mode)) {
$compose_mode = rcube::get_instance()->config->get('addressbook_name_listing', 0);
}
switch ($compose_mode) {
case 3:
$fn = implode(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename']));
break;
case 2:
$fn = implode(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
break;
case 1:
$fn = implode(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
break;
case 0:
$fn = $contact['name'] ?: implode(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
break;
default:
$plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
$fn = $plugin['fn'];
}
$fn = trim($fn, ', ');
$fn = preg_replace('/\s+/u', ' ', $fn);
// fallbacks...
if ($fn === '') {
// ... display name
if ($name = trim($contact['name'])) {
$fn = $name;
}
// ... organization
else if ($org = trim($contact['organization'])) {
$fn = $org;
}
// ... email address
else if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
$fn = $email[0];
}
}
return $fn;
}
/**
* Build contact display name for autocomplete listing
*
* @param array $contact Hash array with contact data as key-value pairs
* @param string $email Optional email address
* @param string $name Optional name (self::compose_list_name() result)
* @param string $templ Optional template to use (defaults to the 'contact_search_name' config option)
*
* @return string Display name
*/
public static function compose_search_name($contact, $email = null, $name = null, $templ = null)
{
static $template;
if (empty($templ) && !isset($template)) { // cache this
$template = rcube::get_instance()->config->get('contact_search_name');
if (empty($template)) {
$template = '{name} <{email}>';
}
}
$result = $templ ?: $template;
if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) {
foreach ($matches[0] as $key) {
$key = trim($key, '{}');
$value = '';
switch ($key) {
case 'name':
$value = $name ?: self::compose_list_name($contact);
// If name(s) are undefined compose_list_name() may return an email address
// here we prevent from returning the same name and email
if ($name === $email && strpos($result, '{email}') !== false) {
$value = '';
}
break;
case 'email':
$value = $email;
break;
}
if (empty($value)) {
$value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
if (is_array($value)) {
$value = $value[0];
}
}
$result = str_replace('{' . $key . '}', $value, $result);
}
}
$result = preg_replace('/\s+/u', ' ', $result);
$result = preg_replace('/\s*(<>|\(\)|\[\])/u', '', $result);
$result = trim($result, '/ ');
return $result;
}
/**
* Create a unique key for sorting contacts
*
* @param array $contact Contact record
* @param string $sort_col Sorting column name
*
* @return string Unique key
*/
public static function compose_contact_key($contact, $sort_col)
{
$key = $contact[$sort_col];
// add email to a key to not skip contacts with the same name (#1488375)
if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
$key .= ':' . implode(':', (array)$email);
}
// Make the key really unique (as we e.g. support contacts with no email)
$key .= ':' . $contact['sourceid'] . ':' . $contact['ID'];
return $key;
}
/**
* Compare search value with contact data
*
* @param string $colname Data name
* @param string|array $value Data value
* @param string $search Search value
* @param int $mode Search mode
*
* @return bool Comparison result
*/
protected function compare_search_value($colname, $value, $search, $mode)
{
// The value is a date string, for date we'll
// use only strict comparison (mode = 1)
// @TODO: partial search, e.g. match only day and month
if (in_array($colname, $this->date_cols)) {
return (($value = rcube_utils::anytodatetime($value))
&& ($search = rcube_utils::anytodatetime($search))
&& $value->format('Ymd') == $search->format('Ymd'));
}
// Gender is a special value, must use strict comparison (#5757)
if ($colname == 'gender') {
$mode = self::SEARCH_STRICT;
}
// composite field, e.g. address
foreach ((array)$value as $val) {
$val = mb_strtolower($val);
if ($mode & self::SEARCH_STRICT) {
$got = ($val == $search);
}
else if ($mode & self::SEARCH_PREFIX) {
$got = ($search == substr($val, 0, strlen($search)));
}
else {
$got = (strpos($val, $search) !== false);
}
if ($got) {
return true;
}
}
return false;
}
}
diff --git a/tests/ActionTestCase.php b/tests/ActionTestCase.php
index e8e6c5a08..a4a52965a 100644
--- a/tests/ActionTestCase.php
+++ b/tests/ActionTestCase.php
@@ -1,191 +1,207 @@
<?php
/**
* Test class to test rcmail_action_mail_index
*
* @package Tests
*/
class ActionTestCase extends PHPUnit\Framework\TestCase
{
static $files = [];
static function setUpBeforeClass()
{
$rcmail = rcmail::get_instance();
$rcmail->load_gui();
}
static function tearDownAfterClass()
{
foreach (self::$files as $file) {
unlink($file);
}
self::$files = [];
$rcmail = rcmail::get_instance();
$rcmail->shutdown();
}
+ public function setUp()
+ {
+ $_GET = [];
+ $_POST = [];
+ $_REQUEST = [];
+ }
+
/**
* Initialize the testing suite
*/
public static function init()
{
self::initSession();
self::initDB();
self::initUser();
self::initStorage();
}
/**
* Initialize "mocked" output class
*/
protected static function initOutput($mode, $task, $action, $framed = false)
{
$rcmail = rcmail::get_instance();
$rcmail->task = $task;
$rcmail->action = $action;
if ($mode == rcmail_action::MODE_AJAX) {
return $rcmail->output = new OutputJsonMock();
}
$rcmail->output = new OutputHtmlMock($task, $framed);
if ($framed) {
$rcmail->comm_path .= '&_framed=1';
$rcmail->output->set_env('framed', true);
}
$rcmail->output->set_env('task', $task);
$rcmail->output->set_env('action', $action);
$rcmail->output->set_env('comm_path', $rcmail->comm_path);
$rcmail->output->set_charset(RCUBE_CHARSET);
return $rcmail->output;
}
/**
* Wipe and re-initialize database
*/
public static function initDB($file = null)
{
$rcmail = rcmail::get_instance();
$dsn = rcube_db::parse_dsn($rcmail->config->get('db_dsnw'));
$db = $rcmail->get_dbh();
if ($file) {
self::loadSQLScript($db, $file);
return;
}
if ($dsn['phptype'] == 'mysql' || $dsn['phptype'] == 'mysqli') {
// drop all existing tables first
$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 . 'src/sql/init.sql'),
escapeshellarg($dsn['hostspec']),
escapeshellarg($dsn['username']),
escapeshellarg($dsn['password']),
escapeshellarg($dsn['database'])
));
}
else if ($dsn['phptype'] == 'sqlite') {
$db->closeConnection();
// delete database file
system(sprintf('rm -f %s', escapeshellarg($dsn['database'])));
// load sample test data
self::loadSQLScript($db, 'init');
}
}
/**
* Set the $rcmail->user property
*/
public static function initUser()
{
$rcmail = rcmail::get_instance();
$rcmail->set_user(new rcube_user(1));
}
/**
* Set the $rcmail->session property
*/
public static function initSession()
{
$rcmail = rcmail::get_instance();
$rcmail->session = new rcube_session_php($rcmail->config);
}
/**
* Set the $rcmail->storage property
*/
public static function initStorage()
{
$rcmail = rcmail::get_instance();
$rcmail->storage = new StorageMock();
}
/**
* Create a temp file
*/
protected function createTempFile($content = '')
{
$file = rcube_utils::temp_filename('tests');
if ($content !== '') {
file_put_contents($file, $content);
}
self::$files[] = $file;
return $file;
}
/**
* Load an execute specified SQL script
*/
protected static function loadSQLScript($db, $name)
{
// load sample test data
// Note: exec_script() does not really work with these queries
$sql = file_get_contents(TESTS_DIR . "src/sql/{$name}.sql");
$sql = preg_split('/;\n/', $sql, -1, PREG_SPLIT_NO_EMPTY);
foreach ($sql as $query) {
$result = $db->query($query);
if ($db->is_error($result)) {
rcube::raise_error($db->is_error(), false, true);
}
}
}
/**
* Call the action's run() method and handle exit exception
*/
protected function runAndAssert($action, $expected_code)
{
// Reset output in case we execute the method multiple times in a single test
$rcmail = rcmail::get_instance();
$rcmail->output->reset(true);
try {
+ StderrMock::start();
$action->run();
+ StderrMock::stop();
}
catch (ExitException $e) {
$this->assertSame($expected_code, $e->getCode());
}
+ catch (Exception $e) {
+ if ($e->getMessage() == 'Error raised' && $expected_code == OutputHtmlMock::E_EXIT) {
+ return;
+ }
+
+ throw $e;
+ }
}
}
diff --git a/tests/Actions/Contacts/Export.php b/tests/Actions/Contacts/Export.php
index eae51e4fa..067ef337f 100644
--- a/tests/Actions/Contacts/Export.php
+++ b/tests/Actions/Contacts/Export.php
@@ -1,19 +1,119 @@
<?php
/**
* Test class to test rcmail_action_contacts_export
*
* @package Tests
*/
class Actions_Contacts_Export extends ActionTestCase
{
/**
* Class constructor
*/
function test_class()
{
$object = new rcmail_action_contacts_export;
$this->assertInstanceOf('rcmail_action', $object);
}
+
+ /**
+ * Test exporting all contacts
+ */
+ function test_export_all()
+ {
+ $action = new rcmail_action_contacts_export;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'contacts', 'export');
+
+ $this->assertTrue($action->checks());
+
+ self::initDB('contacts');
+
+ $_GET = ['_source' => '0'];
+ $_POST = [];
+
+ // Here we expect request security check error
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+
+ $this->assertSame('ERROR: Request security check failed', trim(StderrMock::$output));
+
+ // Now we'll try with the proper token
+ $_SESSION['request_token'] = 'secure';
+ $_SERVER['HTTP_X_ROUNDCUBE_REQUEST'] = 'secure';
+
+ ob_start();
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+ $vcf = ob_get_contents();
+ ob_end_clean();
+
+ $this->assertSame([
+ 'Content-Type: text/vcard; charset=UTF-8',
+ 'Content-Disposition: attachment; filename="contacts.vcf"'
+ ], $output->headers
+ );
+ $this->assertSame(6, substr_count($vcf, 'BEGIN:VCARD'));
+ $this->assertSame(6, substr_count($vcf, 'END:VCARD'));
+ $this->assertSame(1, substr_count($vcf, 'FN:Jane Stalone'));
+ }
+
+ /**
+ * Test exporting selected contacts
+ *
+ * @depends test_export_all
+ */
+ function test_export_selected()
+ {
+ $action = new rcmail_action_contacts_export;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'contacts', 'export');
+
+ $this->assertTrue($action->checks());
+
+ $cids = [];
+ $db = rcmail::get_instance()->get_dbh();
+ $query = $db->query("SELECT `contact_id` FROM `contacts` WHERE `email` IN ('j.rian@gmail.com', 'g.bush@gov.com')");
+ while ($result = $db->fetch_assoc($query)) {
+ $cids[] = $result['contact_id'];
+ }
+
+ $_GET = ['_source' => '0', '_cid' => implode(',', $cids)];
+ // TODO: This really shouldn't be needed
+ $_REQUEST = ['_cid' => implode(',', $cids)];
+
+ $_SESSION['request_token'] = 'secure';
+ $_SERVER['HTTP_X_ROUNDCUBE_REQUEST'] = 'secure';
+
+ ob_start();
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+ $vcf = ob_get_contents();
+ ob_end_clean();
+
+ $this->assertSame([
+ 'Content-Type: text/vcard; charset=UTF-8',
+ 'Content-Disposition: attachment; filename="contacts.vcf"'
+ ], $output->headers
+ );
+ $this->assertSame(2, substr_count($vcf, 'BEGIN:VCARD'));
+ $this->assertSame(2, substr_count($vcf, 'END:VCARD'));
+ $this->assertSame(0, substr_count($vcf, 'FN:Jane Stalone'));
+ $this->assertSame(1, substr_count($vcf, 'FN:Jack Rian'));
+ $this->assertSame(1, substr_count($vcf, 'FN:George Bush'));
+ }
+
+ /**
+ * Test exporting search result
+ *
+ * @depends test_export_all
+ */
+ function test_export_search()
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test prepare_for_export() method
+ */
+ function test_prepare_for_export()
+ {
+ $this->markTestIncomplete();
+ }
}
diff --git a/tests/OutputHtmlMock.php b/tests/OutputHtmlMock.php
index e4429ad85..32e254788 100644
--- a/tests/OutputHtmlMock.php
+++ b/tests/OutputHtmlMock.php
@@ -1,160 +1,160 @@
<?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: |
| A class for easier testing of code that uses rcmail_output classes |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* A class for easier testing of code that uses rcmail_output classes
*
* @package Tests
*/
class OutputHtmlMock extends rcmail_output_html
{
const E_EXIT = 101;
const E_REDIRECT = 102;
public $output;
public $headers = [];
public $template = '';
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
* @param bool $secure Redirect to secure location (see rcmail::url())
*/
public function redirect($p = [], $delay = 1, $secure = false)
{
if (!empty($this->env['extwin'])) {
$p['extwin'] = 1;
}
$location = $this->app->url($p, false, false, $secure);
// header('Location: ' . $location);
throw new ExitException("Location: $location", self::E_REDIRECT);
}
/**
* Send the request output to the client.
* This will either parse a skin template.
*
* @param string $templ Template name
* @param boolean $exit True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
$this->template = $templ;
parent::send($templ, false);
if ($exit) {
throw new ExitException("Output sent", self::E_EXIT);
}
}
/**
* A helper to send output to the browser and exit
*
* @param string $body The output body
* @param array $headers Headers
*/
- public function sendExit($body, $headers = [])
+ public function sendExit($body = '', $headers = [])
{
foreach ($headers as $header) {
$this->header($header);
}
$this->output = $body;
throw new ExitException("Output sent", self::E_EXIT);
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
ob_start();
parent::write($template);
$this->output = ob_get_contents();
ob_end_clean();
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string $name Template name
* @param boolean $exit Exit script
* @param boolean $write Don't write to stdout, return parsed content instead
*
* @link http://php.net/manual/en/function.exit.php
*/
function parse($name = 'main', $exit = true, $write = true)
{
//ob_start();
parent::parse($name, false, $write);
//$this->output = ob_get_contents();
//ob_end_clean();
if ($exit) {
throw new ExitException("Output sent", self::E_EXIT);
}
}
/**
* Delete all stored env variables and commands
*/
public function reset($all = false)
{
parent::reset($all);
$this->headers = [];
$this->output = null;
$this->template = null;
}
/**
* A wrapper for header() function, so it can be replaced for automated tests
*
* @param string $header The header string
* @param bool $replace Replace previously set header?
*/
public function header($header, $replace = true)
{
$this->headers[] = $header;
}
/**
* Return the output
*/
public function getOutput()
{
return $this->output;
}
/**
* Return private/protected property
*/
public function getProperty($name)
{
return $this->{$name};
}
}
diff --git a/tests/OutputJsonMock.php b/tests/OutputJsonMock.php
index fc827947e..ce19420f3 100644
--- a/tests/OutputJsonMock.php
+++ b/tests/OutputJsonMock.php
@@ -1,143 +1,143 @@
<?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: |
| A class for easier testing of code that uses rcmail_output classes |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* A class for easier testing of code that uses rcmail_output classes
*
* @package Tests
*/
class OutputJsonMock extends rcmail_output_json
{
const E_EXIT = 101;
const E_REDIRECT = 102;
public $output;
public $headers = [];
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
*
* @see rcmail::url()
*/
public function redirect($p = [], $delay = 1)
{
$location = $this->app->url($p);
ob_start();
$this->remote_response(sprintf("window.setTimeout(function(){ %s.redirect('%s',true); }, %d);",
self::JS_OBJECT_NAME, $location, $delay));
$this->output = ob_get_contents();
ob_end_clean();
throw new ExitException("Location: $location", self::E_REDIRECT);
}
/**
* Send an AJAX response to the client.
*/
public function send()
{
ob_start();
$this->remote_response();
$this->output = ob_get_contents();
ob_end_clean();
throw new ExitException("Output sent", self::E_EXIT);
}
/**
* A helper to send output to the browser and exit
*
* @param string $body The output body
* @param array $headers Headers
*/
- public function sendExit($body, $headers = [])
+ public function sendExit($body = '', $headers = [])
{
foreach ($headers as $header) {
$this->header($header);
}
$this->output = $body;
throw new ExitException("Output sent", self::E_EXIT);
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
if ($code == 403) {
throw new ExitException("403 Forbidden", self::E_EXIT);
}
$this->show_message("Application Error ($code): $message", 'error');
ob_start();
$this->remote_response();
$this->output = ob_get_contents();
ob_end_clean();
throw new ExitException("Error $code raised", self::E_EXIT);
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
parent::reset();
$this->headers = [];
$this->output = null;
$this->header_sent = false;
}
/**
* A wrapper for header() function, so it can be replaced for automated tests
*
* @param string $header The header string
* @param bool $replace Replace previously set header?
*/
public function header($header, $replace = true)
{
$this->headers[] = $header;
}
/**
* Return the JSON output as an array
*/
public function getOutput()
{
return $this->output ? json_decode($this->output, true) : null;
}
/**
* Return private/protected property
*/
public function getProperty($name)
{
return $this->{$name};
}
}
diff --git a/tests/StderrMock.php b/tests/StderrMock.php
index 656e26ed7..e6593841c 100644
--- a/tests/StderrMock.php
+++ b/tests/StderrMock.php
@@ -1,52 +1,57 @@
<?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: |
| A class for catching STDERR output |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* A class for catching STDERR output
*
* @package Tests
*/
class StderrMock extends php_user_filter
{
+ public static $registered = false;
public static $redirect;
public static $output = '';
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$consumed += $bucket->datalen;
//stream_bucket_append($out, $bucket);
self::$output .= $bucket->data;
}
return PSFS_PASS_ON;
}
public static function start()
{
- stream_filter_register("redirect", "StderrMock") or die("Failed to register filter");
+ if (!self::$registered) {
+ stream_filter_register("redirect", "StderrMock") or die("Failed to register filter");
+ self::$registered = true;
+ }
+
self::$output = '';
self::$redirect = stream_filter_prepend(STDERR, "redirect", STREAM_FILTER_WRITE);
}
public static function stop()
{
stream_filter_remove(self::$redirect);
}
}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index e59521458..049451060 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,51 +1,52 @@
<?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('ROUNDCUBE_TEST_MODE', true);
define('TESTS_DIR', __DIR__ . '/');
if (@is_dir(TESTS_DIR . 'config')) {
define('RCUBE_CONFIG_DIR', TESTS_DIR . 'config');
}
require_once(INSTALL_PATH . 'program/include/iniset.php');
rcmail::get_instance(0, 'test')->config->set('devel_mode', false);
// 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.");
}
require_once(TESTS_DIR . 'ActionTestCase.php');
require_once(TESTS_DIR . 'ExitException.php');
require_once(TESTS_DIR . 'OutputHtmlMock.php');
require_once(TESTS_DIR . 'OutputJsonMock.php');
require_once(TESTS_DIR . 'StderrMock.php');
require_once(TESTS_DIR . 'StorageMock.php');
// Initialize database and environment
ActionTestCase::init();
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index ac833d672..3de33facb 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -1,62 +1,65 @@
<phpunit
backupGlobals="false"
bootstrap="bootstrap.php"
colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="false"
+ convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="Framework">
<directory suffix=".php">Framework</directory>
</testsuite>
<testsuite name="Roundcube">
<directory suffix=".php">Rcmail</directory>
</testsuite>
<testsuite name="Actions">
<directory suffix=".php">Actions/*</directory>
</testsuite>
<testsuite name="Plugins">
<file>./../plugins/acl/tests/Acl.php</file>
<file>./../plugins/additional_message_headers/tests/AdditionalMessageHeaders.php</file>
<file>./../plugins/archive/tests/Archive.php</file>
<file>./../plugins/attachment_reminder/tests/AttachmentReminder.php</file>
<file>./../plugins/autologon/tests/Autologon.php</file>
<file>./../plugins/database_attachments/tests/DatabaseAttachments.php</file>
<file>./../plugins/debug_logger/tests/DebugLogger.php</file>
<file>./../plugins/emoticons/tests/Emoticons.php</file>
<file>./../plugins/enigma/tests/Enigma.php</file>
<file>./../plugins/example_addressbook/tests/ExampleAddressbook.php</file>
<file>./../plugins/filesystem_attachments/tests/FilesystemAttachments.php</file>
<file>./../plugins/help/tests/Help.php</file>
<file>./../plugins/hide_blockquote/tests/HideBlockquote.php</file>
<file>./../plugins/http_authentication/tests/HttpAuthentication.php</file>
<file>./../plugins/identicon/tests/Identicon.php</file>
<file>./../plugins/identity_select/tests/IdentitySelect.php</file>
<file>./../plugins/jqueryui/tests/Jqueryui.php</file>
<file>./../plugins/krb_authentication/tests/KrbAuthentication.php</file>
<file>./../plugins/managesieve/tests/Managesieve.php</file>
<file>./../plugins/managesieve/tests/Parser.php</file>
<file>./../plugins/managesieve/tests/Tokenizer.php</file>
<file>./../plugins/managesieve/tests/Vacation.php</file>
<file>./../plugins/markasjunk/tests/Markasjunk.php</file>
<file>./../plugins/new_user_dialog/tests/NewUserDialog.php</file>
<file>./../plugins/new_user_identity/tests/NewUserIdentity.php</file>
<file>./../plugins/newmail_notifier/tests/NewmailNotifier.php</file>
<file>./../plugins/password/tests/Password.php</file>
<file>./../plugins/redundant_attachments/tests/RedundantAttachments.php</file>
<file>./../plugins/show_additional_headers/tests/ShowAdditionalHeaders.php</file>
<file>./../plugins/squirrelmail_usercopy/tests/Squirrelmail_usercopy.php</file>
<file>./../plugins/subscriptions_option/tests/SubscriptionsOption.php</file>
<file>./../plugins/userinfo/tests/Userinfo.php</file>
<file>./../plugins/vcard_attachments/tests/VcardAttachments.php</file>
<file>./../plugins/virtuser_file/tests/VirtuserFile.php</file>
<file>./../plugins/virtuser_query/tests/VirtuserQuery.php</file>
<file>./../plugins/zipdownload/tests/Zipdownload.php</file>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../program/lib</directory>
<directory suffix=".php">../program/include</directory>
<directory suffix=".php">../program/actions</directory>
</whitelist>
</filter>
</phpunit>

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 19, 11:24 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
435756
Default Alt Text
(282 KB)

Event Timeline