Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
284 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 8d0fc1c11..1122152b5 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,2607 +1,2618 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Application class providing core functions and holding |
| instances of all 'global' objects like db- and imap-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Application class of Roundcube Webmail
* implemented as singleton
*
* @package Webmail
*/
class rcmail extends rcube
{
/**
* Main tasks.
*
* @var array
*/
static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
/**
* Current task.
*
* @var string
*/
public $task;
/**
* Current action.
*
* @var string
*/
public $action = '';
public $comm_path = './';
public $filename = '';
+ public $default_skin;
+ public $login_error;
private $address_books = array();
private $action_map = array();
const ERROR_STORAGE = -2;
const ERROR_INVALID_REQUEST = 1;
const ERROR_INVALID_HOST = 2;
const ERROR_COOKIES_DISABLED = 3;
const ERROR_RATE_LIMIT = 4;
/**
* This implements the 'singleton' design pattern
*
* @param integer $mode Ignored rcube::get_instance() argument
* @param string $env Environment name to run (e.g. live, dev, test)
*
* @return rcmail The one and only instance
*/
static function get_instance($mode = 0, $env = '')
{
if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
// In cli-server mode env=test
if ($env === null && php_sapi_name() == 'cli-server') {
$env = 'test';
}
self::$instance = new rcmail($env);
// init AFTER object was linked with self::$instance
self::$instance->startup();
}
return self::$instance;
}
/**
* Initial startup function
* to register session, create database and imap connections
*/
protected function startup()
{
$this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
// set filename if not index.php
if (($basename = basename($_SERVER['SCRIPT_FILENAME'])) && $basename != 'index.php') {
$this->filename = $basename;
}
// load all configured plugins
$plugins = (array) $this->config->get('plugins', array());
$required_plugins = array('filesystem_attachments', 'jqueryui');
$this->plugins->load_plugins($plugins, $required_plugins);
// start session
$this->session_init();
// Remember default skin, before it's replaced by user prefs
$this->default_skin = $this->config->get('skin');
// create user object
$this->set_user(new rcube_user($_SESSION['user_id']));
// set task and action properties
$this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC));
$this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC));
// reset some session parameters when changing task
if ($this->task != 'utils') {
// we reset list page when switching to another task
// but only to the main task interface - empty action (#1489076, #1490116)
// this will prevent from unintentional page reset on cross-task requests
if ($this->session && $_SESSION['task'] != $this->task && empty($this->action)) {
$this->session->remove('page');
// set current task to session
$_SESSION['task'] = $this->task;
}
}
// init output class (not in CLI mode)
if (!empty($_REQUEST['_remote'])) {
$GLOBALS['OUTPUT'] = $this->json_init();
}
else if ($_SERVER['REMOTE_ADDR']) {
$GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
}
// run init method on all the plugins
$this->plugins->init($this, $this->task);
}
/**
* Setter for application task
*
* @param string $task Task to set
*/
public function set_task($task)
{
if (php_sapi_name() == 'cli') {
$task = 'cli';
}
else if (!$this->user || !$this->user->ID) {
$task = 'login';
}
else {
$task = asciiwords($task, true) ?: 'mail';
}
$this->task = $task;
$this->comm_path = $this->url(array('task' => $this->task));
if (!empty($_REQUEST['_framed'])) {
$this->comm_path .= '&_framed=1';
}
if ($this->output) {
$this->output->set_env('task', $this->task);
$this->output->set_env('comm_path', $this->comm_path);
}
}
/**
* Setter for system user object
*
* @param rcube_user $user Current user instance
*/
public function set_user($user)
{
parent::set_user($user);
$lang = $this->language_prop($this->config->get('language', $_SESSION['language']));
$_SESSION['language'] = $this->user->language = $lang;
// set localization
setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8');
// Workaround for http://bugs.php.net/bug.php?id=18556
// Also strtoupper/strtolower and other methods are locale-aware
// for these locales it is problematic (#1490519)
if (in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8', 'C');
}
}
/**
* Return instance of the internal address book class
*
* @param string $id Address book identifier (-1 for default addressbook)
* @param boolean $writeable True if the address book needs to be writeable
*
* @return rcube_contacts Address book object
*/
public function get_address_book($id, $writeable = false)
{
$contacts = null;
$ldap_config = (array)$this->config->get('ldap_public');
+ $default = false;
// 'sql' is the alias for '0' used by autocomplete
- if ($id == 'sql')
+ if ($id == 'sql') {
$id = '0';
+ }
else if ($id == -1) {
$id = $this->config->get('default_addressbook');
$default = true;
}
// use existing instance
if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
$contacts = $this->address_books[$id];
}
else if ($id && $ldap_config[$id]) {
$domain = $this->config->mail_domain($_SESSION['storage_host']);
$contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $domain);
}
else if ($id === '0') {
$contacts = new rcube_contacts($this->db, $this->get_user_id());
}
else {
$plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
// plugin returned instance of a rcube_addressbook
if ($plugin['instance'] instanceof rcube_addressbook) {
$contacts = $plugin['instance'];
}
}
// when user requested default writeable addressbook
// we need to check if default is writeable, if not we
// will return first writeable book (if any exist)
if ($contacts && $default && $contacts->readonly && $writeable) {
$contacts = null;
}
// Get first addressbook from the list if configured default doesn't exist
// This can happen when user deleted the addressbook (e.g. Kolab folder)
if (!$contacts && (!$id || $default)) {
$source = reset($this->get_address_sources($writeable, !$default));
if (!empty($source)) {
$contacts = $this->get_address_book($source['id']);
if ($contacts) {
$id = $source['id'];
}
}
}
if (!$contacts) {
// there's no default, just return
if ($default) {
return null;
}
self::raise_error(array(
'code' => 700,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Addressbook source ($id) not found!"
),
true, true);
}
// add to the 'books' array for shutdown function
$this->address_books[$id] = $contacts;
if ($writeable && $contacts->readonly) {
return null;
}
// set configured sort order
if ($sort_col = $this->config->get('addressbook_sort_col')) {
$contacts->set_sort_order($sort_col);
}
return $contacts;
}
/**
* Return identifier of the address book object
*
* @param rcube_addressbook $object Addressbook source object
*
* @return string Source identifier
*/
public function get_address_book_id($object)
{
foreach ($this->address_books as $index => $book) {
if ($book === $object) {
return $index;
}
}
}
/**
* Return address books list
*
* @param boolean $writeable True if the address book needs to be writeable
* @param boolean $skip_hidden True if the address book needs to be not hidden
*
* @return array Address books array
*/
public function get_address_sources($writeable = false, $skip_hidden = false)
{
$abook_type = strtolower((string) $this->config->get('address_book_type', 'sql'));
$ldap_config = (array) $this->config->get('ldap_public');
$autocomplete = (array) $this->config->get('autocomplete_addressbooks');
$list = array();
// SQL-based (built-in) address book
if ($abook_type === 'sql') {
if (!isset($this->address_books['0'])) {
$this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
}
$list['0'] = array(
'id' => '0',
'name' => $this->gettext('personaladrbook'),
'groups' => $this->address_books['0']->groups,
'readonly' => $this->address_books['0']->readonly,
'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
'autocomplete' => in_array_nocase('sql', $autocomplete),
);
}
// LDAP address book(s)
if (!empty($ldap_config)) {
foreach ($ldap_config as $id => $prop) {
// handle misconfiguration
if (empty($prop) || !is_array($prop)) {
continue;
}
$list[$id] = array(
'id' => $id,
'name' => html::quote($prop['name']),
'groups' => !empty($prop['groups']) || !empty($prop['group_filters']),
'readonly' => !$prop['writable'],
'hidden' => $prop['hidden'],
'autocomplete' => in_array($id, $autocomplete)
);
}
}
// Plugins can also add address books, or re-order the list
$plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
$list = $plugin['sources'];
foreach ($list as $idx => $item) {
// register source for shutdown function
if (!is_object($this->address_books[$item['id']])) {
$this->address_books[$item['id']] = $item;
}
// remove from list if not writeable as requested
if ($writeable && $item['readonly']) {
unset($list[$idx]);
}
// remove from list if hidden as requested
else if ($skip_hidden && $item['hidden']) {
unset($list[$idx]);
}
}
return $list;
}
/**
* Getter for compose responses.
* These are stored in local config and user preferences.
*
* @param boolean $sorted True to sort the list alphabetically
* @param boolean $user_only True if only this user's responses shall be listed
*
* @return array List of the current user's stored responses
*/
public function get_compose_responses($sorted = false, $user_only = false)
{
$responses = array();
if (!$user_only) {
foreach ($this->config->get('compose_responses_static', array()) as $response) {
if (empty($response['key'])) {
$response['key'] = substr(md5($response['name']), 0, 16);
}
$response['static'] = true;
$response['class'] = 'readonly';
$k = $sorted ? '0000-' . mb_strtolower($response['name']) : $response['key'];
$responses[$k] = $response;
}
}
foreach ($this->config->get('compose_responses', array()) as $response) {
if (empty($response['key'])) {
$response['key'] = substr(md5($response['name']), 0, 16);
}
$k = $sorted ? mb_strtolower($response['name']) : $response['key'];
$responses[$k] = $response;
}
// sort list by name
if ($sorted) {
ksort($responses, SORT_LOCALE_STRING);
}
$responses = array_values($responses);
$hook = $this->plugins->exec_hook('get_compose_responses', array(
'list' => $responses,
'sorted' => $sorted,
'user_only' => $user_only,
));
return $hook['list'];
}
/**
* Init output object for GUI and add common scripts.
* This will instantiate a rcmail_output_html object and set
* environment vars according to the current session and configuration
*
* @param boolean $framed True if this request is loaded in a (i)frame
*
* @return rcube_output Reference to HTML output object
*/
public function load_gui($framed = false)
{
// init output page
if (!($this->output instanceof rcmail_output_html)) {
$this->output = new rcmail_output_html($this->task, $framed);
}
// set refresh interval
$this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0));
$this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60);
if ($framed) {
$this->comm_path .= '&_framed=1';
$this->output->set_env('framed', true);
}
$this->output->set_env('task', $this->task);
$this->output->set_env('action', $this->action);
$this->output->set_env('comm_path', $this->comm_path);
$this->output->set_charset(RCUBE_CHARSET);
if ($this->user && $this->user->ID) {
$this->output->set_env('user_id', $this->user->get_hash());
}
// set compose mode for all tasks (message compose step can be triggered from everywhere)
$this->output->set_env('compose_extwin', $this->config->get('compose_extwin',false));
// add some basic labels to client
$this->output->add_label('loading', 'servererror', 'connerror', 'requesttimedout',
'refreshing', 'windowopenerror', 'uploadingmany', 'uploading', 'close', 'save', 'cancel',
'alerttitle', 'confirmationtitle', 'delete', 'continue', 'ok');
return $this->output;
}
/**
* Create an output object for JSON responses
*
* @return rcube_output Reference to JSON output object
*/
public function json_init()
{
if (!($this->output instanceof rcmail_output_json)) {
$this->output = new rcmail_output_json($this->task);
}
return $this->output;
}
/**
* Create session object and start the session.
*/
public function session_init()
{
parent::session_init();
// set initial session vars
if (!$_SESSION['user_id']) {
$_SESSION['temp'] = true;
}
}
/**
* Perform login to the mail server and to the webmail service.
* This will also create a new user entry if auto_create_user is configured.
*
* @param string $username Mail storage (IMAP) user name
* @param string $password Mail storage (IMAP) password
* @param string $host Mail storage (IMAP) host
* @param bool $cookiecheck Enables cookie check
*
* @return boolean True on success, False on failure
*/
function login($username, $password, $host = null, $cookiecheck = false)
{
$this->login_error = null;
if (empty($username)) {
return false;
}
if ($cookiecheck && empty($_COOKIE)) {
$this->login_error = self::ERROR_COOKIES_DISABLED;
return false;
}
$username_filter = $this->config->get('login_username_filter');
$username_maxlen = $this->config->get('login_username_maxlen', 1024);
$password_maxlen = $this->config->get('login_password_maxlen', 1024);
$default_host = $this->config->get('default_host');
$default_port = $this->config->get('default_port');
$username_domain = $this->config->get('username_domain');
$login_lc = $this->config->get('login_lc', 2);
// check input for security (#1490500)
if (($username_maxlen && strlen($username) > $username_maxlen)
|| ($username_filter && !preg_match($username_filter, $username))
|| ($password_maxlen && strlen($password) > $password_maxlen)
) {
$this->login_error = self::ERROR_INVALID_REQUEST;
return false;
}
// host is validated in rcmail::autoselect_host(), so here
// we'll only handle unset host (if possible)
if (!$host && !empty($default_host)) {
if (is_array($default_host)) {
$key = key($default_host);
$host = is_numeric($key) ? $default_host[$key] : $key;
}
else {
$host = $default_host;
}
$host = rcube_utils::parse_host($host);
}
if (!$host) {
$this->login_error = self::ERROR_INVALID_HOST;
return false;
}
// parse $host URL
$a_host = parse_url($host);
- if ($a_host['host']) {
+ $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']))
+ if (!empty($a_host['port'])) {
$port = $a_host['port'];
- else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143))
+ }
+ else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) {
$port = 993;
+ }
}
- if (!$port) {
+ 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;
}
}
/**
* 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));
}
$pre = array();
$task = $p['_task'] ?: ($p['task'] ?: $this->task);
$pre['_task'] = $task;
unset($p['task'], $p['_task']);
$url = $this->filename;
$delm = '?';
foreach (array_merge($pre, $p) as $key => $val) {
if ($val !== '' && $val !== null) {
$par = $key[0] == '_' ? $key : '_'.$key;
$url .= $delm.urlencode($par).'='.urlencode($val);
$delm = '&';
}
}
$base_path = strval($_SERVER['REDIRECT_SCRIPT_URL'] ?: $_SERVER['SCRIPT_NAME']);
$base_path = preg_replace('![^/]+$!', '', $base_path);
if ($secure && ($token = $this->get_secure_url_token(true))) {
// add token to the url
$url = $token . '/' . $url;
// remove old token from the path
$base_path = rtrim($base_path, '/');
$base_path = preg_replace('/\/[a-zA-Z0-9]{' . strlen($token) . '}$/', '', $base_path);
// this need to be full url to make redirects work
$absolute = true;
}
else if ($secure && ($token = $this->get_request_token()))
$url .= $delm . '_token=' . urlencode($token);
if ($absolute || $full) {
// add base path to this Roundcube installation
if ($base_path == '') $base_path = '/';
$prefix = $base_path;
// prepend protocol://hostname:port
if ($full) {
$prefix = rcube_utils::resolve_url($prefix);
}
$prefix = rtrim($prefix, '/') . '/';
}
else {
$prefix = './';
}
return $prefix . $url;
}
/**
* Function to be executed in script shutdown
*/
public function shutdown()
{
parent::shutdown();
foreach ($this->address_books as $book) {
if (is_object($book) && is_a($book, 'rcube_addressbook')) {
$book->close();
}
}
// write performance stats to logs/console
if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) {
// we have to disable per_user_logging to make sure stats end up in the main console log
$this->config->set('per_user_logging', false);
// make sure logged numbers use unified format
setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C');
if (function_exists('memory_get_usage')) {
$mem = round(memory_get_usage() / 1024 /1024, 1);
- }
- if (function_exists('memory_get_peak_usage')) {
- $mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
+
+ if (function_exists('memory_get_peak_usage')) {
+ $mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
+ }
}
- $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
+ $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);
}
}
/**
* Registers action aliases for current task
*
* @param array $map Alias-to-filename hash array
*/
public function register_action_map($map)
{
if (is_array($map)) {
foreach ($map as $idx => $val) {
$this->action_map[$idx] = $val;
}
}
}
/**
* Returns current action filename
*
* @param array $map Alias-to-filename hash array
*/
public function get_action_file()
{
if (!empty($this->action_map[$this->action])) {
return $this->action_map[$this->action];
}
return strtr($this->action, '-', '_') . '.inc';
}
/**
* Fixes some user preferences according to namespace handling change.
* Old Roundcube versions were using folder names with removed namespace prefix.
* Now we need to add the prefix on servers where personal namespace has prefix.
*
* @param rcube_user $user User object
*/
private function fix_namespace_settings($user)
{
$prefix = $this->storage->get_namespace('prefix');
$prefix_len = strlen($prefix);
if (!$prefix_len) {
return;
}
if ($this->config->get('namespace_fixed')) {
return;
}
$prefs = array();
// Build namespace prefix regexp
$ns = $this->storage->get_namespace();
$regexp = array();
foreach ($ns as $entry) {
if (!empty($entry)) {
foreach ($entry as $item) {
if (strlen($item[0])) {
$regexp[] = preg_quote($item[0], '/');
}
}
}
}
$regexp = '/^('. implode('|', $regexp).')/';
// Fix preferences
$opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
foreach ($opts as $opt) {
if ($value = $this->config->get($opt)) {
if ($value != 'INBOX' && !preg_match($regexp, $value)) {
$prefs[$opt] = $prefix.$value;
}
}
}
if (($search_mods = $this->config->get('search_mods')) && !empty($search_mods)) {
$folders = array();
foreach ($search_mods as $idx => $value) {
if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$idx] = $value;
}
$prefs['search_mods'] = $folders;
}
if (($threading = $this->config->get('message_threading')) && !empty($threading)) {
$folders = array();
foreach ($threading as $idx => $value) {
if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$prefix.$idx] = $value;
}
$prefs['message_threading'] = $folders;
}
if ($collapsed = $this->config->get('collapsed_folders')) {
$folders = explode('&&', $collapsed);
$count = count($folders);
$folders_str = '';
if ($count) {
$folders[0] = substr($folders[0], 1);
$folders[$count-1] = substr($folders[$count-1], 0, -1);
}
foreach ($folders as $value) {
if ($value != 'INBOX' && !preg_match($regexp, $value)) {
$value = $prefix.$value;
}
$folders_str .= '&'.$value.'&';
}
$prefs['collapsed_folders'] = $folders_str;
}
$prefs['namespace_fixed'] = true;
// save updated preferences and reset imap settings (default folders)
$user->save_prefs($prefs);
$this->set_storage_prop();
}
/**
* Overwrite action variable
*
* @param string $action New action value
*/
public function overwrite_action($action)
{
$this->action = $action;
$this->output->set_env('action', $action);
}
/**
* Set environment variables for specified config options
*
* @param array $options List of configuration option names
*/
public function set_env_config($options)
{
foreach ((array) $options as $option) {
if ($this->config->get($option)) {
$this->output->set_env($option, true);
}
}
}
/**
* Returns RFC2822 formatted current date in user's timezone
*
* @return string Date
*/
public function user_date()
{
// get user's timezone
try {
$tz = new DateTimeZone($this->config->get('timezone'));
$date = new DateTime('now', $tz);
}
catch (Exception $e) {
$date = new DateTime();
}
return $date->format('r');
}
/**
* Write login data (name, ID, IP address) to the 'userlogins' log file.
*/
public function log_login($user = null, $failed_login = false, $error_code = 0)
{
if (!$this->config->get('log_logins')) {
return;
}
// don't log full session id for security reasons
$session_id = session_id();
$session_id = $session_id ? substr($session_id, 0, 16) : 'no-session';
// failed login
if ($failed_login) {
// don't fill the log with complete input, which could
// have been prepared by a hacker
if (strlen($user) > 256) {
$user = substr($user, 0, 256) . '...';
}
$message = sprintf('Failed login for %s from %s in session %s (error: %d)',
$user, rcube_utils::remote_ip(), $session_id, $error_code);
}
// successful login
else {
$user_name = $this->get_user_name();
$user_id = $this->get_user_id();
if (!$user_id) {
return;
}
$message = sprintf('Successful login for %s (ID: %d) from %s in session %s',
$user_name, $user_id, rcube_utils::remote_ip(), $session_id);
}
// log login
self::write_log('userlogins', $message);
}
/**
* Check if specified asset file exists
*
* @param string $path Asset path
* @param bool $minified Fallback to minified version of the file
*
* @return string Asset path if found (modified if minified file found)
*/
public function find_asset($path, $minified = true)
{
if (empty($path)) {
return;
}
$assets_dir = $this->config->get('assets_dir');
$root_path = unslashify($assets_dir ?: INSTALL_PATH) . '/';
$full_path = $root_path . trim($path, '/');
if (file_exists($full_path)) {
return $path;
}
if ($minified && preg_match('/(?<!\.min)\.(js|css)$/', $path)) {
$path = preg_replace('/\.(js|css)$/', '.min.\\1', $path);
$full_path = $root_path . trim($path, '/');
if (file_exists($full_path)) {
return $path;
}
}
}
/**
* Create a HTML table based on the given data
*
* @param array $attrib Named table attributes
* @param mixed $table_data Table row data. Either a two-dimensional array
* or a valid SQL result set
* @param array $show_cols List of cols to show
* @param string $id_col Name of the identifier col
*
* @return string HTML table code
*/
public function table_output($attrib, $table_data, $show_cols, $id_col)
{
$table = new html_table($attrib);
// add table header
if (!$attrib['noheader']) {
foreach ($show_cols as $col) {
$table->add_header($col, $this->Q($this->gettext($col)));
}
}
if (!is_array($table_data)) {
$db = $this->get_dbh();
while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
$table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
// format each col
foreach ($show_cols as $col) {
$table->add($col, $this->Q($sql_arr[$col]));
}
}
}
else {
foreach ($table_data as $row_data) {
$class = !empty($row_data['class']) ? $row_data['class'] : null;
if (!empty($attrib['rowclass']))
$class = trim($class . ' ' . $attrib['rowclass']);
$rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
$table->add_row(array('id' => $rowid, 'class' => $class));
// format each col
foreach ($show_cols as $col) {
$val = is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col];
$table->add($col, empty($attrib['ishtml']) ? $this->Q($val) : $val);
}
}
}
return $table->show($attrib);
}
/**
* Convert the given date to a human readable form
* This uses the date formatting properties from config
*
* @param mixed $date Date representation (string, timestamp or DateTime object)
* @param string $format Date format to use
* @param bool $convert Enables date conversion according to user timezone
*
* @return string Formatted date string
*/
public function format_date($date, $format = null, $convert = true)
{
if (is_object($date) && is_a($date, 'DateTime')) {
$timestamp = $date->format('U');
}
else {
if (!empty($date)) {
$timestamp = rcube_utils::strtotime($date);
}
if (empty($timestamp)) {
return '';
}
try {
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return '';
}
}
if ($convert) {
try {
// convert to the right timezone
$stz = date_default_timezone_get();
$tz = new DateTimeZone($this->config->get('timezone'));
$date->setTimezone($tz);
date_default_timezone_set($tz->getName());
$timestamp = $date->format('U');
}
catch (Exception $e) {
}
}
// define date format depending on current time
if (!$format) {
$now = time();
$now_date = getdate($now);
$today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
$week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
$pretty_date = $this->config->get('prettydate');
if ($pretty_date && $timestamp > $today_limit && $timestamp <= $now) {
$format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
$today = true;
}
else if ($pretty_date && $timestamp > $week_limit && $timestamp <= $now) {
$format = $this->config->get('date_short', 'D H:i');
}
else {
$format = $this->config->get('date_long', 'Y-m-d H:i');
}
}
// strftime() format
if (preg_match('/%[a-z]+/i', $format)) {
$format = strftime($format, $timestamp);
- if ($stz) {
+
+ if (isset($stz)) {
date_default_timezone_set($stz);
}
- return $today ? ($this->gettext('today') . ' ' . $format) : $format;
+
+ 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 ($today) {
+ 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 ($stz) {
+ 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
*/
public function folder_list($attrib)
{
static $a_mailboxes;
$attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
$type = $attrib['type'] ? $attrib['type'] : 'ul';
unset($attrib['type']);
if ($type == 'ul' && !$attrib['id']) {
$attrib['id'] = 'rcmboxlist';
}
if (empty($attrib['folder_name'])) {
$attrib['folder_name'] = '*';
}
// get current folder
$storage = $this->get_storage();
$mbox_name = $storage->get_folder();
+ $delimiter = $storage->get_hierarchy_delimiter();
// build the folders tree
if (empty($a_mailboxes)) {
// get mailbox list
$a_folders = $storage->list_folders_subscribed(
'', $attrib['folder_name'], $attrib['folder_filter']);
- $delimiter = $storage->get_hierarchy_delimiter();
$a_mailboxes = array();
foreach ($a_folders as $folder) {
$this->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $this->plugins->exec_hook('render_mailboxlist', array(
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'type' => $type,
'attribs' => $attrib,
));
$a_mailboxes = $hook['list'];
$attrib = $hook['attribs'];
if ($type == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
// add no-selection option
if ($attrib['noselection']) {
$select->add(html::quote($this->gettext($attrib['noselection'])), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
$out = $select->show($attrib['default']);
}
else {
+ $out = '';
$js_mailboxlist = array();
$tree = $this->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
if ($type != 'js') {
$out = html::tag('ul', $attrib, $tree, html::$common_attrib);
$this->output->include_script('treelist.js');
$this->output->add_gui_object('mailboxlist', $attrib['id']);
$this->output->set_env('unreadwrap', $attrib['unreadwrap']);
$this->output->set_env('collapsed_folders', (string) $this->config->get('collapsed_folders'));
}
$this->output->set_env('mailboxes', $js_mailboxlist);
// we can't use object keys in javascript because they are unordered
// we need sorted folders list for folder-selector widget
$this->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
}
// add some labels to client
$this->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
return $out;
}
/**
* Return folders list as html_select object
*
* @param array $p Named parameters
*
* @return html_select HTML drop-down object
*/
public function folder_selector($p = array())
{
$realnames = $this->config->get('show_real_foldernames');
$p += array('maxlength' => 100, 'realnames' => $realnames, 'is_escaped' => true);
$a_mailboxes = array();
$storage = $this->get_storage();
if (empty($p['folder_name'])) {
$p['folder_name'] = '*';
}
if ($p['unsubscribed']) {
$list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
else {
$list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
$delimiter = $storage->get_hierarchy_delimiter();
if (!empty($p['exceptions'])) {
$list = array_diff($list, (array) $p['exceptions']);
}
if (!empty($p['additional'])) {
foreach ($p['additional'] as $add_folder) {
$add_items = explode($delimiter, $add_folder);
$folder = '';
while (count($add_items)) {
$folder .= array_shift($add_items);
// @TODO: sorting
if (!in_array($folder, $list)) {
$list[] = $folder;
}
$folder .= $delimiter;
}
}
}
foreach ($list as $folder) {
$this->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $this->plugins->exec_hook('render_folder_selector', array(
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'attribs' => $p,
));
$a_mailboxes = $hook['list'];
$p = $hook['attribs'];
$select = new html_select($p);
if ($p['noselection']) {
$select->add(html::quote($p['noselection']), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
return $select;
}
/**
* Create a hierarchical array of the mailbox list
*/
public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
{
// Handle namespace prefix
$prefix = '';
if (!$path) {
$n_folder = $folder;
$folder = $this->storage->mod_folder($folder);
if ($n_folder != $folder) {
$prefix = substr($n_folder, 0, -strlen($folder));
}
}
$pos = strpos($folder, $delm);
if ($pos !== false) {
$subFolders = substr($folder, $pos+1);
$currentFolder = substr($folder, 0, $pos);
// sometimes folder has a delimiter as the last character
if (!strlen($subFolders)) {
$virtual = false;
}
else if (!isset($arrFolders[$currentFolder])) {
$virtual = true;
}
else {
$virtual = $arrFolders[$currentFolder]['virtual'];
}
}
else {
$subFolders = false;
$currentFolder = $folder;
$virtual = false;
}
$path .= $prefix . $currentFolder;
if (!isset($arrFolders[$currentFolder])) {
$arrFolders[$currentFolder] = array(
'id' => $path,
'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
'virtual' => $virtual,
'folders' => array()
);
}
else {
$arrFolders[$currentFolder]['virtual'] = $virtual;
}
if (strlen($subFolders)) {
$this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
}
}
/**
* Return html for a structured list &lt;ul&gt; for the mailbox tree
*/
public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
{
$maxlength = intval($attrib['maxlength']);
$realnames = (bool)$attrib['realnames'];
$msgcounts = $this->storage->get_cache('messagecount');
$collapsed = $this->config->get('collapsed_folders');
$realnames = $this->config->get('show_real_foldernames');
$out = '';
foreach ($arrFolders as $folder) {
$title = null;
$folder_class = $this->folder_classname($folder['id']);
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
if ($folder_class && !$realnames && $this->text_exists($folder_class)) {
$foldername = $this->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$fname = abbreviate_string($foldername, $maxlength);
if ($fname != $foldername) {
$title = $foldername;
}
$foldername = $fname;
}
}
// make folder name safe for ids and class names
$folder_id = rcube_utils::html_identifier($folder['id'], true);
$classes = array('mailbox');
// set special class for Sent, Drafts, Trash and Junk
if ($folder_class) {
$classes[] = $folder_class;
}
if ($folder['id'] == $mbox_name) {
$classes[] = 'selected';
}
if ($folder['virtual']) {
$classes[] = 'virtual';
}
else if ($unread) {
$classes[] = 'unread';
}
$js_name = $this->JQ($folder['id']);
$html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount skip-content', sprintf($attrib['unreadwrap'], $unread)) : '');
$link_attrib = $folder['virtual'] ? array() : array(
'href' => $this->url(array('_mbox' => $folder['id'])),
'onclick' => sprintf("return %s.command('list','%s',this,event)", rcmail_output::JS_OBJECT_NAME, $js_name),
'rel' => $folder['id'],
'title' => $title,
);
$out .= html::tag('li', array(
'id' => "rcmli" . $folder_id,
'class' => implode(' ', $classes),
'noclose' => true
),
html::a($link_attrib, $html_name));
if (!empty($folder['folders'])) {
$out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
}
$jslist[$folder['id']] = array(
'id' => $folder['id'],
'name' => $foldername,
'virtual' => $folder['virtual'],
);
if (!empty($folder_class)) {
$jslist[$folder['id']]['class'] = $folder_class;
}
if (!empty($folder['folders'])) {
$out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
$this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
}
$out .= "</li>\n";
}
return $out;
}
/**
* Return html for a flat list <select> for the mailbox tree
*/
public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
{
$out = '';
foreach ($arrFolders as $folder) {
// skip exceptions (and its subfolders)
if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
continue;
}
// skip folders in which it isn't possible to create subfolders
if (!empty($opts['skip_noinferiors'])) {
$attrs = $this->storage->folder_attributes($folder['id']);
if ($attrs && in_array_nocase('\\Noinferiors', $attrs)) {
continue;
}
}
if (!$realnames && ($folder_class = $this->folder_classname($folder['id'])) && $this->text_exists($folder_class)) {
$foldername = $this->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$foldername = abbreviate_string($foldername, $maxlength);
}
}
$select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
if (!empty($folder['folders'])) {
$out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
$select, $realnames, $nestLevel+1, $opts);
}
}
return $out;
}
/**
* 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
*/
public function folder_classname($folder_id)
{
static $classes;
if ($classes === null) {
$classes = array('INBOX' => 'inbox');
// for these mailboxes we have css classes
foreach (array('sent', 'drafts', 'trash', 'junk') as $type) {
if (($mbox = $this->config->get($type . '_mbox')) && !isset($classes[$mbox])) {
$classes[$mbox] = $type;
}
}
$storage = $this->get_storage();
// add classes for shared/other user namespace roots
foreach (array('other', 'shared') as $ns_name) {
if ($ns = $storage->get_namespace($ns_name)) {
foreach ($ns as $root) {
$root = substr($root[0], 0, -1);
if (strlen($root) && !isset($classes[$root])) {
$classes[$root] = "ns-$ns_name";
}
}
}
}
}
return $classes[$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
*/
public function localize_foldername($name, $with_path = false, $path_remove = false)
{
$realnames = $this->config->get('show_real_foldernames');
if (!$realnames && ($folder_class = $this->folder_classname($name)) && $this->text_exists($folder_class)) {
return $this->gettext($folder_class);
}
$storage = $this->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
// Remove the path
if ($path_remove) {
if (strpos($name, $delimiter)) {
$path = explode($delimiter, $name);
$name = array_pop($path);
}
}
// try to localize path of the folder
else if ($with_path && !$realnames) {
$path = explode($delimiter, $name);
$count = count($path);
if ($count > 1) {
for ($i = 1; $i < $count; $i++) {
$folder = implode($delimiter, array_slice($path, 0, -$i));
$folder_class = $this->folder_classname($folder);
if ($folder_class && $this->text_exists($folder_class)) {
$name = implode($delimiter, array_slice($path, $count - $i));
$name = rcube_charset::convert($name, 'UTF7-IMAP');
return $this->gettext($folder_class) . $delimiter . $name;
}
}
}
}
return rcube_charset::convert($name, 'UTF7-IMAP');
}
/**
* Localize folder path
*/
public function localize_folderpath($path)
{
$protect_folders = $this->config->get('protect_default_folders');
$delimiter = $this->storage->get_hierarchy_delimiter();
$path = explode($delimiter, $path);
$result = array();
foreach ($path as $idx => $dir) {
$directory = implode($delimiter, array_slice($path, 0, $idx+1));
if ($protect_folders && $this->storage->is_special_folder($directory)) {
unset($result);
$result[] = $this->localize_foldername($directory);
}
else {
$result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
}
}
return implode($delimiter, $result);
}
/**
* Return HTML for quota indicator object
*
* @param array $attrib Named parameters
*
* @return string HTML code for the quota indicator object
*/
public static function quota_display($attrib)
{
$rcmail = rcmail::get_instance();
if (!$attrib['id']) {
$attrib['id'] = 'rcmquotadisplay';
}
$_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
$rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
$quota = $rcmail->quota_content($attrib);
$rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
return html::span($attrib, '&nbsp;');
}
/**
* Return (parsed) quota information
*
* @param array $attrib Named parameters
* @param array $folder Current folder
*
* @return array Quota information
*/
public function quota_content($attrib = null, $folder = null)
{
$quota = $this->storage->get_quota($folder);
$quota = $this->plugins->exec_hook('quota', $quota);
$quota_result = (array) $quota;
$quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
$quota_result['folder'] = $folder !== null && $folder !== '' ? $folder : 'INBOX';
if ($quota['total'] > 0) {
if (!isset($quota['percent'])) {
$quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
}
$title = $this->gettext('quota') . ': ' . sprintf('%s / %s (%.0f%%)',
$this->show_bytes($quota['used'] * 1024),
$this->show_bytes($quota['total'] * 1024),
$quota_result['percent']
);
$quota_result['title'] = $title;
if ($attrib['width']) {
$quota_result['width'] = $attrib['width'];
}
if ($attrib['height']) {
$quota_result['height'] = $attrib['height'];
}
// build a table of quota types/roots info
if (($root_cnt = count($quota_result['all'])) > 1 || count($quota_result['all'][key($quota_result['all'])]) > 1) {
$table = new html_table(array('cols' => 3, 'class' => 'quota-info'));
$table->add_header(null, self::Q($this->gettext('quotatype')));
$table->add_header(null, self::Q($this->gettext('quotatotal')));
$table->add_header(null, self::Q($this->gettext('quotaused')));
foreach ($quota_result['all'] as $root => $data) {
if ($root_cnt > 1 && $root) {
$table->add(array('colspan' => 3, 'class' => 'root'), self::Q($root));
}
if ($storage = $data['storage']) {
$percent = min(100, round(($storage['used']/max(1,$storage['total']))*100));
$table->add('name', self::Q($this->gettext('quotastorage')));
$table->add(null, $this->show_bytes($storage['total'] * 1024));
$table->add(null, sprintf('%s (%.0f%%)', $this->show_bytes($storage['used'] * 1024), $percent));
}
if ($message = $data['message']) {
$percent = min(100, round(($message['used']/max(1,$message['total']))*100));
$table->add('name', self::Q($this->gettext('quotamessage')));
$table->add(null, intval($message['total']));
$table->add(null, sprintf('%d (%.0f%%)', $message['used'], $percent));
}
}
$quota_result['table'] = $table->show();
}
}
else {
$unlimited = $this->config->get('quota_zero_as_unlimited');
$quota_result['title'] = $this->gettext($unlimited ? 'unlimited' : 'unknown');
$quota_result['percent'] = 0;
}
// cleanup
unset($quota_result['abort']);
if (empty($quota_result['table'])) {
unset($quota_result['all']);
}
return $quota_result;
}
/**
* Outputs error message according to server error/response codes
*
* @param string $fallback Fallback message label
* @param array $fallback_args Fallback message label arguments
* @param string $suffix Message label suffix
* @param array $params Additional parameters (type, prefix)
*/
public function display_server_error($fallback = null, $fallback_args = null, $suffix = '', $params = array())
{
$err_code = $this->storage->get_error_code();
$res_code = $this->storage->get_response_code();
$args = array();
if ($res_code == rcube_storage::NOPERM) {
$error = 'errornoperm';
}
else if ($res_code == rcube_storage::READONLY) {
$error = 'errorreadonly';
}
else if ($res_code == rcube_storage::OVERQUOTA) {
$error = 'erroroverquota';
}
else if ($err_code && ($err_str = $this->storage->get_error_str())) {
// try to detect access rights problem and display appropriate message
if (stripos($err_str, 'Permission denied') !== false) {
$error = 'errornoperm';
}
// try to detect full mailbox problem and display appropriate message
// there can be e.g. "Quota exceeded" / "quotum would exceed" / "Over quota"
else if (stripos($err_str, 'quot') !== false && preg_match('/exceed|over/i', $err_str)) {
$error = 'erroroverquota';
}
else {
$error = 'servererrormsg';
$args = array('msg' => rcube::Q($err_str));
}
}
else if ($err_code < 0) {
$error = 'storageerror';
}
else if ($fallback) {
$error = $fallback;
$args = $fallback_args;
$params['prefix'] = false;
}
- if ($error) {
+ if (!empty($error)) {
if ($suffix && $this->text_exists($error . $suffix)) {
$error .= $suffix;
}
$msg = $this->gettext(array('name' => $error, 'vars' => $args));
if ($params['prefix'] && $fallback) {
$msg = $this->gettext(array('name' => $fallback, 'vars' => $fallback_args)) . ' ' . $msg;
}
$this->output->show_message($msg, $params['type'] ?: 'error');
}
}
/**
* Displays an error message on storage fatal errors
*/
public function storage_fatal_error()
{
$err_code = $this->storage->get_error_code();
switch ($err_code) {
// Not all are really fatal, but these should catch
// connection/authentication errors the best we can
case rcube_imap_generic::ERROR_NO:
case rcube_imap_generic::ERROR_BAD:
case rcube_imap_generic::ERROR_BYE:
$this->display_server_error();
}
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*/
public function html_editor($mode = '')
{
$spellcheck = intval($this->config->get('enable_spellcheck'));
$spelldict = intval($this->config->get('spellcheck_dictionary'));
$disabled_plugins = array();
$disabled_buttons = array();
$extra_plugins = array();
$extra_buttons = array();
if (!$spellcheck) {
$disabled_plugins[] = 'spellchecker';
}
$hook = $this->plugins->exec_hook('html_editor', array(
'mode' => $mode,
'disabled_plugins' => $disabled_plugins,
'disabled_buttons' => $disabled_buttons,
'extra_plugins' => $extra_plugins,
'extra_buttons' => $extra_buttons,
));
if ($hook['abort']) {
return;
}
$lang_codes = array($_SESSION['language']);
$assets_dir = $this->config->get('assets_dir') ?: INSTALL_PATH;
$skin_path = $this->output->get_skin_path();
if ($pos = strpos($_SESSION['language'], '_')) {
$lang_codes[] = substr($_SESSION['language'], 0, $pos);
}
foreach ($lang_codes as $code) {
if (file_exists("$assets_dir/program/js/tinymce/langs/$code.js")) {
$lang = $code;
break;
}
}
if (empty($lang)) {
$lang = 'en';
}
$config = array(
'mode' => $mode,
'lang' => $lang,
'skin_path' => $skin_path,
'spellcheck' => $spellcheck, // deprecated
'spelldict' => $spelldict,
'content_css' => 'program/resources/tinymce/content.css',
'disabled_plugins' => $hook['disabled_plugins'],
'disabled_buttons' => $hook['disabled_buttons'],
'extra_plugins' => $hook['extra_plugins'],
'extra_buttons' => $hook['extra_buttons'],
);
if ($path = $this->config->get('editor_css_location')) {
if ($path = $this->find_asset($skin_path . $path)) {
$config['content_css'] = $path;
}
}
$this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia', 'close');
$this->output->set_env('editor_config', $config);
if ($path = $this->config->get('media_browser_css_location', 'program/resources/tinymce/browser.css')) {
if ($path != 'none' && ($path = $this->find_asset($path))) {
$this->output->include_css($path);
}
}
$this->output->include_script('tinymce/tinymce.min.js');
$this->output->include_script('editor.js');
}
/**
* File upload progress handler.
*
* @deprecated We're using HTML5 upload progress
*/
public function upload_progress()
{
// NOOP
$this->output->send();
}
/**
* Initializes file uploading interface.
*
* @param int $max_size Optional maximum file size in bytes
*
* @return string Human-readable file size limit
*/
public function upload_init($max_size = null)
{
// find max filesize value
$max_filesize = rcube_utils::max_upload_size();
if ($max_size && $max_size < $max_filesize) {
$max_filesize = $max_size;
}
$max_filesize_txt = $this->show_bytes($max_filesize);
$this->output->set_env('max_filesize', $max_filesize);
$this->output->set_env('filesizeerror', $this->gettext(array(
'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize_txt))));
if ($max_filecount = ini_get('max_file_uploads')) {
$this->output->set_env('max_filecount', $max_filecount);
$this->output->set_env('filecounterror', $this->gettext(array(
'name' => 'filecounterror', 'vars' => array('count' => $max_filecount))));
}
$this->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B');
return $max_filesize_txt;
}
/**
* Upload form object
*
* @param array $attrib Object attributes
* @param string $name Form object name
* @param string $action Form action name
* @param array $input_attr File input attributes
* @param int $max_size Maximum upload size
*
* @return string HTML output
*/
public function upload_form($attrib, $name, $action, $input_attr = array(), $max_size = null)
{
// Get filesize, enable upload progress bar
$max_filesize = $this->upload_init($max_size);
$hint = html::div('hint', $this->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))));
if ($attrib['mode'] == 'hint') {
return $hint;
}
// set defaults
$attrib += array('id' => 'rcmUploadbox', 'buttons' => 'yes');
$event = rcmail_output::JS_OBJECT_NAME . ".command('$action', this.form)";
$form_id = $attrib['id'] . 'Frm';
// Default attributes of file input and form
$input_attr += array(
'id' => $attrib['id'] . 'Input',
'type' => 'file',
'name' => '_attachments[]',
);
$form_attr = array(
'id' => $form_id,
'name' => $name,
'method' => 'post',
'enctype' => 'multipart/form-data'
);
if ($attrib['mode'] == 'smart') {
unset($attrib['buttons']);
$form_attr['class'] = 'smart-upload';
$input_attr = array_merge($input_attr, array(
// #5854: Chrome does not execute onchange when selecting the same file.
// To fix this we reset the input using null value.
'onchange' => "$event; this.value=null",
'class' => 'smart-upload',
'tabindex' => '-1',
));
}
$input = new html_inputfield($input_attr);
$content = $attrib['prefix'] . $input->show();
if ($attrib['mode'] != 'smart') {
$content = html::div(null, $content . $hint);
}
if (rcube_utils::get_boolean($attrib['buttons'])) {
$button = new html_inputfield(array('type' => 'button'));
$content .= html::div('buttons',
$button->show($this->gettext('close'), array('class' => 'button', 'onclick' => "$('#{$attrib['id']}').hide()")) . ' ' .
$button->show($this->gettext('upload'), array('class' => 'button mainaction', 'onclick' => $event))
);
}
$this->output->add_gui_object($name, $form_id);
return html::div($attrib, $this->output->form_tag($form_attr, $content));
}
/**
* Outputs uploaded file content (with image thumbnails support
*
* @param array $file Upload file data
*/
public function display_uploaded_file($file)
{
if (empty($file)) {
return;
}
$file = $this->plugins->exec_hook('attachment_display', $file);
if ($file['status']) {
if (empty($file['size'])) {
$file['size'] = $file['data'] ? strlen($file['data']) : @filesize($file['path']);
}
// generate image thumbnail for file browser in HTML editor
if (!empty($_GET['_thumbnail'])) {
$thumbnail_size = 80;
$mimetype = $file['mimetype'];
$file_ident = $file['id'] . ':' . $file['mimetype'] . ':' . $file['size'];
$thumb_name = 'thumb' . md5($file_ident . ':' . $this->user->ID . ':' . $thumbnail_size);
$cache_file = rcube_utils::temp_filename($thumb_name, false, false);
// render thumbnail image if not done yet
if (!is_file($cache_file)) {
if (!$file['path']) {
$orig_name = $filename = $cache_file . '.tmp';
file_put_contents($orig_name, $file['data']);
}
else {
$filename = $file['path'];
}
$image = new rcube_image($filename);
if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) {
$mimetype = 'image/' . $imgtype;
- if ($orig_name) {
+ if (!empty($orig_name)) {
unlink($orig_name);
}
}
}
if (is_file($cache_file)) {
// cache for 1h
$this->output->future_expire_header(3600);
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($cache_file));
readfile($cache_file);
exit;
}
}
header('Content-Type: ' . $file['mimetype']);
header('Content-Length: ' . $file['size']);
if ($file['data']) {
echo $file['data'];
}
else if ($file['path']) {
readfile($file['path']);
}
}
}
/**
* Initializes client-side autocompletion.
*/
public function autocomplete_init()
{
static $init;
if ($init) {
return;
}
$init = 1;
if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
$book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
if (count($book_types) > 1) {
$this->output->set_env('autocomplete_threads', $threads);
$this->output->set_env('autocomplete_sources', $book_types);
}
}
$this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
$this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
$this->output->add_label('autocompletechars', 'autocompletemore');
}
/**
* Returns supported font-family specifications
*
* @param string $font Font name
*
* @param string|array Font-family specification array or string (if $font is used)
*/
public static function font_defs($font = null)
{
$fonts = array(
'Andale Mono' => '"Andale Mono",Times,monospace',
'Arial' => 'Arial,Helvetica,sans-serif',
'Arial Black' => '"Arial Black","Avant Garde",sans-serif',
'Book Antiqua' => '"Book Antiqua",Palatino,serif',
'Courier New' => '"Courier New",Courier,monospace',
'Georgia' => 'Georgia,Palatino,serif',
'Helvetica' => 'Helvetica,Arial,sans-serif',
'Impact' => 'Impact,Chicago,sans-serif',
'Tahoma' => 'Tahoma,Arial,Helvetica,sans-serif',
'Terminal' => 'Terminal,Monaco,monospace',
'Times New Roman' => '"Times New Roman",Times,serif',
'Trebuchet MS' => '"Trebuchet MS",Geneva,sans-serif',
'Verdana' => 'Verdana,Geneva,sans-serif',
);
if ($font) {
return $fonts[$font];
}
return $fonts;
}
/**
* Create a human readable string for a number of bytes
*
* @param int $bytes Number of bytes
* @param string &$unit Size unit
*
* @return string Byte string
*/
public function show_bytes($bytes, &$unit = null)
{
// Plugins may want to display different units
$plugin = $this->plugins->exec_hook('show_bytes', array('bytes' => $bytes));
$unit = $plugin['unit'];
if ($plugin['result'] !== null) {
return $plugin['result'];
}
if ($bytes >= 1073741824) {
$unit = 'GB';
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . $this->gettext($unit);
}
else if ($bytes >= 1048576) {
$unit = 'MB';
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . $this->gettext($unit);
}
else if ($bytes >= 1024) {
$unit = 'KB';
$str = sprintf("%d ", round($bytes/1024)) . $this->gettext($unit);
}
else {
$unit = 'B';
$str = sprintf('%d ', $bytes) . $this->gettext($unit);
}
return $str;
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_part $part Message part
*
* @return string Part size (and unit)
*/
public function message_part_size($part)
{
if (isset($part->d_parameters['size'])) {
$size = $this->show_bytes((int)$part->d_parameters['size']);
}
else {
$size = $part->size;
if ($size === 0) {
$part->exact_size = true;
}
if ($part->encoding == 'base64') {
$size = $size / 1.33;
}
$size = $this->show_bytes($size);
}
if (!$part->exact_size) {
$size = '~' . $size;
}
return $size;
}
/**
* Returns message UID(s) and IMAP folder(s) from GET/POST data
*
* @param string $uids UID value to decode
* @param string $mbox Default mailbox value (if not encoded in UIDs)
* @param bool $is_multifolder Will be set to True if multi-folder request
* @param int $mode Request mode. Default: rcube_utils::INPUT_GPC.
*
* @return array List of message UIDs per folder
*/
public static function get_uids($uids = null, $mbox = null, &$is_multifolder = false, $mode = null)
{
// message UID (or comma-separated list of IDs) is provided in
// the form of <ID>-<MBOX>[,<ID>-<MBOX>]*
$_uid = $uids ?: rcube_utils::get_input_value('_uid', $mode ?: rcube_utils::INPUT_GPC);
$_mbox = $mbox ?: (string) rcube_utils::get_input_value('_mbox', $mode ?: rcube_utils::INPUT_GPC);
// already a hash array
if (is_array($_uid) && !isset($_uid[0])) {
return $_uid;
}
$result = array();
// special case: *
if ($_uid == '*' && is_object($_SESSION['search'][1]) && $_SESSION['search'][1]->multi) {
$is_multifolder = true;
// extract the full list of UIDs per folder from the search set
foreach ($_SESSION['search'][1]->sets as $subset) {
$mbox = $subset->get_parameters('MAILBOX');
$result[$mbox] = $subset->get();
}
}
else {
if (is_string($_uid)) {
$_uid = explode(',', $_uid);
}
// create a per-folder UIDs array
foreach ((array)$_uid as $uid) {
list($uid, $mbox) = explode('-', $uid, 2);
if (!strlen($mbox)) {
$mbox = $_mbox;
}
else {
$is_multifolder = true;
}
if ($uid == '*') {
$result[$mbox] = $uid;
}
else if (preg_match('/^[0-9:.]+$/', $uid)) {
$result[$mbox][] = $uid;
}
}
}
return $result;
}
/**
* Get resource file content (with assets_dir support)
*
* @param string $name File name
*
* @return string File content
*/
public function get_resource_content($name)
{
if (!strpos($name, '/')) {
$name = "program/resources/$name";
}
$assets_dir = $this->config->get('assets_dir');
if ($assets_dir) {
$path = slashify($assets_dir) . $name;
if (@file_exists($path)) {
$name = $path;
}
}
return file_get_contents($name, false);
}
/**
* Converts HTML content into plain text
*
* @param string $html HTML content
* @param array $options Conversion parameters (width, links, charset)
*
* @return string Plain text
*/
public function html2text($html, $options = array())
{
$default_options = array(
'links' => true,
'width' => 75,
'body' => $html,
'charset' => RCUBE_CHARSET,
);
$options = array_merge($default_options, (array) $options);
// Plugins may want to modify HTML in another/additional way
$options = $this->plugins->exec_hook('html2text', $options);
// Convert to text
if (!$options['abort']) {
$converter = new rcube_html2text($options['body'],
false, $options['links'], $options['width'], $options['charset']);
$options['body'] = rtrim($converter->get_text());
}
return $options['body'];
}
/**
* Connect to the mail storage server with stored session data
*
* @return bool True on success, False on error
*/
public function storage_connect()
{
$storage = $this->get_storage();
if ($_SESSION['storage_host'] && !$storage->is_connected()) {
$host = $_SESSION['storage_host'];
$user = $_SESSION['username'];
$port = $_SESSION['storage_port'];
$ssl = $_SESSION['storage_ssl'];
$pass = $this->decrypt($_SESSION['password']);
if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
if (is_object($this->output)) {
$this->output->show_message('storageerror', 'error');
}
}
else {
$this->set_storage_prop();
}
}
return $storage->is_connected();
}
}
diff --git a/program/include/rcmail_install.php b/program/include/rcmail_install.php
index ebd23417d..8d80fb113 100644
--- a/program/include/rcmail_install.php
+++ b/program/include/rcmail_install.php
@@ -1,878 +1,879 @@
<?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: |
| Roundcube Installer |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class to control the installation process of the Roundcube Webmail package
*
* @category Install
* @package Webmail
*/
class rcmail_install
{
public $step;
public $last_error;
public $is_post = false;
public $failures = 0;
public $config = array();
+ public $defaults = array();
+ public $comments = array();
public $configured = false;
public $legacy_config = false;
public $email_pattern = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
public $bool_config_props = array();
public $local_config = array('db_dsnw', 'default_host', 'support_url', 'des_key', 'plugins');
public $obsolete_config = array('db_backend', 'db_max_length', 'double_auth', 'preview_pane', 'debug_level', 'referer_check');
public $replaced_config = array(
'skin_path' => 'skin',
'locale_string' => 'language',
'multiple_identities' => 'identities_level',
'addrbook_show_images' => 'show_images',
'imap_root' => 'imap_ns_personal',
'pagesize' => 'mail_pagesize',
'top_posting' => 'reply_mode',
'keep_alive' => 'refresh_interval',
'min_keep_alive' => 'min_refresh_interval',
);
// list of supported database drivers
public $supported_dbs = array(
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
'SQLite (v2)' => 'pdo_sqlite2',
'SQL Server (SQLSRV)' => 'pdo_sqlsrv',
'SQL Server (DBLIB)' => 'pdo_dblib',
'Oracle' => 'oci8',
);
/** @var array List of config options with default value change per-release */
public $defaults_changes = array(
'1.4.0' => array('skin', 'smtp_port', 'smtp_user', 'smtp_pass'),
'1.4.1' => array('jquery_ui_skin_map'),
);
/**
* Constructor
*/
public function __construct()
{
$this->step = intval($_REQUEST['_step']);
$this->is_post = $_SERVER['REQUEST_METHOD'] == 'POST';
}
/**
* Singleton getter
*/
public static function get_instance()
{
static $inst;
if (!$inst) {
$inst = new rcmail_install();
}
return $inst;
}
/**
* Read the local config files and store properties
*/
public function load_config()
{
// defaults
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'defaults.inc.php')) {
$this->config = (array) $config;
$this->defaults = $this->config;
}
$config = null;
// config
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'config.inc.php')) {
$this->config = array_merge($this->config, $config);
}
else {
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'main.inc.php')) {
$this->config = array_merge($this->config, $config);
$this->legacy_config = true;
}
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'db.inc.php')) {
$this->config = array_merge($this->config, $config);
$this->legacy_config = true;
}
}
$this->configured = !empty($config);
}
/**
* Read the default config file and store properties
*
* @param string $file File name with path
*/
public function load_config_file($file)
{
if (!is_readable($file)) {
return;
}
+ $config = array();
+ $rcmail_config = array(); // deprecated var name
+
include $file;
// read comments from config file
if (function_exists('token_get_all')) {
$tokens = token_get_all(file_get_contents($file));
$in_config = false;
$buffer = '';
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if ($token[0] == T_VARIABLE && ($token[1] == '$config' || $token[1] == '$rcmail_config')) {
$in_config = true;
if ($buffer && $tokens[$i+1] == '[' && $tokens[$i+2][0] == T_CONSTANT_ENCAPSED_STRING) {
$propname = trim($tokens[$i+2][1], "'\"");
$this->comments[$propname] = $buffer;
$buffer = '';
$i += 3;
}
}
else if ($in_config && $token[0] == T_COMMENT) {
$buffer .= strtr($token[1], array('\n' => "\n"));
}
}
}
- // deprecated name of config variable
- if (is_array($rcmail_config)) {
- return $rcmail_config;
- }
-
- return $config;
+ return array_merge(array(), (array) $rcmail_config, (array) $config);
}
/**
* Getter for a certain config property
*
* @param string $name Property name
* @param string $default Default value
*
* @return string The property value
*/
public function getprop($name, $default = '')
{
$value = $this->config[$name];
if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"])) {
$value = rcube_utils::random_bytes(24);
}
return $value !== null && $value !== '' ? $value : $default;
}
/**
* Create configuration file that contains parameters
* that differ from default values.
*
* @return string The complete config file content
*/
public function create_config()
{
$config = array();
foreach ($this->config as $prop => $default) {
$is_default = !isset($_POST["_$prop"]);
$value = !$is_default || $this->bool_config_props[$prop] ? $_POST["_$prop"] : $default;
// always disable installer
if ($prop == 'enable_installer') {
$value = false;
}
// reset useragent to default (keeps version up-to-date)
if ($prop == 'useragent' && stripos($value, 'Roundcube Webmail/') !== false) {
$value = $this->defaults[$prop];
}
// generate new encryption key, never use the default value
if ($prop == 'des_key' && $value == $this->defaults[$prop]) {
$value = rcube_utils::random_bytes(24);
}
// convert some form data
if ($prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
if ($_POST['_dbtype'] == 'sqlite') {
$value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'],
$_POST['_dbname'][0] == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
}
else if ($_POST['_dbtype']) {
$value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'],
rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
}
}
else if ($prop == 'smtp_auth_type' && $value == '0') {
$value = '';
}
else if ($prop == 'default_host' && is_array($value)) {
$value = self::_clean_array($value);
if (count($value) <= 1) {
$value = $value[0];
}
}
else if ($prop == 'mail_pagesize' || $prop == 'addressbook_pagesize') {
$value = max(2, intval($value));
}
else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
$value = '%u';
}
else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
$value = '%p';
}
else if (is_bool($default)) {
$value = (bool) $value;
}
else if (is_numeric($value)) {
$value = intval($value);
}
else if ($prop == 'plugins' && !empty($_POST['submit'])) {
$value = array();
foreach (array_keys($_POST) as $key) {
if (preg_match('/^_plugins_*/', $key)) {
array_push($value, $_POST[$key]);
}
}
}
// skip this property
if ($value == $this->defaults[$prop]
&& (!in_array($prop, $this->local_config)
|| in_array($prop, array_merge($this->obsolete_config, array_keys($this->replaced_config)))
|| preg_match('/^db_(table|sequence)_/', $prop)
)
) {
continue;
}
// save change
$this->config[$prop] = $value;
$config[$prop] = $value;
}
$out = "<?php\n\n";
$out .= "/* Local configuration for Roundcube Webmail */\n\n";
foreach ($config as $prop => $value) {
// copy option descriptions from existing config or defaults.inc.php
$out .= $this->comments[$prop];
$out .= "\$config['$prop'] = " . self::_dump_var($value, $prop) . ";\n\n";
}
return $out;
}
/**
* save generated config file in RCUBE_CONFIG_DIR
*
* @return boolean True if the file was saved successfully, false if not
*/
public function save_configfile($config)
{
if (is_writable(RCUBE_CONFIG_DIR)) {
return file_put_contents(RCUBE_CONFIG_DIR . 'config.inc.php', $config);
}
return false;
}
/**
* Check the current configuration for missing properties
* and deprecated or obsolete settings
*
* @param string $version Previous version on upgrade
*
* @return array List with problems detected
*/
public function check_config($version = null)
{
$this->load_config();
if (!$this->configured) {
return;
}
$out = $seen = array();
// iterate over the current configuration
foreach (array_keys($this->config) as $prop) {
if ($replacement = $this->replaced_config[$prop]) {
$out['replaced'][] = array('prop' => $prop, 'replacement' => $replacement);
$seen[$replacement] = true;
}
else if (!$seen[$prop] && in_array($prop, $this->obsolete_config)) {
$out['obsolete'][] = array('prop' => $prop);
$seen[$prop] = true;
}
}
// the old default mime_magic reference is obsolete
if ($this->config['mime_magic'] == '/usr/share/misc/magic') {
$out['obsolete'][] = array(
'prop' => 'mime_magic',
'explain' => "Set value to null in order to use system default"
);
}
// check config dependencies and contradictions
if ($this->config['enable_spellcheck'] && $this->config['spellcheck_engine'] == 'pspell') {
if (!extension_loaded('pspell')) {
$out['dependencies'][] = array(
'prop' => 'spellcheck_engine',
'explain' => "This requires the <tt>pspell</tt> extension which could not be loaded."
);
}
else if (!empty($this->config['spellcheck_languages'])) {
foreach ($this->config['spellcheck_languages'] as $lang => $descr) {
if (!@pspell_new($lang)) {
$out['dependencies'][] = array(
'prop' => 'spellcheck_languages',
'explain' => "You are missing pspell support for language $lang ($descr)"
);
}
}
}
}
if ($this->config['log_driver'] == 'syslog') {
if (!function_exists('openlog')) {
$out['dependencies'][] = array(
'prop' => 'log_driver',
'explain' => "This requires the <tt>syslog</tt> extension which could not be loaded."
);
}
if (empty($this->config['syslog_id'])) {
$out['dependencies'][] = array(
'prop' => 'syslog_id',
'explain' => "Using <tt>syslog</tt> for logging requires a syslog ID to be configured"
);
}
}
// check ldap_public sources having global_search enabled
if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
foreach ($this->config['ldap_public'] as $ldap_public) {
if ($ldap_public['global_search']) {
$out['replaced'][] = array(
'prop' => 'ldap_public::global_search',
'replacement' => 'autocomplete_addressbooks'
);
break;
}
}
}
if ($version) {
$out['defaults'] = array();
foreach ($this->defaults_changes as $v => $opts) {
if (version_compare($v, $version, '>')) {
$out['defaults'] = array_merge($out['defaults'], $opts);
}
}
$out['defaults'] = array_unique($out['defaults']);
}
return $out;
}
/**
* Merge the current configuration with the defaults
* and copy replaced values to the new options.
*/
public function merge_config()
{
$current = $this->config;
$this->config = array();
foreach ($this->replaced_config as $prop => $replacement) {
if (isset($current[$prop])) {
if ($prop == 'skin_path') {
$this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
}
else if ($prop == 'multiple_identities') {
$this->config[$replacement] = $current[$prop] ? 2 : 0;
}
else {
$this->config[$replacement] = $current[$prop];
}
}
unset($current[$prop]);
}
foreach ($this->obsolete_config as $prop) {
unset($current[$prop]);
}
// add all ldap_public sources having global_search enabled to autocomplete_addressbooks
if (is_array($current['ldap_public'])) {
foreach ($current['ldap_public'] as $key => $ldap_public) {
if ($ldap_public['global_search']) {
$this->config['autocomplete_addressbooks'][] = $key;
unset($current['ldap_public'][$key]['global_search']);
}
}
}
$this->config = array_merge($this->config, $current);
foreach (array_keys((array) $current['ldap_public']) as $key) {
$this->config['ldap_public'][$key] = $current['ldap_public'][$key];
}
}
/**
* Compare the local database schema with the reference schema
* required for this version of Roundcube
*
* @param rcube_db $db Database object
*
* @return boolean True if the schema is up-to-date, false if not or an error occurred
*/
public function db_schema_check($db)
{
if (!$this->configured) {
return false;
}
// read reference schema from mysql.initial.sql
$engine = $db->db_provider;
$db_schema = $this->db_read_schema(INSTALL_PATH . "SQL/$engine.initial.sql", $schema_version);
$errors = array();
// Just check the version
if ($schema_version) {
$version = rcmail_utils::db_version();
if (empty($version)) {
$errors[] = "Schema version not found";
}
else if ($schema_version != $version) {
$errors[] = "Schema version: {$version} (required: {$schema_version})";
}
return !empty($errors) ? $errors : false;
}
// check list of tables
$existing_tables = $db->list_tables();
foreach ($db_schema as $table => $cols) {
$table = $this->config['db_prefix'] . $table;
if (!in_array($table, $existing_tables)) {
$errors[] = "Missing table '".$table."'";
}
else { // compare cols
$db_cols = $db->list_cols($table);
$diff = array_diff(array_keys($cols), $db_cols);
if (!empty($diff)) {
$errors[] = "Missing columns in table '$table': " . implode(',', $diff);
}
}
}
return !empty($errors) ? $errors : false;
}
/**
* Utility function to read database schema from an .sql file
*/
private function db_read_schema($schemafile, &$version = null)
{
- $lines = file($schemafile);
- $schema = array();
- $keywords = array('PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN');
+ $lines = file($schemafile);
+ $schema = array();
+ $keywords = array('PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN');
+ $table_name = null;
foreach ($lines as $line) {
if (preg_match('/^\s*create table ([\S]+)/i', $line, $m)) {
$table_name = explode('.', $m[1]);
$table_name = end($table_name);
$table_name = preg_replace('/[`"\[\]]/', '', $table_name);
}
else if (preg_match('/insert into/i', $line) && preg_match('/\'roundcube-version\',\s*\'([0-9]+)\'/', $line, $m)) {
$version = $m[1];
}
else if ($table_name && ($line = trim($line))) {
if ($line == 'GO' || $line[0] == ')' || $line[strlen($line)-1] == ';') {
$table_name = null;
}
else {
$items = explode(' ', $line);
$col = $items[0];
$col = preg_replace('/[`"\[\]]/', '', $col);
if (!in_array(strtoupper($col), $keywords)) {
$type = strtolower($items[1]);
$type = preg_replace('/[^a-zA-Z0-9()]/', '', $type);
$schema[$table_name][$col] = $type;
}
}
}
}
return $schema;
}
/**
* Try to detect some file's mimetypes to test the correct behavior of fileinfo
*/
public function check_mime_detection()
{
$errors = array();
$files = array(
'program/resources/tinymce/video.png' => 'image/png',
'program/resources/blank.tiff' => 'image/tiff',
'program/resources/blocked.gif' => 'image/gif',
);
foreach ($files as $path => $expected) {
$mimetype = rcube_mime::file_content_type(INSTALL_PATH . $path, basename($path));
if ($mimetype != $expected) {
$errors[] = array($path, $mimetype, $expected);
}
}
return $errors;
}
/**
* Check the correct configuration of the 'mime_types' mapping option
*/
public function check_mime_extensions()
{
$errors = array();
$types = array(
'application/zip' => 'zip',
'text/css' => 'css',
'application/pdf' => 'pdf',
'image/gif' => 'gif',
'image/svg+xml' => 'svg',
);
foreach ($types as $mimetype => $expected) {
$ext = rcube_mime::get_mime_extensions($mimetype);
if (!in_array($expected, (array) $ext)) {
$errors[] = array($mimetype, $ext, $expected);
}
}
return $errors;
}
/**
* Getter for the last error message
*
* @return string Error message or null if none exists
*/
public function get_error()
{
return $this->last_error['message'];
}
/**
* Return a list with all imap hosts configured
*
* @return array Clean list with imap hosts
*/
public function get_hostlist()
{
$default_hosts = (array) $this->getprop('default_host');
$out = array();
foreach ($default_hosts as $key => $name) {
if (!empty($name)) {
$out[] = rcube_utils::parse_host(is_numeric($key) ? $name : $key);
}
}
return $out;
}
/**
* Create a HTML dropdown to select a previous version of Roundcube
*/
public function versions_select($attrib = array())
{
$select = new html_select($attrib);
$select->add(array(
'0.1-stable', '0.1.1',
'0.2-alpha', '0.2-beta', '0.2-stable',
'0.3-stable', '0.3.1',
'0.4-beta', '0.4.2',
'0.5-beta', '0.5', '0.5.1', '0.5.2', '0.5.3', '0.5.4',
'0.6-beta', '0.6',
'0.7-beta', '0.7', '0.7.1', '0.7.2', '0.7.3', '0.7.4',
'0.8-beta', '0.8-rc', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5', '0.8.6',
'0.9-beta', '0.9-rc', '0.9-rc2',
// Note: Do not add newer versions here
));
return $select;
}
/**
* Return a list with available subfolders of the skin directory
*/
public function list_skins()
{
$skins = array();
$skindir = INSTALL_PATH . 'skins/';
foreach (glob($skindir . '*') as $path) {
if (is_dir($path) && is_readable($path)) {
$skins[] = substr($path, strlen($skindir));
}
}
return $skins;
}
/**
* Return a list with available subfolders of the plugins directory
* (with their associated description in composer.json)
*/
public function list_plugins()
{
$plugins = array();
$plugin_dir = INSTALL_PATH . 'plugins/';
foreach (glob($plugin_dir . '*') as $path) {
if (!is_dir($path)) {
continue;
}
if (is_readable($path . '/composer.json')) {
$file_json = json_decode(file_get_contents($path . '/composer.json'));
$plugin_desc = $file_json->description ?: 'N/A';
}
else {
$plugin_desc = 'N/A';
}
$name = substr($path, strlen($plugin_dir));
$plugins[] = array(
'name' => $name,
'desc' => $plugin_desc,
'enabled' => in_array($name, (array) $this->config['plugins'])
);
}
return $plugins;
}
/**
* Display OK status
*
* @param string $name Test name
* @param string $message Confirm message
*/
public function pass($name, $message = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="success">OK</span>';
$this->_showhint($message);
}
/**
* Display an error status and increase failure count
*
* @param string $name Test name
* @param string $message Error message
* @param string $url URL for details
* @param bool $optional Do not count this failure
*/
public function fail($name, $message = '', $url = '', $optional=false)
{
if (!$optional) {
$this->failures++;
}
echo rcube::Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
$this->_showhint($message, $url);
}
/**
* Display an error status for optional settings/features
*
* @param string $name Test name
* @param string $message Error message
* @param string $url URL for details
*/
public function optfail($name, $message = '', $url = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="na">NOT OK</span>';
$this->_showhint($message, $url);
}
/**
* Display warning status
*
* @param string $name Test name
* @param string $message Warning message
* @param string $url URL for details
*/
public function na($name, $message = '', $url = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
$this->_showhint($message, $url);
}
private function _showhint($message, $url = '')
{
$hint = rcube::Q($message);
if ($url) {
$hint .= ($hint ? '; ' : '') . 'See <a href="' . rcube::Q($url) . '" target="_blank">' . rcube::Q($url) . '</a>';
}
if ($hint) {
echo '<span class="indent">(' . $hint . ')</span>';
}
}
private static function _clean_array($arr)
{
$out = array();
foreach (array_unique($arr) as $k => $val) {
if (!empty($val)) {
if (is_numeric($k)) {
$out[] = $val;
}
else {
$out[$k] = $val;
}
}
}
return $out;
}
private static function _dump_var($var, $name=null)
{
// special values
switch ($name) {
case 'syslog_facility':
$list = array(32 => 'LOG_AUTH', 80 => 'LOG_AUTHPRIV', 72 => ' LOG_CRON',
24 => 'LOG_DAEMON', 0 => 'LOG_KERN', 128 => 'LOG_LOCAL0',
136 => 'LOG_LOCAL1', 144 => 'LOG_LOCAL2', 152 => 'LOG_LOCAL3',
160 => 'LOG_LOCAL4', 168 => 'LOG_LOCAL5', 176 => 'LOG_LOCAL6',
184 => 'LOG_LOCAL7', 48 => 'LOG_LPR', 16 => 'LOG_MAIL',
56 => 'LOG_NEWS', 40 => 'LOG_SYSLOG', 8 => 'LOG_USER', 64 => 'LOG_UUCP'
);
if ($val = $list[$var]) {
return $val;
}
break;
/*
// RCMAIL_VERSION is undefined here
case 'useragent':
if (preg_match('|^(.*)/('.preg_quote(RCMAIL_VERSION, '|').')$|i', $var, $m)) {
return '"' . addcslashes($var, '"') . '/" . RCMAIL_VERSION';
}
break;
*/
}
if (is_array($var)) {
if (empty($var)) {
return 'array()';
}
else { // check if all keys are numeric
$isnum = true;
foreach (array_keys($var) as $key) {
if (!is_numeric($key)) {
$isnum = false;
break;
}
}
if ($isnum) {
return 'array(' . implode(', ', array_map(array('rcmail_install', '_dump_var'), $var)) . ')';
}
}
}
return var_export($var, true);
}
/**
* Initialize the database with the according schema
*
* @param rcube_db $db Database connection
*
* @return boolen True on success, False on error
*/
public function init_db($db)
{
$engine = $db->db_provider;
// read schema file from /SQL/*
$fname = INSTALL_PATH . "SQL/$engine.initial.sql";
if ($sql = @file_get_contents($fname)) {
$db->set_option('table_prefix', $this->config['db_prefix']);
$db->exec_script($sql);
}
else {
$this->fail('DB Schema', "Cannot read the schema file: $fname");
return false;
}
if ($err = $this->get_error()) {
$this->fail('DB Schema', "Error creating database schema: $err");
return false;
}
return true;
}
/**
* Update database schema
*
* @param string $version Version to update from
*
* @return boolen True on success, False on error
*/
public function update_db($version)
{
return rcmail_utils::db_update(INSTALL_PATH . 'SQL',
'roundcube', $version, array('quiet' => true));
}
/**
* Handler for Roundcube errors
*/
public function raise_error($p)
{
$this->last_error = $p;
}
}
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index 2a820e42e..4c995dc6f 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2525 +1,2536 @@
<?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', $_SESSION['language']);
$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 + $version[2]);
// 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)) {
$skin = rcube_config::DEFAULT_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 (!$meta['name']) {
$meta['name'] = $skin_id;
}
$this->skins[$skin_id] = $meta;
// Keep skin config for ajax requests (#6613)
$_SESSION['skin_config'] = array();
if ($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
$this->meta_tags = array_merge($this->meta_tags, (array) $meta['meta']);
$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));
// 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 || $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 ($this->env['extwin'])
$p['extwin'] = 1;
$location = $this->app->url($p, false, false, $secure);
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 ($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 || $this->env['framed'];
if ($framed && ($xopt = $this->app->config->get('x_frame_options', 'sameorigin'))) {
if (strtolower($xopt) === 'deny') {
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 = preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']);
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)
{
global $__page_content, $ERROR_CODE, $ERROR_MESSAGE;
$ERROR_CODE = $code;
$ERROR_MESSAGE = $message;
include RCUBE_INSTALL_PATH . 'program/steps/utils/error.inc';
exit;
}
/**
* 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(
"\$_SESSION['\\1']",
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"\$this->env['\\1']",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"\$_COOKIE['\\1']",
"\$this->browser->{'\\1'}",
"'{$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 ($attrib['name'] || $attrib['command']) {
return $this->button($attrib);
}
break;
// frame
case 'frame':
return $this->frame($attrib);
break;
// show a label
case 'label':
if ($attrib['expression'])
$attrib['name'] = $this->eval_expression($attrib['expression']);
if ($attrib['name'] || $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 = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (rcube_utils::get_boolean((string)$attrib['html']) ? 'no' : '');
// 'noshow' can be used in skins to define new labels
if ($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 ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
if ($attrib['file'][0] != '/') {
$attrib['file'] = '/templates/' . $attrib['file'];
}
$old_base_path = $this->base_path;
$include = '';
if (!empty($attrib['skin_path'])) {
$attrib['skinpath'] = $attrib['skin_path'];
}
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skinpath'])) {
// 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 = '';
// correct deprecated object names
if ($this->deprecated_template_objects[$object]) {
$object = $this->deprecated_template_objects[$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"')));
if (!empty($attrib['type']) && ($template_logo = $this->get_template_logo($attrib['type'])) !== null) {
$attrib['src'] = $template_logo;
}
else if (($template_logo = $this->get_template_logo()) !== null) {
$attrib['src'] = $template_logo;
}
// process alternative logos (eg for Elastic small screen)
foreach ($attrib as $key => $value) {
if (preg_match('/data-src-(.*)/', $key, $matches)) {
if (($template_logo = $this->get_template_logo($matches[1])) !== null) {
$attrib[$key] = $template_logo;
}
$attrib[$key] = !empty($attrib[$key]) ? $this->abs_url($attrib[$key]) : null;
}
}
if ($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 if ($object == 'links') {
+ 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 (!($attrib['command'] || $attrib['name'] || $attrib['href'])) {
return '';
}
// try to find out the button type
if ($attrib['type']) {
$attrib['type'] = strtolower($attrib['type']);
if (strpos($attrib['type'], '-menuitem')) {
$attrib['type'] = substr($attrib['type'], 0, -9);
$menuitem = true;
}
}
else {
$attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'button';
}
$command = $attrib['command'];
$action = $command ?: $attrib['name'];
if ($attrib['task']) {
$command = $attrib['task'] . '.' . $command;
$element = $attrib['task'] . '.' . $action;
}
else {
$element = ($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 '';
}
if (!$attrib['image']) {
$attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact'];
}
if (!$attrib['id']) {
$attrib['id'] = sprintf('rcmbtn%d', $s_button_count++);
}
// get localized text for labels and titles
if ($attrib['title']) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $attrib['domain']));
}
if ($attrib['label']) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $attrib['domain']));
}
if ($attrib['alt']) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $attrib['domain']));
}
// set accessibility attributes
if (!$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 && !$attrib['title'] && $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 ($attrib['command']) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
self::JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
$attrib['imageact'] ? $this->abs_url($attrib['imageact']) : $attrib['classact'],
$attrib['imagesel'] ? $this->abs_url($attrib['imagesel']) : $attrib['classsel'],
$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 ($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 (!$attrib['href']) {
$attrib['href'] = '#';
}
if ($attrib['task']) {
if ($attrib['classact']) {
$attrib['class'] = $attrib['classact'];
}
}
else if ($command && !$attrib['onclick']) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
self::JS_OBJECT_NAME,
$command,
$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 ($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'] : ($attrib['label'] ? $attrib['label'] : $attrib['command']);
$link_attrib = array_merge(html::$common_attrib, array('href', 'onclick', 'tabindex', 'target', 'rel'));
if ($attrib['innerclass']) {
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($attrib['command']) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
else {
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($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 ($attrib['wrapper']) {
$out = html::tag($attrib['wrapper'], null, $out);
}
- if ($menuitem) {
+ if (!empty($menuitem)) {
$class = $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 (!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()) {
header('Content-Language: ' . $lang);
}
}
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
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
$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";
$page_header .= array_reduce((array) $this->script_files['head_bottom'], $merge_script_files);
$page_footer .= array_reduce((array) $this->script_files['foot'], $merge_script_files);
$page_footer .= $this->footer . "\n";
$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) && !$is_empty
+ 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 (!$attrib['id']) {
$attrib['id'] = 'rcmframe' . ++$idcount;
}
$attrib['name'] = $attrib['id'];
$attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'about:blank';
// register as 'contentframe' object
if ($is_contentframe || $attrib['contentframe']) {
$this->set_env('contentframe', $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 ($this->env['extwin']) {
$hiddenfield = new html_hiddenfield(array('name' => '_extwin', 'value' => '1'));
$hidden = $hiddenfield->show();
}
else if ($this->framed || $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 ($attrib['task']) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if ($attrib['action']) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
// we already have a <form> tag
if ($attrib['form']) {
if ($this->framed || $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();
}
return rcube_utils::idn_to_utf8($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');
$_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');
$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'));
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')
+ $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')));
}
// 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($attrib['class'] . ' no-bs');
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if ($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();
// Support for multiple searchforms on the same page
if ($attrib['gui-object'] !== false && $attrib['gui-object'] !== 'false') {
$this->add_gui_object($attrib['gui-object'] ?: 'qsearchbox', $attrib['id']);
}
// add form tag around text field
if (empty($attrib['form']) && empty($attrib['no-form'])) {
$out = $this->form_tag(array(
'name' => $attrib['form-name'] ?: 'rcmqsearchform',
'onsubmit' => sprintf("%s.command('%s'); return false", self::JS_OBJECT_NAME, $attrib['command'] ?: 'search'),
// 'style' => "display:inline"
), $out);
}
if (!empty($attrib['wrapper'])) {
- $header = html::tag($attrib['ariatag'] ?: 'h2', array(
- 'id' => 'aria-label-' . $attrib['label'],
- 'class' => 'voice'
- ), rcube::Q($this->app->gettext('arialabel' . $attrib['label'], $attrib['label-domain'])));
+ $options_button = '';
+ $header_label = $this->app->gettext('arialabel' . $attrib['label'], $attrib['label-domain']);
+ $header_attrs = array(
+ 'id' => 'aria-label-' . $attrib['label'],
+ 'class' => 'voice'
+ );
+
+ $header = html::tag($attrib['ariatag'] ?: 'h2', $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' => $attrib['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' => $attrib['reset-command'] ?: 'reset-search',
'class' => 'button reset',
'label' => 'resetsearch',
'title' => 'resetsearch',
'tabindex' => '0',
'innerclass' => 'inner',
));
$out = html::div(array(
'role' => 'search',
'aria-labelledby' => $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)
*
* @return string image URL
*/
protected function get_template_logo($type = 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)) {
// Use strict matching, remove wild card options
$template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
}
else {
// No type set so remove those options from the list
$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/include/rcmail_sendmail.php b/program/include/rcmail_sendmail.php
index 503710a1a..e2ac45b9f 100644
--- a/program/include/rcmail_sendmail.php
+++ b/program/include/rcmail_sendmail.php
@@ -1,1536 +1,1545 @@
<?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: |
| Common code for generating and saving/sending mail message |
| with support for common user interface elements |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Common code for generating and saving/sending mail message
* with support for common user interface elements.
*
* @package Webmail
*/
class rcmail_sendmail
{
public $data = array();
public $options = array();
protected $parse_data = array();
- protected $compose_form;
+ protected $message_form;
+ protected $rcmail;
// define constants for message compose mode
const MODE_REPLY = 'reply';
const MODE_FORWARD = 'forward';
const MODE_DRAFT = 'draft';
const MODE_EDIT = 'edit';
/**
* Object constructor
*
* @param array $data Compose data
* @param array $options Operation options:
* savedraft (bool) - Enable save-draft mode
* sendmail (bool) - Enable send-mail mode
* saveonly (bool) - Enable save-only mode
* message (object) - Message object to get some data from
* error_handler (callback) - Error handler
*/
public function __construct($data = array(), $options = array())
{
$this->rcmail = rcube::get_instance();
$this->data = (array) $data;
$this->options = (array) $options;
$this->options['sendmail_delay'] = (int) $this->rcmail->config->get('sendmail_delay');
if (empty($options['error_handler'])) {
$this->options['error_handler'] = function() { return false; };
}
if ($this->options['message']) {
$this->compose_init($this->options['message']);
}
}
/**
* Collect input data for message headers
*
* @return array Message headers
*/
public function headers_input()
{
if ($this->options['sendmail'] && $this->options['sendmail_delay']) {
$last_time = $this->rcmail->config->get('last_message_time');
$wait_sec = time() - $this->options['sendmail_delay'] - intval($last_time);
if ($wait_sec < 0) {
return $this->options['error_handler']('senttooquickly', 'error', array('sec' => $wait_sec * -1));
}
}
// set default charset
if (!($charset = $this->options['charset'])) {
$charset = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST) ?: $this->rcmail->output->get_charset();
$this->options['charset'] = $charset;
}
$this->parse_data = array();
$mailto = $this->email_input_format(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true, $charset), true);
$mailcc = $this->email_input_format(rcube_utils::get_input_value('_cc', rcube_utils::INPUT_POST, true, $charset), true);
$mailbcc = $this->email_input_format(rcube_utils::get_input_value('_bcc', rcube_utils::INPUT_POST, true, $charset), true);
if ($this->parse_data['INVALID_EMAIL'] && !$this->options['savedraft']) {
return $this->options['error_handler']('emailformaterror', 'error', array('email' => $this->parse_data['INVALID_EMAIL']));
}
if (($max_recipients = (int) $this->rcmail->config->get('max_recipients')) > 0) {
if ($this->parse_data['RECIPIENT_COUNT'] > $max_recipients) {
return $this->options['error_handler']('toomanyrecipients', 'error', array('max' => $max_recipients));
}
}
if (empty($mailto) && !empty($mailcc)) {
$mailto = $mailcc;
$mailcc = null;
}
else if (empty($mailto)) {
$mailto = 'undisclosed-recipients:;';
}
$dont_override = (array) $this->rcmail->config->get('dont_override');
$mdn_enabled = in_array('mdn_default', $dont_override) ? $this->rcmail->config->get('mdn_default') : !empty($_POST['_mdn']);
$dsn_enabled = in_array('dsn_default', $dont_override) ? $this->rcmail->config->get('dsn_default') : !empty($_POST['_dsn']);
$subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, true, $charset);
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST, true, $charset);
$replyto = rcube_utils::get_input_value('_replyto', rcube_utils::INPUT_POST, true, $charset);
$followupto = rcube_utils::get_input_value('_followupto', rcube_utils::INPUT_POST, true, $charset);
+ $from_string = '';
// Get sender name and address from identity...
if (is_numeric($from)) {
if (is_array($identity_arr = $this->get_identity($from))) {
if ($identity_arr['mailto']) {
$from = $identity_arr['mailto'];
}
if ($identity_arr['string']) {
$from_string = $identity_arr['string'];
}
}
else {
$from = null;
}
}
// ... if there is no identity record, this might be a custom from
else if (($from_string = $this->email_input_format($from))
&& preg_match('/(\S+@\S+)/', $from_string, $m)
) {
$from = trim($m[1], '<>');
}
// ... otherwise it's empty or invalid
else {
$from = null;
}
// check 'From' address (identity may be incomplete)
if (!$this->options['savedraft'] && !$this->options['saveonly'] && empty($from)) {
return $this->options['error_handler']('nofromaddress', 'error');
}
if (!$from_string && $from) {
$from_string = $from;
}
$from_string = rcube_charset::convert($from_string, RCUBE_CHARSET, $charset);
$message_id = $this->data['param']['message-id'];
if (!$message_id) {
$message_id = $this->rcmail->gen_message_id($from);
}
$this->options['dsn_enabled'] = $dsn_enabled;
$this->options['from'] = $from;
$this->options['mailto'] = $mailto;
// compose headers array
$headers = array(
'Received' => $this->header_received(),
'Date' => $this->rcmail->user_date(),
'From' => $from_string,
'To' => $mailto,
'Cc' => $mailcc,
'Bcc' => $mailbcc,
'Subject' => trim($subject),
'Reply-To' => $this->email_input_format($replyto),
- 'Mail-Reply-To' => $headers['Reply-To'],
+ 'Mail-Reply-To' => $this->email_input_format($replyto),
'Mail-Followup-To' => $this->email_input_format($followupto),
'In-Reply-To' => $this->data['reply_msgid'],
'References' => $this->data['references'],
'User-Agent' => $this->rcmail->config->get('useragent'),
'Message-ID' => $message_id,
'X-Sender' => $from,
);
if (!empty($identity_arr['organization'])) {
$headers['Organization'] = $identity_arr['organization'];
}
if ($mdn_enabled) {
$headers['Return-Receipt-To'] = $from_string;
$headers['Disposition-Notification-To'] = $from_string;
}
if (!empty($_POST['_priority'])) {
$priority = intval($_POST['_priority']);
$a_priorities = array(1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest');
if ($str_priority = $a_priorities[$priority]) {
$headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority));
}
}
// remember reply/forward UIDs in special headers
if ($this->options['savedraft']) {
// Note: We ignore <UID>.<PART> forwards/replies here
if (($uid = $this->data['reply_uid']) && !preg_match('/^\d+\.[0-9.]+$/', $uid)) {
$headers['X-Draft-Info'] = $this->draftinfo_encode(array(
'type' => 'reply',
'uid' => $uid,
'folder' => $this->data['mailbox']
));
}
else if (!empty($this->data['forward_uid'])
&& ($uid = rcube_imap_generic::compressMessageSet($this->data['forward_uid']))
&& !preg_match('/^\d+[0-9.]+$/', $uid)
) {
$headers['X-Draft-Info'] = $this->draftinfo_encode(array(
'type' => 'forward',
'uid' => $uid,
'folder' => $this->data['mailbox']
));
}
}
return array_filter($headers);
}
/**
* Set charset and transfer encoding on the message
*
* @param Mail_mime $message Message object
* @param bool $flowed Enable format=flowed
*/
public function set_message_encoding($message, $flowed = false)
{
$text_charset = $this->options['charset'];
$transfer_encoding = '7bit';
$head_encoding = 'quoted-printable';
// choose encodings for plain/text body and message headers
if (preg_match('/ISO-2022/i', $text_charset)) {
$head_encoding = 'base64'; // RFC1468
}
else if (preg_match('/[^\x00-\x7F]/', $message->getTXTBody())) {
$transfer_encoding = $this->rcmail->config->get('force_7bit') ? 'quoted-printable' : '8bit';
}
else if ($this->options['charset'] == 'UTF-8') {
$text_charset = 'US-ASCII';
}
if ($flowed) {
$text_charset .= ";\r\n format=flowed";
}
// encoding settings for mail composing
$message->setParam('text_encoding', $transfer_encoding);
$message->setParam('html_encoding', 'quoted-printable');
$message->setParam('head_encoding', $head_encoding);
$message->setParam('head_charset', $this->options['charset']);
$message->setParam('html_charset', $this->options['charset']);
$message->setParam('text_charset', $text_charset);
}
/**
* Create a message to be saved/sent
*
* @param array $headers Message headers
* @param string $body Message body
* @param bool $isHtml The body is HTML or not
* @param array $attachments Optional message attachments array
*
* @return Mail_mime Message object
*/
public function create_message($headers, $body, $isHtml = false, $attachments = array())
{
// set line length for body wrapping
$line_length = $this->rcmail->config->get('line_length', 72);
$charset = $this->options['charset'];
$flowed = $this->options['savedraft'] || $this->rcmail->config->get('send_format_flowed', true);
// create PEAR::Mail_mime instance
$MAIL_MIME = new Mail_mime("\r\n");
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (is_array($attachments)) {
$memory = 0;
foreach ($attachments as $attachment) {
$memory += $attachment['size'];
}
// Yeah, Net_SMTP needs up to 12x more memory, 1.33 is for base64
if (!rcube_utils::mem_check($memory * 1.33 * 12)) {
$MAIL_MIME->setParam('delay_file_io', true);
}
}
$plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', array(
'body' => $body,
'type' => $isHtml ? 'html' : 'plain',
'message' => $MAIL_MIME
));
// For HTML-formatted messages, construct the MIME message with both
// the HTML part and the plain-text part
if ($isHtml) {
$MAIL_MIME->setHTMLBody($plugin['body']);
$plain_body = $this->rcmail->html2text($plugin['body'], array('width' => 0, 'charset' => $charset));
$plain_body = rcube_mime::wordwrap($plain_body, $line_length, "\r\n", false, $charset);
$plain_body = wordwrap($plain_body, 998, "\r\n", true);
// There's no sense to use multipart/alternative if the text/plain
// part would be blank. Completely blank text/plain part may confuse
// some mail clients (#5283)
if (strlen(trim($plain_body)) > 0) {
// make sure all line endings are CRLF (#1486712)
$plain_body = preg_replace('/\r?\n/', "\r\n", $plain_body);
$plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', array(
'body' => $plain_body,
'type' => 'alternative',
'message' => $MAIL_MIME
));
// add a plain text version of the e-mail as an alternative part.
$MAIL_MIME->setTXTBody($plugin['body']);
}
// Extract image Data URIs into message attachments (#1488502)
$this->extract_inline_images($MAIL_MIME, $this->options['from']);
}
else {
$body = $plugin['body'];
// compose format=flowed content if enabled
if ($flowed) {
$body = rcube_mime::format_flowed($body, min($line_length + 2, 79), $charset);
}
else {
$body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
}
$body = wordwrap($body, 998, "\r\n", true);
$MAIL_MIME->setTXTBody($body, false, true);
}
// encoding settings for mail composing
$this->set_message_encoding($MAIL_MIME, $flowed);
// pass headers to message object
$MAIL_MIME->headers($headers);
return $MAIL_MIME;
}
/**
* Message delivery, and setting Replied/Forwarded flag on success
*
* @param Mail_mime $message Message object
* @param bool $disconnect Close SMTP connection after delivery
*
* @return bool True on success, False on failure
*/
public function deliver_message($message, $disconnect = true)
{
// Handle Delivery Status Notification request
- $smtp_opts = array('dsn' => $this->options['dsn_enabled']);
+ $smtp_opts = array('dsn' => $this->options['dsn_enabled']);
+ $smtp_error = null;
+ $mailbody_file = null;
$sent = $this->rcmail->deliver_message($message,
$this->options['from'],
$this->options['mailto'],
$smtp_error, $mailbody_file, $smtp_opts, $disconnect
);
// return to compose page if sending failed
if (!$sent) {
// remove temp file
if ($mailbody_file) {
unlink($mailbody_file);
}
if ($smtp_error && is_string($smtp_error)) {
$this->options['error_handler']($smtp_error, 'error');
}
else if ($smtp_error && !empty($smtp_error['label'])) {
$this->options['error_handler']($smtp_error['label'], 'error', $smtp_error['vars']);
}
else {
$this->options['error_handler']('sendingfailed', 'error');
}
return false;
}
$message->mailbody_file = $mailbody_file;
// save message sent time
if ($this->options['sendmail_delay']) {
$this->rcmail->user->save_prefs(array('last_message_time' => time()));
}
// set replied/forwarded flag
if ($this->data['reply_uid']) {
foreach (rcmail::get_uids($this->data['reply_uid'], $this->data['mailbox']) as $mbox => $uids) {
// skip <UID>.<PART> replies
if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) {
$this->rcmail->storage->set_flag($uids, 'ANSWERED', $mbox);
}
}
}
else if ($this->data['forward_uid']) {
foreach (rcmail::get_uids($this->data['forward_uid'], $this->data['mailbox']) as $mbox => $uids) {
// skip <UID>.<PART> forwards
if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) {
$this->rcmail->storage->set_flag($uids, 'FORWARDED', $mbox);
}
}
}
return true;
}
/**
* Save the message into Drafts folder (in savedraft mode)
* or in Sent mailbox if specified/configured
*
* @param Mail_mime $message Message object
*
* @return mixed Operation status
*/
public function save_message($message)
{
+ $store_folder = false;
+ $store_target = null;
+ $saved = false;
+
// Determine which folder to save message
if ($this->options['savedraft']) {
$store_target = $this->rcmail->config->get('drafts_mbox');
}
else if (!$this->rcmail->config->get('no_save_sent_messages')) {
if (isset($_POST['_store_target'])) {
$store_target = rcube_utils::get_input_value('_store_target', rcube_utils::INPUT_POST, true);
}
else {
$store_target = $this->rcmail->config->get('sent_mbox');
}
}
if ($store_target) {
$storage = $this->rcmail->get_storage();
// check if folder is subscribed
if ($storage->folder_exists($store_target, true)) {
$store_folder = true;
}
// folder may be existing but not subscribed (#1485241)
else if (!$storage->folder_exists($store_target)) {
$store_folder = $storage->create_folder($store_target, true);
}
else if ($storage->subscribe($store_target)) {
$store_folder = true;
}
// append message to sent box
if ($store_folder) {
// message body in file
if ($message->mailbody_file || $message->getParam('delay_file_io')) {
$headers = $message->txtHeaders();
// file already created
if ($message->mailbody_file) {
$msg = $message->mailbody_file;
}
else {
$message->mailbody_file = rcube_utils::temp_filename('msg');
$msg = $message->saveMessageBody($message->mailbody_file);
if (!is_a($msg, 'PEAR_Error')) {
$msg = $message->mailbody_file;
}
}
}
else {
$msg = $message->getMessage();
$headers = '';
}
if (is_a($msg, 'PEAR_Error')) {
rcube::raise_error(array(
'code' => 650, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$msg->getMessage()),
true, false);
}
else {
$saved = $storage->save_message($store_target, $msg, $headers,
$message->mailbody_file ? true : false, array('SEEN'));
}
}
// raise error if saving failed
if (!$saved) {
rcube::raise_error(array('code' => 800, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not save message in $store_target"), true, false);
}
}
if ($message->mailbody_file) {
unlink($message->mailbody_file);
unset($message->mailbody_file);
}
$this->options['store_target'] = $store_target;
$this->options['store_folder'] = $store_folder;
return $saved;
}
/**
* If enabled, returns Received header content to be prepended
* to message headers
*
* @return string Received header content
*/
public function header_received()
{
if ($this->rcmail->config->get('http_received_header')) {
$nldlm = "\r\n\t";
$http_header = 'from ';
// FROM/VIA
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
$http_header .= $this->received_host($hosts[0]) . $nldlm . ' via ';
}
$http_header .= $this->received_host($_SERVER['REMOTE_ADDR']);
// BY
$http_header .= $nldlm . 'by ' . rcube_utils::server_name('HTTP_HOST');
// WITH
$http_header .= $nldlm . 'with HTTP (' . $_SERVER['SERVER_PROTOCOL']
. ' ' . $_SERVER['REQUEST_METHOD'] . '); ' . date('r');
return wordwrap($http_header, 69, $nldlm);
}
}
/**
* Converts host address into host spec. for Received header
*/
protected function received_host($host)
{
$hostname = gethostbyaddr($host);
$result = $this->encrypt_host($hostname);
if ($host != $hostname) {
$result .= ' (' . $this->encrypt_host($host) . ')';
}
return $result;
}
/**
* Encrypt host IP or hostname for Received header
*/
protected function encrypt_host($host)
{
if ($this->rcmail->config->get('http_received_header_encrypt')) {
return $this->rcmail->encrypt($host);
}
if (!preg_match('/[^0-9:.]/', $host)) {
return "[$host]";
}
return $host;
}
/**
* Returns user identity record
*
* @param int $id Identity ID
*
* @return array User identity data
*/
public function get_identity($id)
{
if ($sql_arr = $this->rcmail->user->get_identity($id)) {
$out = $sql_arr;
if ($this->options['charset'] != RCUBE_CHARSET) {
foreach ($out as $k => $v) {
$out[$k] = rcube_charset::convert($v, RCUBE_CHARSET, $this->options['charset']);
}
}
$out['mailto'] = $sql_arr['email'];
$out['string'] = format_email_recipient($sql_arr['email'], $sql_arr['name']);
return $out;
}
return false;
}
/**
* Extract image attachments from HTML message (data URIs)
*
* @param Mail_mime $message Message object
* @param string $from Sender email address
*/
public static function extract_inline_images($message, $from)
{
$body = $message->getHTMLBody();
$offset = 0;
$list = array();
$domain = 'localhost';
$regexp = '#img[^>]+src=[\'"](data:([^;]*);base64,([a-z0-9+/=\r\n]+))([\'"])#i';
if (preg_match_all($regexp, $body, $matches, PREG_OFFSET_CAPTURE)) {
// get domain for the Content-ID, must be the same as in Mail_Mime::get()
if (preg_match('#@([0-9a-zA-Z\-\.]+)#', $from, $m)) {
$domain = $m[1];
}
foreach ($matches[1] as $idx => $m) {
$data = preg_replace('/\r\n/', '', $matches[3][$idx][0]);
$data = base64_decode($data);
if (empty($data)) {
continue;
}
$hash = md5($data) . '@' . $domain;
$mime_type = $matches[2][$idx][0];
$name = $list[$hash];
if (empty($mime_type)) {
$mime_type = rcube_mime::image_content_type($data);
}
// add the image to the MIME message
if (!$name) {
$ext = preg_replace('#^[^/]+/#', '', $mime_type);
$name = substr($hash, 0, 8) . '.' . $ext;
$list[$hash] = $name;
$message->addHTMLImage($data, $mime_type, $name, false, $hash);
}
$body = substr_replace($body, $name, $m[1] + $offset, strlen($m[0]));
$offset += strlen($name) - strlen($m[0]);
}
}
$message->setHTMLBody($body);
}
/**
* Parse and cleanup email address input (and count addresses)
*
* @param string $mailto Address input
* @param boolean $count Do count recipients (count saved in $this->parse_data['RECIPIENT_COUNT'])
* @param boolean $check Validate addresses (errors saved in $this->parse_data['INVALID_EMAIL'])
*
* @return string Canonical recipients string (comma separated)
*/
public function email_input_format($mailto, $count = false, $check = true)
{
// simplified email regexp, supporting quoted local part
$email_regexp = '(\S+|("[^"]+"))@\S+';
$delim = ',;';
$regexp = array("/[$delim]\s*[\r\n]+/", '/[\r\n]+/', "/[$delim]\s*\$/m", '/;/', '/(\S{1})(<'.$email_regexp.'>)/U');
$replace = array(', ', ', ', '', ',', '\\1 \\2');
// replace new lines and strip ending ', ', make address input more valid
$mailto = trim(preg_replace($regexp, $replace, $mailto));
$items = rcube_utils::explode_quoted_string("[$delim]", $mailto);
$result = array();
foreach ($items as $item) {
$item = trim($item);
// address in brackets without name (do nothing)
if (preg_match('/^<'.$email_regexp.'>$/', $item)) {
$item = rcube_utils::idn_to_ascii(trim($item, '<>'));
$result[] = $item;
}
// address without brackets and without name (add brackets)
else if (preg_match('/^'.$email_regexp.'$/', $item)) {
$item = rcube_utils::idn_to_ascii($item);
$result[] = $item;
}
// address with name (handle name)
else if (preg_match('/<*'.$email_regexp.'>*$/', $item, $matches)) {
$address = $matches[0];
$name = trim(str_replace($address, '', $item));
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
}
$name = stripcslashes($name);
$address = rcube_utils::idn_to_ascii(trim($address, '<>'));
$result[] = format_email_recipient($address, $name);
$item = $address;
}
// check address format
$item = trim($item, '<>');
if ($item && $check && !rcube_utils::check_email($item)) {
$this->parse_data['INVALID_EMAIL'] = $item;
return;
}
}
if ($count) {
$this->parse_data['RECIPIENT_COUNT'] += count($result);
}
return implode(', ', $result);
}
/**
* Returns configured generic message footer
*
* @param bool $isHtml Return HTML or Plain text version of the footer?
*
* @return string Footer content
*/
public function generic_message_footer($isHtml)
{
if ($isHtml && ($file = $this->rcmail->config->get('generic_message_footer_html'))) {
$html_footer = true;
}
else {
$file = $this->rcmail->config->get('generic_message_footer');
$html_footer = false;
}
if ($file && realpath($file)) {
// sanity check
if (!preg_match('/\.(php|ini|conf)$/', $file) && strpos($file, '/etc/') === false) {
$footer = file_get_contents($file);
if ($isHtml && !$html_footer) {
$t2h = new rcube_text2html($footer, false);
$footer = $t2h->get_html();
}
if ($this->options['charset'] && $this->options['charset'] != RCUBE_CHARSET) {
$footer = rcube_charset::convert($footer, RCUBE_CHARSET, $this->options['charset']);
}
return $footer;
}
}
return false;
}
/**
* Encode data array into a string for use in X-Draft-Info header
*
* @param array $data Data array
*
* @return string Decoded data as a string
*/
public static function draftinfo_encode($data)
{
$parts = array();
foreach ($data as $key => $val) {
$encode = $key == 'folder' || strpos($val, ';') !== false;
$parts[] = $key . '=' . ($encode ? 'B::' . base64_encode($val) : $val);
}
return implode('; ', $parts);
}
/**
* Decode X-Draft-Info header value into an array
*
* @param string $str Encoded data string (see self::draftinfo_encode())
*
* @return array Decoded data
*/
public static function draftinfo_decode($str)
{
$info = array();
foreach (preg_split('/;\s+/', $str) as $part) {
list($key, $val) = explode('=', $part, 2);
if (strpos($val, 'B::') === 0) {
$val = base64_decode(substr($val, 3));
}
else if ($key == 'folder') {
$val = base64_decode($val);
}
$info[$key] = $val;
}
return $info;
}
/**
* Header (From, To, Cc, etc.) input object for templates
*/
public function headers_output($attrib)
{
list($form_start,) = $this->form_tags($attrib);
- $out = '';
- $part = strtolower($attrib['part']);
+ $out = '';
+ $part = strtolower($attrib['part']);
+ $fname = null;
+ $field_type = null;
+ $allow_attrib = array();
+ $param = $part;
switch ($part) {
case 'from':
return $form_start . $this->compose_header_from($attrib);
case 'to':
case 'cc':
case 'bcc':
$fname = '_' . $part;
- $header = $param = $part;
$allow_attrib = array('id', 'class', 'style', 'cols', 'rows', 'tabindex');
$field_type = 'html_textarea';
break;
case 'replyto':
case 'reply-to':
$fname = '_replyto';
$param = 'replyto';
- $header = 'reply-to';
case 'followupto':
case 'followup-to':
if (!$fname) {
$fname = '_followupto';
$param = 'followupto';
- $header = 'mail-followup-to';
}
$allow_attrib = array('id', 'class', 'style', 'size', 'tabindex');
$field_type = 'html_inputfield';
break;
}
if ($fname && $field_type) {
// pass the following attributes to the form class
$field_attrib = array('name' => $fname, 'spellcheck' => 'false');
foreach ($attrib as $attr => $value) {
if (stripos($attr, 'data-') === 0 || in_array($attr, $allow_attrib)) {
$field_attrib[$attr] = $value;
}
}
// create teaxtarea object
$input = new $field_type($field_attrib);
$out = $input->show($this->compose_header_value($param, $this->data['mode']));
}
if ($form_start) {
$out = $form_start . $out;
}
// configure autocompletion
$this->rcmail->autocomplete_init();
return $out;
}
/**
* Returns From header input element
*/
protected function compose_header_from($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_from');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
if (!empty($this->options['message']->identities)) {
$a_signatures = array();
$identities = array();
$top_posting = intval($this->rcmail->config->get('reply_mode')) > 0
&& !$this->rcmail->config->get('sig_below')
&& ($this->data['mode'] == self::MODE_REPLY || $this->data['mode'] == self::MODE_FORWARD);
$separator = $top_posting ? '---' : '-- ';
$add_separator = (bool) $this->rcmail->config->get('sig_separator');
$field_attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".change_identity(this)";
$select_from = new html_select($field_attrib);
// create SELECT element
foreach ($this->options['message']->identities as $sql_arr) {
$identity_id = $sql_arr['identity_id'];
$select_from->add(format_email_recipient($sql_arr['email'], $sql_arr['name']), $identity_id);
// add signature to array
if (!empty($sql_arr['signature']) && empty($this->data['param']['nosig'])) {
$text = $html = $sql_arr['signature'];
if ($sql_arr['html_signature']) {
$text = $this->rcmail->html2text($html, array('links' => false));
$text = trim($text, "\r\n");
}
else {
$t2h = new rcube_text2html($text, false);
$html = $t2h->get_html();
}
if ($add_separator && !preg_match('/^--[ -]\r?\n/m', $text)) {
$text = $separator . "\n" . ltrim($text, "\r\n");
$html = $separator . "<br>" . $html;
}
$a_signatures[$identity_id]['text'] = $text;
$a_signatures[$identity_id]['html'] = $html;
}
// add bcc and reply-to
if (!empty($sql_arr['reply-to'])) {
$identities[$identity_id]['replyto'] = $sql_arr['reply-to'];
}
if (!empty($sql_arr['bcc'])) {
$identities[$identity_id]['bcc'] = $sql_arr['bcc'];
}
$identities[$identity_id]['email'] = $sql_arr['email'];
}
$out = $select_from->show($this->options['message']->compose['from']);
// add signatures to client
$this->rcmail->output->set_env('signatures', $a_signatures);
$this->rcmail->output->set_env('identities', $identities);
}
// no identities, display text input field
else {
$field_attrib['class'] = 'from_address';
$input_from = new html_inputfield($field_attrib);
$out = $input_from->show($this->options['message']->compose['from']);
}
return $out;
}
/**
* Set the value of specified header depending on compose mode
*/
protected function compose_header_value($header, $mode)
{
$fvalue = '';
$decode_header = true;
$message = $this->options['message'];
$charset = $message->headers->charset;
$separator = ', ';
// we have a set of recipients stored is session
if ($header == 'to' && ($mailto_id = $this->data['param']['mailto'])
&& $_SESSION['mailto'][$mailto_id]
) {
$fvalue = urldecode($_SESSION['mailto'][$mailto_id]);
$decode_header = false;
$charset = $this->rcmail->output->charset;
// make session to not grow up too much
$this->rcmail->session->remove("mailto.$mailto_id");
}
else if (!empty($_POST['_' . $header])) {
$fvalue = rcube_utils::get_input_value('_' . $header, rcube_utils::INPUT_POST, true);
$charset = $this->rcmail->output->charset;
}
else if (!empty($this->data['param'][$header])) {
$fvalue = $this->data['param'][$header];
$charset = $this->rcmail->output->charset;
}
else if ($mode == self::MODE_REPLY) {
// get recipent address(es) out of the message headers
if ($header == 'to') {
$mailfollowup = $message->headers->others['mail-followup-to'];
$mailreplyto = $message->headers->others['mail-reply-to'];
// Reply to mailing list...
if ($message->reply_all == 'list' && $mailfollowup) {
$fvalue = $mailfollowup;
}
else if ($message->reply_all == 'list'
&& preg_match('/<mailto:([^>]+)>/i', $message->headers->others['list-post'], $m)
) {
$fvalue = $m[1];
}
// Reply to...
else if ($message->reply_all && $mailfollowup) {
$fvalue = $mailfollowup;
}
else if ($mailreplyto) {
$fvalue = $mailreplyto;
}
else if (!empty($message->headers->replyto)) {
$fvalue = $message->headers->replyto;
$replyto = true;
}
else if (!empty($message->headers->from)) {
$fvalue = $message->headers->from;
}
// Reply to message sent by yourself (#1487074, #1489230, #1490439)
// Reply-To address need to be unset (#1490233)
if (!empty($message->compose['ident']) && empty($replyto)) {
foreach (array($fvalue, $message->headers->from) as $sender) {
$senders = rcube_mime::decode_address_list($sender, null, false, $charset, true);
if (in_array($message->compose['ident']['email_ascii'], $senders)) {
$fvalue = $message->headers->to;
break;
}
}
}
}
// add recipient of original message if reply to all
else if ($header == 'cc' && !empty($message->reply_all) && $message->reply_all != 'list') {
if ($v = $message->headers->to) {
$fvalue .= $v;
}
if ($v = $message->headers->cc) {
$fvalue .= (!empty($fvalue) ? $separator : '') . $v;
}
// Deliberately ignore 'Sender' header (#6506)
// When To: and Reply-To: are the same we add From: address to the list (#1489037)
if ($v = $message->headers->from) {
$to = $message->headers->to;
$replyto = $message->headers->replyto;
$from = rcube_mime::decode_address_list($v, null, false, $charset, true);
$to = rcube_mime::decode_address_list($to, null, false, $charset, true);
$replyto = rcube_mime::decode_address_list($replyto, null, false, $charset, true);
if (!empty($replyto) && !count(array_diff($to, $replyto)) && count(array_diff($from, $to))) {
$fvalue .= (!empty($fvalue) ? $separator : '') . $v;
}
}
}
}
else if (in_array($mode, array(self::MODE_DRAFT, self::MODE_EDIT))) {
// get drafted headers
if ($header == 'to' && !empty($message->headers->to)) {
$fvalue = $message->get_header('to', true);
}
else if ($header == 'cc' && !empty($message->headers->cc)) {
$fvalue = $message->get_header('cc', true);
}
else if ($header == 'bcc' && !empty($message->headers->bcc)) {
$fvalue = $message->get_header('bcc', true);
}
else if ($header == 'replyto' && !empty($message->headers->others['mail-reply-to'])) {
$fvalue = $message->get_header('mail-reply-to');
}
else if ($header == 'replyto' && !empty($message->headers->replyto)) {
$fvalue = $message->get_header('reply-to');
}
else if ($header == 'followupto' && !empty($message->headers->others['mail-followup-to'])) {
$fvalue = $message->get_header('mail-followup-to');
}
}
// split recipients and put them back together in a unique way
if (!empty($fvalue) && in_array($header, array('to', 'cc', 'bcc'))) {
$from_email = @mb_strtolower($message->compose['ident']['email']);
$to_addresses = rcube_mime::decode_address_list($fvalue, null, $decode_header, $charset);
$fvalue = array();
foreach ($to_addresses as $addr_part) {
if (empty($addr_part['mailto'])) {
continue;
}
// According to RFC5321 local part of email address is case-sensitive
// however, here it is better to compare addresses in case-insensitive manner
$mailto = format_email(rcube_utils::idn_to_utf8($addr_part['mailto']));
$mailto_lc = mb_strtolower($addr_part['mailto']);
if (($header == 'to' || $mode != self::MODE_REPLY || $mailto_lc != $from_email)
&& !in_array($mailto_lc, (array) $message->recipients)
) {
if ($addr_part['name'] && $mailto != $addr_part['name']) {
$mailto = format_email_recipient($mailto, $addr_part['name']);
}
$fvalue[] = $mailto;
$message->recipients[] = $mailto_lc;
}
}
$fvalue = implode($separator, $fvalue);
}
return $fvalue;
}
/**
* Creates reply subject by removing common subject
* prefixes/suffixes from the original message subject
*
* @param string $subject Subject string
*
* @return string Modified subject string
*/
public static function reply_subject($subject)
{
$subject = trim($subject);
// replace Re:, Re[x]:, Re-x (#1490497)
$prefix = '/^(re:|re\[\d\]:|re-\d:)\s*/i';
do {
$subject = preg_replace($prefix, '', $subject, -1, $count);
}
while ($count);
// replace (was: ...) (#1489375)
$subject = preg_replace('/\s*\([wW]as:[^\)]+\)\s*$/', '', $subject);
return 'Re: ' . $subject;
}
/**
* Subject input object for templates
*/
public function compose_subject($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
$attrib['name'] = '_subject';
$attrib['spellcheck'] = 'true';
$textfield = new html_inputfield($attrib);
$subject = '';
// use subject from post
if (isset($_POST['_subject'])) {
$subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE);
}
else if (!empty($this->data['param']['subject'])) {
$subject = $this->data['param']['subject'];
}
// create a reply-subject
else if ($this->data['mode'] == self::MODE_REPLY) {
$subject = self::reply_subject($this->options['message']->subject);
}
// create a forward-subject
else if ($this->data['mode'] == self::MODE_FORWARD) {
if (preg_match('/^fwd:/i', $this->options['message']->subject)) {
$subject = $this->options['message']->subject;
}
else {
$subject = 'Fwd: ' . $this->options['message']->subject;
}
}
// creeate a draft-subject
else if ($this->data['mode'] == self::MODE_DRAFT || $this->data['mode'] == self::MODE_EDIT) {
$subject = $this->options['message']->subject;
}
$out = $form_start ? "$form_start\n" : '';
$out .= $textfield->show($subject);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Returns compose form tag (if not used already)
*/
public function form_tags($attrib)
{
if (rcube_utils::get_boolean((string) $attrib['noform'])) {
return array('', '');
}
$form_start = '';
if (!$this->message_form) {
$hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $this->rcmail->task));
$hiddenfields->add(array('name' => '_action', 'value' => 'send'));
$hiddenfields->add(array('name' => '_id', 'value' => $this->data['id']));
$hiddenfields->add(array('name' => '_attachments'));
if (empty($attrib['form'])) {
$form_attr = array('name' => "form", 'method' => "post", 'class' => $attrib['class']);
$form_start = $this->rcmail->output->form_tag($form_attr);
}
$form_start .= $hiddenfields->show();
}
$form_end = ($this->message_form && !strlen($attrib['form'])) ? '</form>' : '';
$form_name = $attrib['form'] ?: 'form';
if (!$this->message_form) {
$this->rcmail->output->add_gui_object('messageform', $form_name);
}
$this->message_form = $form_name;
return array($form_start, $form_end);
}
/**
* Returns compose form "head"
*/
public function form_head($attrib)
{
list($form_start,) = $this->form_tags($attrib);
return $form_start;
}
/**
* Folder selector object for templates
*/
public function folder_selector($attrib)
{
$attrib['name'] = '_store_target';
$select = $this->rcmail->folder_selector(array_merge($attrib, array(
'noselection' => '- ' . $this->rcmail->gettext('dontsave') . ' -',
'folder_filter' => 'mail',
'folder_rights' => 'w',
)));
return $select->show(isset($_POST['_store_target']) ? $_POST['_store_target'] : $this->data['param']['sent_mbox'], $attrib);
}
/**
* Mail Disposition Notification checkbox object for templates
*/
public function mdn_checkbox($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
if (!isset($attrib['id'])) {
$attrib['id'] = 'receipt';
}
$attrib['name'] = '_mdn';
$attrib['value'] = '1';
$checkbox = new html_checkbox($attrib);
if (isset($_POST['_mdn'])) {
$mdn_default = $_POST['_mdn'];
}
else if (in_array($this->data['mode'], array(self::MODE_DRAFT, self::MODE_EDIT))) {
$mdn_default = (bool) $this->options['message']->headers->mdn_to;
}
else {
$mdn_default = $this->rcmail->config->get('mdn_default');
}
$out = $form_start ? "$form_start\n" : '';
$out .= $checkbox->show($mdn_default);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Delivery Status Notification checkbox object for templates
*/
public function dsn_checkbox($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
if (!isset($attrib['id'])) {
$attrib['id'] = 'dsn';
}
$attrib['name'] = '_dsn';
$attrib['value'] = '1';
$checkbox = new html_checkbox($attrib);
if (isset($_POST['_dsn'])) {
$dsn_value = (int) $_POST['_dsn'];
}
else {
$dsn_value = $this->rcmail->config->get('dsn_default');
}
$out = $form_start ? "$form_start\n" : '';
$out .= $checkbox->show($dsn_value);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Priority selector object for templates
*/
public function priority_selector($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
$attrib['name'] = '_priority';
$prio_list = array(
$this->rcmail->gettext('lowest') => 5,
$this->rcmail->gettext('low') => 4,
$this->rcmail->gettext('normal') => 0,
$this->rcmail->gettext('high') => 2,
$this->rcmail->gettext('highest') => 1,
);
$selector = new html_select($attrib);
$selector->add(array_keys($prio_list), array_values($prio_list));
if (isset($_POST['_priority'])) {
$sel = (int) $_POST['_priority'];
}
else if (isset($this->options['message']->headers->priority)
&& intval($this->options['message']->headers->priority) != 3
) {
$sel = (int) $this->options['message']->headers->priority;
}
else {
$sel = 0;
}
$out = $form_start ? "$form_start\n" : '';
$out .= $selector->show((int) $sel);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Helper to create Sent folder if not exists
*/
public static function check_sent_folder($folder, $create = false)
{
$rcmail = rcmail::get_instance();
// we'll not save the message, so it doesn't matter
if ($rcmail->config->get('no_save_sent_messages')) {
return true;
}
if ($rcmail->storage->folder_exists($folder, true)) {
return true;
}
// folder may exist but isn't subscribed (#1485241)
if ($create) {
if (!$rcmail->storage->folder_exists($folder))
return $rcmail->storage->create_folder($folder, true);
else
return $rcmail->storage->subscribe($folder);
}
return false;
}
/**
* Initialize mail compose UI elements
*/
protected function compose_init($message)
{
$message->compose = array();
// get user's identities
$message->identities = $this->rcmail->user->list_identities(null, true);
// Set From field value
if (!empty($_POST['_from'])) {
$message->compose['from'] = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
}
else if (!empty($this->data['param']['from'])) {
$message->compose['from'] = $this->data['param']['from'];
}
else if (!empty($message->identities)) {
$ident = self::identity_select($message, $message->identities, $this->data['mode']);
$message->compose['from'] = $ident['identity_id'];
$message->compose['ident'] = $ident;
}
$this->rcmail->output->add_handlers(array(
'storetarget' => array($this, 'folder_selector'),
'composeheaders' => array($this, 'headers_output'),
'composesubject' => array($this, 'compose_subject'),
'priorityselector' => array($this, 'priority_selector'),
'mdncheckbox' => array($this, 'mdn_checkbox'),
'dsncheckbox' => array($this, 'dsn_checkbox'),
'composeformhead' => array($this, 'form_head'),
));
// add some labels to client
$this->rcmail->output->add_label('nosubject', 'nosenderwarning', 'norecipientwarning',
'nosubjectwarning', 'cancel', 'nobodywarning', 'notsentwarning', 'savingmessage',
'sendingmessage', 'searching', 'disclosedrecipwarning', 'disclosedreciptitle',
'bccinstead', 'nosubjecttitle', 'sendmessage');
$this->rcmail->output->set_env('max_disclosed_recipients', (int) $this->rcmail->config->get('max_disclosed_recipients', 5));
}
/**
* Detect recipient identity from specified message
*
* @param rcube_message $message Message object
* @param array $identities User identities (if NULL all user identities will be used)
* @param string $mode Composing mode (see self::MODE_*)
*
* @return array Selected user identity (or the default identity) data
*/
public static function identity_select($message, $identities = null, $mode = null)
{
$a_recipients = array();
$a_names = array();
if ($identities === null) {
$identities = rcmail::get_instance()->user->list_identities(null, true);
}
if (!$mode) {
$mode = self::MODE_REPLY;
}
// extract all recipients of the reply-message
if (is_object($message->headers) && in_array($mode, array(self::MODE_REPLY, self::MODE_FORWARD))) {
$a_to = rcube_mime::decode_address_list($message->headers->to, null, true, $message->headers->charset);
foreach ($a_to as $addr) {
if (!empty($addr['mailto'])) {
$a_recipients[] = strtolower($addr['mailto']);
$a_names[] = $addr['name'];
}
}
if (!empty($message->headers->cc)) {
$a_cc = rcube_mime::decode_address_list($message->headers->cc, null, true, $message->headers->charset);
foreach ($a_cc as $addr) {
if (!empty($addr['mailto'])) {
$a_recipients[] = strtolower($addr['mailto']);
$a_names[] = $addr['name'];
}
}
}
}
// decode From: address
$from = rcube_mime::decode_address_list($message->headers->from, null, true, $message->headers->charset);
$from = array_shift($from);
$from['mailto'] = strtolower($from['mailto']);
$from_idx = null;
$found_idx = array('to' => null, 'from' => null);
$check_from = in_array($mode, array(self::MODE_DRAFT, self::MODE_EDIT, self::MODE_REPLY));
// Select identity
foreach ($identities as $idx => $ident) {
// use From: header when in edit/draft or reply-to-self
if ($check_from && $from['mailto'] == strtolower($ident['email_ascii'])) {
// remember first matching identity address
if ($found_idx['from'] === null) {
$found_idx['from'] = $idx;
}
// match identity name
if ($from['name'] && $ident['name'] && $from['name'] == $ident['name']) {
$from_idx = $idx;
break;
}
}
// use replied/forwarded message recipients
if (($found = array_search(strtolower($ident['email_ascii']), $a_recipients)) !== false) {
// remember first matching identity address
if ($found_idx['to'] === null) {
$found_idx['to'] = $idx;
}
// match identity name
if ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name']) {
$from_idx = $idx;
break;
}
}
}
// If matching by name+address didn't find any matches,
// get first found identity (address) if any
if ($from_idx === null) {
$from_idx = $found_idx['to'] !== null ? $found_idx['to'] : $found_idx['from'];
}
// Try Return-Path
if ($from_idx === null && ($return_path = $message->headers->others['return-path'])) {
$return_path = array_map('strtolower', (array) $return_path);
foreach ($identities as $idx => $ident) {
// Return-Path header contains an email address, but on some mailing list
// it can be e.g. <pear-dev-return-55250-local=domain.tld@lists.php.net>
// where local@domain.tld is the address we're looking for (#1489241)
$ident1 = strtolower($ident['email_ascii']);
$ident2 = str_replace('@', '=', $ident1);
$ident1 = '<' . $ident1 . '>';
$ident2 = '-' . $ident2 . '@';
foreach ($return_path as $path) {
if ($path == $ident1 || stripos($path, $ident2)) {
$from_idx = $idx;
break 2;
}
}
}
}
// See identity_select plugin for example usage of this hook
$plugin = rcmail::get_instance()->plugins->exec_hook('identity_select', array(
'message' => $message,
'identities' => $identities,
'selected' => $from_idx
));
$selected = $plugin['selected'];
// default identity is always first on the list
return $identities[$selected !== null ? $selected : 0];
}
}
diff --git a/program/include/rcmail_utils.php b/program/include/rcmail_utils.php
index ca1359355..fced68fd4 100644
--- a/program/include/rcmail_utils.php
+++ b/program/include/rcmail_utils.php
@@ -1,389 +1,390 @@
<?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: |
| Roundcube utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Roundcube utilities
*
* @package Webmail
* @subpackage Utils
*/
class rcmail_utils
{
public static $db;
/**
* Initialize database object and connect
*
* @return rcube_db Database instance
*/
public static function db()
{
if (self::$db === null) {
$rc = rcube::get_instance();
$db = rcube_db::factory($rc->config->get('db_dsnw'));
$db->set_debug((bool)$rc->config->get('sql_debug'));
// Connect to database
$db->db_connect('w');
if (!$db->is_connected()) {
rcube::raise_error("Failed to connect to database", false, true);
}
self::$db = $db;
}
return self::$db;
}
/**
* Initialize database schema
*
* @param string $dir Directory with sql files
*/
public static function db_init($dir)
{
- $db = self::db();
+ $db = self::db();
+ $error = null;
+ $file = $dir . '/' . $db->db_provider . '.initial.sql';
- $file = $dir . '/' . $db->db_provider . '.initial.sql';
if (!file_exists($file)) {
rcube::raise_error("DDL file $file not found", false, true);
}
echo "Creating database schema... ";
if ($sql = file_get_contents($file)) {
if (!$db->exec_script($sql)) {
$error = $db->is_error();
}
}
else {
$error = "Unable to read file $file or it is empty";
}
if ($error) {
echo "[FAILED]\n";
rcube::raise_error($error, false, true);
}
else {
echo "[OK]\n";
}
}
/**
* Update database schema
*
* @param string $dir Directory with sql files
* @param string $package Component name
* @param string $ver Optional current version number
* @param array $opts Parameters (errors, quiet)
*
* @return True on success, False on failure
*/
public static function db_update($dir, $package, $ver = null, $opts = array())
{
// Check if directory exists
if (!file_exists($dir)) {
if ($opts['errors']) {
rcube::raise_error("Specified database schema directory doesn't exist.", false, true);
}
return false;
}
$db = self::db();
// Read DB schema version from database (if 'system' table exists)
if (in_array($db->table_name('system'), (array)$db->list_tables())) {
$version = self::db_version($package);
}
// DB version not found, but release version is specified
- if (!$version && $ver) {
+ if (empty($version) && $ver) {
// Map old release version string to DB schema version
// Note: This is for backward compat. only, do not need to be updated
$map = array(
'0.1-stable' => 1,
'0.1.1' => 2008030300,
'0.2-alpha' => 2008040500,
'0.2-beta' => 2008060900,
'0.2-stable' => 2008092100,
'0.2.1' => 2008092100,
'0.2.2' => 2008092100,
'0.3-stable' => 2008092100,
'0.3.1' => 2009090400,
'0.4-beta' => 2009103100,
'0.4' => 2010042300,
'0.4.1' => 2010042300,
'0.4.2' => 2010042300,
'0.5-beta' => 2010100600,
'0.5' => 2010100600,
'0.5.1' => 2010100600,
'0.5.2' => 2010100600,
'0.5.3' => 2010100600,
'0.5.4' => 2010100600,
'0.6-beta' => 2011011200,
'0.6' => 2011011200,
'0.7-beta' => 2011092800,
'0.7' => 2011111600,
'0.7.1' => 2011111600,
'0.7.2' => 2011111600,
'0.7.3' => 2011111600,
'0.7.4' => 2011111600,
'0.8-beta' => 2011121400,
'0.8-rc' => 2011121400,
'0.8.0' => 2011121400,
'0.8.1' => 2011121400,
'0.8.2' => 2011121400,
'0.8.3' => 2011121400,
'0.8.4' => 2011121400,
'0.8.5' => 2011121400,
'0.8.6' => 2011121400,
'0.9-beta' => 2012080700,
);
$version = $map[$ver];
}
// Assume last version before the 'system' table was added
if (empty($version)) {
$version = 2012080700;
}
$dir .= '/' . $db->db_provider;
if (!file_exists($dir)) {
if ($opts['errors']) {
rcube::raise_error("DDL Upgrade files for " . $db->db_provider . " driver not found.", false, true);
}
return false;
}
$dh = opendir($dir);
$result = array();
while ($file = readdir($dh)) {
if (preg_match('/^([0-9]+)\.sql$/', $file, $m) && $m[1] > $version) {
$result[] = $m[1];
}
}
sort($result, SORT_NUMERIC);
foreach ($result as $v) {
if (!$opts['quiet']) {
echo "Updating database schema ($v)... ";
}
// Ignore errors here to print the error only once
$db->set_option('ignore_errors', true);
$error = self::db_update_schema($package, $v, "$dir/$v.sql");
$db->set_option('ignore_errors', false);
if ($error) {
if (!$opts['quiet']) {
echo "[FAILED]\n";
}
if ($opts['errors']) {
rcube::raise_error("Error in DDL upgrade $v: $error", false, true);
}
return false;
}
else if (!$opts['quiet']) {
echo "[OK]\n";
}
}
return true;
}
/**
* Run database update from a single sql file
*/
protected static function db_update_schema($package, $version, $file)
{
$db = self::db();
// read DDL file
if ($sql = file_get_contents($file)) {
if (!$db->exec_script($sql)) {
return $db->is_error();
}
}
// escape if 'system' table does not exist
if ($version < 2013011000) {
return;
}
$system_table = $db->table_name('system', true);
$db->query("UPDATE " . $system_table
. " SET `value` = ?"
. " WHERE `name` = ?",
$version, $package . '-version');
if (!$db->is_error() && !$db->affected_rows()) {
$db->query("INSERT INTO " . $system_table
." (`name`, `value`) VALUES (?, ?)",
$package . '-version', $version);
}
return $db->is_error();
}
/**
* Get version string for the specified package
*
* @param string $package Package name
*
* @return string Version string
*/
public static function db_version($package = 'roundcube')
{
$db = self::db();
$db->query("SELECT `value`"
. " FROM " . $db->table_name('system', true)
. " WHERE `name` = ?",
$package . '-version');
$row = $db->fetch_array();
$version = preg_replace('/[^0-9]/', '', $row[0]);
return $version;
}
/**
* Removes all deleted records older than X days
*
* @param int $days Number of days
*/
public static function db_clean($days)
{
// mapping for table name => primary key
$primary_keys = array(
'contacts' => 'contact_id',
'contactgroups' => 'contactgroup_id',
);
$db = self::db();
$threshold = date('Y-m-d 00:00:00', time() - $days * 86400);
foreach (array('contacts','contactgroups','identities') as $table) {
$sqltable = $db->table_name($table, true);
// also delete linked records
// could be skipped for databases which respect foreign key constraints
if ($db->db_provider == 'sqlite' && ($table == 'contacts' || $table == 'contactgroups')) {
$pk = $primary_keys[$table];
$memberstable = $db->table_name('contactgroupmembers');
$db->query(
"DELETE FROM " . $db->quote_identifier($memberstable)
. " WHERE `$pk` IN ("
. "SELECT `$pk` FROM $sqltable"
. " WHERE `del` = 1 AND `changed` < ?"
. ")",
$threshold);
echo $db->affected_rows() . " records deleted from '$memberstable'\n";
}
// delete outdated records
$db->query("DELETE FROM $sqltable WHERE `del` = 1 AND `changed` < ?", $threshold);
echo $db->affected_rows() . " records deleted from '$table'\n";
}
}
/**
* Reindex contacts
*/
public static function indexcontacts()
{
$db = self::db();
// iterate over all users
$sql_result = $db->query("SELECT `user_id` FROM " . $db->table_name('users', true) . " ORDER BY `user_id`");
while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
echo "Indexing contacts for user " . $sql_arr['user_id'] . "...\n";
$contacts = new rcube_contacts($db, $sql_arr['user_id']);
$contacts->set_pagesize(9999);
$result = $contacts->list_records();
while ($result->count && ($row = $result->next())) {
unset($row['words']);
$contacts->update($row['ID'], $row);
}
}
echo "done.\n";
}
/**
* Modify user preferences
*
* @param string $name Option name
* @param string $value Option value
* @param int $userid Optional user identifier
* @param string $type Optional value type (bool, int, string)
*/
public static function mod_pref($name, $value, $userid = null, $type = 'string')
{
$db = self::db();
if ($userid) {
$query = '`user_id` = ' . intval($userid);
}
else {
$query = '1=1';
}
$type = strtolower($type);
if ($type == 'bool' || $type == 'boolean') {
$value = rcube_utils::get_boolean($value);
}
else if ($type == 'int' || $type == 'integer') {
$value = (int) $value;
}
// iterate over all users
$sql_result = $db->query("SELECT * FROM " . $db->table_name('users', true) . " WHERE $query");
while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
echo "Updating prefs for user " . $sql_arr['user_id'] . "...";
$user = new rcube_user($sql_arr['user_id'], $sql_arr);
$prefs = $old_prefs = $user->get_prefs();
$prefs[$name] = $value;
if ($prefs != $old_prefs) {
$user->save_prefs($prefs, true);
echo "saved.\n";
}
else {
echo "nothing changed.\n";
}
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Apr 24, 10:21 AM (9 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18888149
Default Alt Text
(284 KB)

Event Timeline