Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/config/defaults.inc.php b/config/defaults.inc.php
index a304a696b..04f58c314 100644
--- a/config/defaults.inc.php
+++ b/config/defaults.inc.php
@@ -1,1310 +1,1311 @@
<?php
// ---------------------------------------------------------------------
// WARNING: Do not edit this file! Copy configuration to config.inc.php.
// ---------------------------------------------------------------------
/*
+-----------------------------------------------------------------------+
| Default settings for all configuration options |
| |
| 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. |
+-----------------------------------------------------------------------+
*/
$config = array();
// ----------------------------------
// SQL DATABASE
// ----------------------------------
// Database connection string (DSN) for read+write operations
// Format (compatible with PEAR MDB2): db_provider://user:password@host/database
// Currently supported db_providers: mysql, pgsql, sqlite, mssql, sqlsrv, oracle
// For examples see http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php
// Note: for SQLite use absolute path (Linux): 'sqlite:////full/path/to/sqlite.db?mode=0646'
// or (Windows): 'sqlite:///C:/full/path/to/sqlite.db'
// Note: Various drivers support various additional arguments for connection,
// for Mysql: key, cipher, cert, capath, ca, verify_server_cert,
// for Postgres: application_name, sslmode, sslcert, sslkey, sslrootcert, sslcrl, sslcompression, service.
// e.g. 'mysql://roundcube:@localhost/roundcubemail?verify_server_cert=false'
$config['db_dsnw'] = 'mysql://roundcube:@localhost/roundcubemail';
// Database DSN for read-only operations (if empty write database will be used)
// useful for database replication
$config['db_dsnr'] = '';
// Disable the use of already established dsnw connections for subsequent reads
$config['db_dsnw_noread'] = false;
// use persistent db-connections
// beware this will not "always" work as expected
// see: http://www.php.net/manual/en/features.persistent-connections.php
$config['db_persistent'] = false;
// you can define specific table (and sequence) names prefix
$config['db_prefix'] = '';
// Mapping of table names and connections to use for ALL operations.
// This can be used in a setup with replicated databases and a DB master
// where read/write access to cache tables should not go to master.
$config['db_table_dsn'] = array(
// 'cache' => 'r',
// 'cache_index' => 'r',
// 'cache_thread' => 'r',
// 'cache_messages' => 'r',
);
// It is possible to specify database variable values e.g. some limits here.
// Use them if your server is not MySQL or for better performance.
// For example Roundcube uses max_allowed_packet value (in bytes)
// which limits query size for database cache operations.
$config['db_max_allowed_packet'] = null;
// ----------------------------------
// LOGGING/DEBUGGING
// ----------------------------------
// log driver: 'syslog', 'stdout' or 'file'.
$config['log_driver'] = 'file';
// date format for log entries
// (read http://php.net/manual/en/function.date.php for all format characters)
$config['log_date_format'] = 'd-M-Y H:i:s O';
// length of the session ID to prepend each log line with
// set to 0 to avoid session IDs being logged.
$config['log_session_id'] = 8;
// Default extension used for log file name
$config['log_file_ext'] = '.log';
// Syslog ident string to use, if using the 'syslog' log driver.
$config['syslog_id'] = 'roundcube';
// Syslog facility to use, if using the 'syslog' log driver.
// For possible values see installer or http://php.net/manual/en/function.openlog.php
$config['syslog_facility'] = LOG_USER;
// Activate this option if logs should be written to per-user directories.
// Data will only be logged if a directory <log_dir>/<username>/ exists and is writable.
$config['per_user_logging'] = false;
// Log sent messages to <log_dir>/sendmail.log or to syslog
$config['smtp_log'] = true;
// Log successful/failed logins to <log_dir>/userlogins.log or to syslog
$config['log_logins'] = false;
// Log session debug information/authentication errors to <log_dir>/session.log or to syslog
$config['session_debug'] = false;
// Log SQL queries to <log_dir>/sql.log or to syslog
$config['sql_debug'] = false;
// Log IMAP conversation to <log_dir>/imap.log or to syslog
$config['imap_debug'] = false;
// Log LDAP conversation to <log_dir>/ldap.log or to syslog
$config['ldap_debug'] = false;
// Log SMTP conversation to <log_dir>/smtp.log or to syslog
$config['smtp_debug'] = false;
// Log Memcache conversation to <log_dir>/memcache.log or to syslog
$config['memcache_debug'] = false;
// Log APC conversation to <log_dir>/apc.log or to syslog
$config['apc_debug'] = false;
// Log Redis conversation to <log_dir>/redis.log or to syslog
$config['redis_debug'] = false;
// ----------------------------------
// IMAP
// ----------------------------------
// The IMAP host chosen to perform the log-in.
// Leave blank to show a textbox at login, give a list of hosts
// to display a pulldown menu or set one host as string.
// Enter hostname with prefix ssl:// to use Implicit TLS, or use
// prefix tls:// to use STARTTLS.
// Supported replacement variables:
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %s - domain name after the '@' from e-mail address provided at login screen
// For example %n = mail.domain.tld, %t = domain.tld
// WARNING: After hostname change update of mail_host column in users table is
// required to match old user data records with the new host.
$config['default_host'] = 'localhost';
// TCP port used for IMAP connections
$config['default_port'] = 143;
// IMAP authentication method (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or null).
// Use 'IMAP' to authenticate with IMAP LOGIN command.
// By default the most secure method (from supported) will be selected.
$config['imap_auth_type'] = null;
// IMAP socket context options
// See http://php.net/manual/en/context.ssl.php
// The example below enables server certificate validation
//$config['imap_conn_options'] = array(
// 'ssl' => array(
// 'verify_peer' => true,
// 'verify_depth' => 3,
// 'cafile' => '/etc/openssl/certs/ca.crt',
// ),
// );
// Note: These can be also specified as an array of options indexed by hostname
$config['imap_conn_options'] = null;
// IMAP connection timeout, in seconds. Default: 0 (use default_socket_timeout)
$config['imap_timeout'] = 0;
// Optional IMAP authentication identifier to be used as authorization proxy
$config['imap_auth_cid'] = null;
// Optional IMAP authentication password to be used for imap_auth_cid
$config['imap_auth_pw'] = null;
// If you know your imap's folder delimiter, you can specify it here.
// Otherwise it will be determined automatically
$config['imap_delimiter'] = null;
// If you know your imap's folder vendor, you can specify it here.
// Otherwise it will be determined automatically. Use lower-case
// identifiers, e.g. 'dovecot', 'cyrus', 'gimap', 'hmail', 'uw-imap'.
$config['imap_vendor'] = null;
// If IMAP server doesn't support NAMESPACE extension, but you're
// using shared folders or personal root folder is non-empty, you'll need to
// set these options. All can be strings or arrays of strings.
// Note: Folders need to be ended with directory separator, e.g. "INBOX."
// (special directory "~" is an exception to this rule)
// Note: These can be used also to overwrite server's namespaces
// Note: Set these to FALSE to disable access to specified namespace
$config['imap_ns_personal'] = null;
$config['imap_ns_other'] = null;
$config['imap_ns_shared'] = null;
// By default IMAP capabilities are readed after connection to IMAP server
// In some cases, e.g. when using IMAP proxy, there's a need to refresh the list
// after login. Set to True if you've got this case.
$config['imap_force_caps'] = false;
// By default list of subscribed folders is determined using LIST-EXTENDED
// extension if available. Some servers (dovecot 1.x) returns wrong results
// for shared namespaces in this case. https://github.com/roundcube/roundcubemail/issues/2474
// Enable this option to force LSUB command usage instead.
// Deprecated: Use imap_disabled_caps = array('LIST-EXTENDED')
$config['imap_force_lsub'] = false;
// Some server configurations (e.g. Courier) doesn't list folders in all namespaces
// Enable this option to force listing of folders in all namespaces
$config['imap_force_ns'] = false;
// Some servers return hidden folders (name starting witha dot)
// from user home directory. IMAP RFC does not forbid that.
// Enable this option to hide them and disable possibility to create such.
$config['imap_skip_hidden_folders'] = false;
// Some servers do not support folders with both folders and messages inside
// If your server supports that use true, if it does not, use false.
// By default it will be determined automatically (once per user session).
$config['imap_dual_use_folders'] = null;
// List of disabled imap extensions.
// Use if your IMAP server has broken implementation of some feature
// and you can't remove it from CAPABILITY string on server-side.
// For example UW-IMAP server has broken ESEARCH.
// Note: Because the list is cached, re-login is required after change.
$config['imap_disabled_caps'] = array();
// Log IMAP session identifiers after each IMAP login.
// This is used to relate IMAP session with Roundcube user sessions
$config['imap_log_session'] = false;
// Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'.
$config['imap_cache'] = null;
// Enables messages cache. Only 'db' cache is supported.
// This requires an IMAP server that supports QRESYNC and CONDSTORE
// extensions (RFC7162). See synchronize() in program/lib/Roundcube/rcube_imap_cache.php
// for further info, or if you experience syncing problems.
$config['messages_cache'] = false;
// Lifetime of IMAP indexes cache. Possible units: s, m, h, d, w
$config['imap_cache_ttl'] = '10d';
// Lifetime of messages cache. Possible units: s, m, h, d, w
$config['messages_cache_ttl'] = '10d';
// Maximum cached message size in kilobytes.
// Note: On MySQL this should be less than (max_allowed_packet - 30%)
$config['messages_cache_threshold'] = 50;
// ----------------------------------
// SMTP
// ----------------------------------
// SMTP server host (for sending mails).
// Enter hostname with prefix ssl:// to use Implicit TLS, or use
// prefix tls:// to use STARTTLS.
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
// To specify differnt SMTP servers for different IMAP hosts provide an array
// of IMAP host (no prefix or port) and SMTP server e.g. array('imap.example.com' => 'smtp.example.net')
$config['smtp_server'] = 'localhost';
// SMTP port. Use 25 for cleartext, 465 for Implicit TLS, or 587 for STARTTLS (default)
$config['smtp_port'] = 587;
// SMTP username (if required) if you use %u as the username Roundcube
// will use the current username for login
$config['smtp_user'] = '%u';
// SMTP password (if required) if you use %p as the password Roundcube
// will use the current user's password for login
$config['smtp_pass'] = '%p';
// SMTP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or empty to use
// best server supported one)
$config['smtp_auth_type'] = null;
// Optional SMTP authentication identifier to be used as authorization proxy
$config['smtp_auth_cid'] = null;
// Optional SMTP authentication password to be used for smtp_auth_cid
$config['smtp_auth_pw'] = null;
// SMTP HELO host
// Hostname to give to the remote server for SMTP 'HELO' or 'EHLO' messages
// Leave this blank and you will get the server variable 'server_name' or
// localhost if that isn't defined.
$config['smtp_helo_host'] = '';
// SMTP connection timeout, in seconds. Default: 0 (use default_socket_timeout)
// Note: There's a known issue where using ssl connection with
// timeout > 0 causes connection errors (https://bugs.php.net/bug.php?id=54511)
$config['smtp_timeout'] = 0;
// SMTP socket context options
// See http://php.net/manual/en/context.ssl.php
// The example below enables server certificate validation, and
// requires 'smtp_timeout' to be non zero.
// $config['smtp_conn_options'] = array(
// 'ssl' => array(
// 'verify_peer' => true,
// 'verify_depth' => 3,
// 'cafile' => '/etc/openssl/certs/ca.crt',
// ),
// );
// Note: These can be also specified as an array of options indexed by hostname
$config['smtp_conn_options'] = null;
// ----------------------------------
// LDAP
// ----------------------------------
// Type of LDAP cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'.
$config['ldap_cache'] = 'db';
// Lifetime of LDAP cache. Possible units: s, m, h, d, w
$config['ldap_cache_ttl'] = '10m';
// ----------------------------------
// CACHE(S)
// ----------------------------------
// Use these hosts for accessing memcached
// Define any number of hosts in the form of hostname:port or unix:///path/to/socket.file
// Example: array('localhost:11211', '192.168.1.12:11211', 'unix:///var/tmp/memcached.sock');
$config['memcache_hosts'] = null;
// Controls the use of a persistent connections to memcache servers
// See http://php.net/manual/en/memcache.addserver.php
$config['memcache_pconnect'] = true;
// Value in seconds which will be used for connecting to the daemon
// See http://php.net/manual/en/memcache.addserver.php
$config['memcache_timeout'] = 1;
// Controls how often a failed server will be retried (value in seconds).
// Setting this parameter to -1 disables automatic retry.
// See http://php.net/manual/en/memcache.addserver.php
$config['memcache_retry_interval'] = 15;
// Use these hosts for accessing Redis.
// Currently only one host is supported. Cluster support may come in a future release.
// You can pass 4 fields, host, port (optional), database (optional) and password (optional).
// Unset fields will be set to the default values host=127.0.0.1, port=6379.
// Examples:
// array('localhost:6379');
// array('192.168.1.1:6379:1:secret');
// array('unix:///var/run/redis/redis-server.sock:1:secret');
$config['redis_hosts'] = null;
// Maximum size of an object in memcache (in bytes). Default: 2MB
$config['memcache_max_allowed_packet'] = '2M';
// Maximum size of an object in APC cache (in bytes). Default: 2MB
$config['apc_max_allowed_packet'] = '2M';
// Maximum size of an object in Redis cache (in bytes). Default: 2MB
$config['redis_max_allowed_packet'] = '2M';
// ----------------------------------
// SYSTEM
// ----------------------------------
// THIS OPTION WILL ALLOW THE INSTALLER TO RUN AND CAN EXPOSE SENSITIVE CONFIG DATA.
// ONLY ENABLE IT IF YOU'RE REALLY SURE WHAT YOU'RE DOING!
$config['enable_installer'] = false;
// don't allow these settings to be overridden by the user
$config['dont_override'] = array();
// List of disabled UI elements/actions
$config['disabled_actions'] = array();
// define which settings should be listed under the 'advanced' block
// which is hidden by default
$config['advanced_prefs'] = array();
// provide an URL where a user can get support for this Roundcube installation
// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!
$config['support_url'] = '';
// Logo image replacement. Specifies location of the image as:
// - URL relative to the document root of this Roundcube installation
// - full URL with http:// or https:// prefix
// - URL relative to the current skin folder (when starts with a '/')
//
// An array can be used to specify different logos for specific template files
// The array key specifies the place(s) the logo should be applied to and
// is made up of (up to) 3 parts:
// - skin name prefix (always with colon, can be replaced with *)
// - template name (or * for all templates)
// - logo type - it is used for logos used on multiple templates
// the available types include '[favicon]' for favicon, '[print]' for logo on all print
// templates (e.g. messageprint, contactprint) and '[small]' for small screen logo in supported skins
+// '[dark]' and '[small-dark]' for dark mode logo in supported skins
//
// Example config for skin_logo
/*
array(
// show the image /images/logo_login_small.png for the Login screen in the Elastic skin on small screens
"elastic:login[small]" => "/images/logo_login_small.png",
// show the image /images/logo_login.png for the Login screen in the Elastic skin
"elastic:login" => "/images/logo_login.png",
// show the image /images/logo_small.png in the Elastic skin
"elastic:*[small]" => "/images/logo_small.png",
// show the image /images/larry.png in the Larry skin
"larry:*" => "/images/larry.png",
// show the image /images/logo_login.png on the login template in all skins
"login" => "/images/logo_login.png",
// show the image /images/logo_print.png for all print type logos in all skins
"[print]" => "/images/logo_print.png",
);
*/
$config['skin_logo'] = null;
// automatically create a new Roundcube user when log-in the first time.
// a new user will be created once the IMAP login succeeds.
// set to false if only registered users can use this service
$config['auto_create_user'] = true;
// Enables possibility to log in using email address from user identities
$config['user_aliases'] = false;
// use this folder to store log files
// must be writeable for the user who runs PHP process (Apache user if mod_php is being used)
// This is used by the 'file' log driver.
$config['log_dir'] = RCUBE_INSTALL_PATH . 'logs/';
// Location of temporary saved files such as attachments and cache files
// must be writeable for the user who runs PHP process (Apache user if mod_php is being used)
$config['temp_dir'] = RCUBE_INSTALL_PATH . 'temp/';
// expire files in temp_dir after 48 hours
// possible units: s, m, h, d, w
$config['temp_dir_ttl'] = '48h';
// Enforce connections over https
// With this option enabled, all non-secure connections will be redirected.
// It can be also a port number, hostname or hostname:port if they are
// different than default HTTP_HOST:443
$config['force_https'] = false;
// tell PHP that it should work as under secure connection
// even if it doesn't recognize it as secure ($_SERVER['HTTPS'] is not set)
// e.g. when you're running Roundcube behind a https proxy
// this option is mutually exclusive to 'force_https' and only either one of them should be set to true.
$config['use_https'] = false;
// Allow browser-autocompletion on login form.
// 0 - disabled, 1 - username and host only, 2 - username, host, password
$config['login_autocomplete'] = 0;
// Forces conversion of logins to lower case.
// 0 - disabled, 1 - only domain part, 2 - domain and local part.
// If users authentication is case-insensitive this must be enabled.
// Note: After enabling it all user records need to be updated, e.g. with query:
// UPDATE users SET username = LOWER(username);
$config['login_lc'] = 2;
// Maximum length (in bytes) of logon username and password.
$config['login_username_maxlen'] = 1024;
$config['login_password_maxlen'] = 1024;
// Logon username filter. Regular expression for use with preg_match().
// Use special value 'email' if you accept only full email addresses as user logins.
// Example: '/^[a-z0-9_@.-]+$/'
$config['login_username_filter'] = null;
// Brute-force attacks prevention.
// The value specifies maximum number of failed logon attempts per minute.
$config['login_rate_limit'] = 3;
// Includes should be interpreted as PHP files
$config['skin_include_php'] = false;
// display product name and software version on login screen
// 0 - hide product name and version number, 1 - show product name only, 2 - show product name and version number
$config['display_product_info'] = 1;
// Session lifetime in minutes
$config['session_lifetime'] = 10;
// Session domain: .example.org
$config['session_domain'] = '';
// Session name. Default: 'roundcube_sessid'
$config['session_name'] = null;
// Session authentication cookie name. Default: 'roundcube_sessauth'
$config['session_auth_name'] = null;
// Session path. Defaults to PHP session.cookie_path setting.
$config['session_path'] = null;
// Session samesite. Defaults to PHP session.cookie_samesite setting.
// Requires PHP >= 7.3.0, see https://wiki.php.net/rfc/same-site-cookie for more info
// Possible values: null (default), 'Lax', or 'Strict'
$config['session_samesite'] = null;
// Backend to use for session storage. Can either be 'db' (default), 'redis', 'memcache', or 'php'
//
// If set to 'memcache' or 'memcached', a list of servers need to be specified in 'memcache_hosts'
// Make sure the Memcache extension (https://pecl.php.net/package/memcache) version >= 2.0.0
// or the Memcached extension (https://pecl.php.net/package/memcached) version >= 2.0.0 is installed.
//
// If set to 'redis', a server needs to be specified in 'redis_hosts'
// Make sure the Redis extension (https://pecl.php.net/package/redis) version >= 2.0.0 is installed.
//
// Setting this value to 'php' will use the default session save handler configured in PHP
$config['session_storage'] = 'db';
// List of trusted proxies
// X_FORWARDED_* and X_REAL_IP headers are only accepted from these IPs
$config['proxy_whitelist'] = array();
// List of trusted host names
// Attackers can modify Host header of the HTTP request causing $_SERVER['SERVER_NAME']
// or $_SERVER['HTTP_HOST'] variables pointing to a different host, that could be used
// to collect user names and passwords. Some server configurations prevent that, but not all.
// An empty list accepts any host name. The list can contain host names
// or PCRE patterns (without // delimiters, that will be added automatically).
$config['trusted_host_patterns'] = array();
// check client IP in session authorization
$config['ip_check'] = false;
// X-Frame-Options HTTP header value sent to prevent from Clickjacking.
// Possible values: sameorigin|deny|allow-from <uri>.
// Set to false in order to disable sending the header.
$config['x_frame_options'] = 'sameorigin';
// This key is used for encrypting purposes, like storing of imap password
// in the session. For historical reasons it's called DES_key, but it's used
// with any configured cipher_method (see below).
// For the default cipher_method a required key length is 24 characters.
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';
// Encryption algorithm. You can use any method supported by OpenSSL.
// Default is set for backward compatibility to DES-EDE3-CBC,
// but you can choose e.g. AES-256-CBC which we consider a better choice.
$config['cipher_method'] = 'DES-EDE3-CBC';
// Automatically add this domain to user names for login
// Only for IMAP servers that require full e-mail addresses for login
// Specify an array with 'host' => 'domain' values to support multiple hosts
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$config['username_domain'] = '';
// Force domain configured in username_domain to be used for login.
// Any domain in username will be replaced by username_domain.
$config['username_domain_forced'] = false;
// This domain will be used to form e-mail addresses of new users
// Specify an array with 'host' => 'domain' values to support multiple hosts
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - http hostname ($_SERVER['SERVER_NAME'])
// %d - domain (http hostname without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$config['mail_domain'] = '';
// Password character set, to change the password for user
// authentication or for password change operations
$config['password_charset'] = 'UTF-8';
// How many seconds must pass between emails sent by a user
$config['sendmail_delay'] = 0;
// Message size limit. Note that SMTP server(s) may use a different value.
// This limit is verified when user attaches files to a composed message.
// Size in bytes (possible unit suffix: K, M, G)
$config['max_message_size'] = '100M';
// Maximum number of recipients per message (including To, Cc, Bcc).
// Default: 0 (no limit)
$config['max_recipients'] = 0;
// Maximum number of recipients per message exluding Bcc header.
// This is a soft limit, which means we only display a warning to the user.
// Default: 5
$config['max_disclosed_recipients'] = 5;
// Maximum allowed number of members of an address group. Default: 0 (no limit)
// If 'max_recipients' is set this value should be less or equal
$config['max_group_members'] = 0;
// Name your service. This is displayed on the login screen and in the window title
$config['product_name'] = 'Roundcube Webmail';
// Add this user-agent to message headers when sending
$config['useragent'] = 'Roundcube Webmail/'.RCUBE_VERSION;
// try to load host-specific configuration
// see https://github.com/roundcube/roundcubemail/wiki/Configuration:-Multi-Domain-Setup
// for more details
$config['include_host_config'] = false;
// path to a text file which will be added to each sent message
// paths are relative to the Roundcube root folder
$config['generic_message_footer'] = '';
// path to a text file which will be added to each sent HTML message
// paths are relative to the Roundcube root folder
$config['generic_message_footer_html'] = '';
// add a received header to outgoing mails containing the creators IP and hostname
$config['http_received_header'] = false;
// Whether or not to encrypt the IP address and the host name
// these could, in some circles, be considered as sensitive information;
// however, for the administrator, these could be invaluable help
// when tracking down issues.
$config['http_received_header_encrypt'] = false;
// number of chars allowed for line when wrapping text.
// text wrapping is done when composing/sending messages
$config['line_length'] = 72;
// send plaintext messages as format=flowed
$config['send_format_flowed'] = true;
// According to RFC2298, return receipt envelope sender address must be empty.
// If this option is true, Roundcube will use user's identity as envelope sender for MDN responses.
$config['mdn_use_from'] = false;
// Set identities access level:
// 0 - many identities with possibility to edit all params
// 1 - many identities with possibility to edit all params but not email address
// 2 - one identity with possibility to edit all params
// 3 - one identity with possibility to edit all params but not email address
// 4 - one identity with possibility to edit only signature
$config['identities_level'] = 0;
// Maximum size of uploaded image in kilobytes
// Images (in html signatures) are stored in database as data URIs
$config['identity_image_size'] = 64;
// Mimetypes supported by the browser.
// Attachments of these types will open in a preview window.
// Either a comma-separated list or an array. Default list includes:
// text/plain,text/html,
// image/jpeg,image/gif,image/png,image/bmp,image/tiff,image/webp,
// application/x-javascript,application/pdf,application/x-shockwave-flash
$config['client_mimetypes'] = null;
// Path to a local mime magic database file for PHPs finfo extension.
// Set to null if the default path should be used.
$config['mime_magic'] = null;
// Absolute path to a local mime.types mapping table file.
// This is used to derive mime-types from the filename extension or vice versa.
// Such a file is usually part of the apache webserver. If you don't find a file named mime.types on your system,
// download it from http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
$config['mime_types'] = null;
// path to imagemagick identify binary (if not set we'll use Imagick or GD extensions)
$config['im_identify_path'] = null;
// path to imagemagick convert binary (if not set we'll use Imagick or GD extensions)
$config['im_convert_path'] = null;
// Size of thumbnails from image attachments displayed below the message content.
// Note: whether images are displayed at all depends on the 'inline_images' option.
// Set to 0 to display images in full size.
$config['image_thumbnail_size'] = 240;
// maximum size of uploaded contact photos in pixel
$config['contact_photo_size'] = 160;
// Enable DNS checking for e-mail address validation
$config['email_dns_check'] = false;
// Disables saving sent messages in Sent folder (like gmail) (Default: false)
// Note: useful when SMTP server stores sent mail in user mailbox
$config['no_save_sent_messages'] = false;
// Improve system security by using special URL with security token.
// This can be set to a number defining token length. Default: 16.
// Warning: This requires http server configuration. Sample:
// RewriteRule ^/roundcubemail/[a-zA-Z0-9]{16}/(.*) /roundcubemail/$1 [PT]
// Alias /roundcubemail /var/www/roundcubemail/
// Note: Use assets_path to not prevent the browser from caching assets
$config['use_secure_urls'] = false;
// Allows to define separate server/path for image/js/css files
// Warning: If the domain is different cross-domain access to some
// resources need to be allowed
// Sample:
// <FilesMatch ".(eot|ttf|woff)">
// Header set Access-Control-Allow-Origin "*"
// </FilesMatch>
$config['assets_path'] = '';
// While assets_path is for the browser, assets_dir informs
// PHP code about the location of asset files in filesystem
$config['assets_dir'] = '';
// ----------------------------------
// PLUGINS
// ----------------------------------
// List of active plugins (in plugins/ directory)
$config['plugins'] = array();
// ----------------------------------
// USER INTERFACE
// ----------------------------------
// default messages sort column. Use empty value for default server's sorting,
// or 'arrival', 'date', 'subject', 'from', 'to', 'fromto', 'size', 'cc'
$config['message_sort_col'] = '';
// default messages sort order
$config['message_sort_order'] = 'DESC';
// These cols are shown in the message list. Available cols are:
// subject, from, to, fromto, cc, replyto, date, size, status, flag, attachment, priority
$config['list_cols'] = array('subject', 'status', 'fromto', 'date', 'size', 'flag', 'attachment');
// the default locale setting (leave empty for auto-detection)
// RFC1766 formatted language name like en_US, de_DE, de_CH, fr_FR, pt_BR
$config['language'] = null;
// use this format for date display (date or strftime format)
$config['date_format'] = 'Y-m-d';
// give this choice of date formats to the user to select from
// Note: do not use ambiguous formats like m/d/Y
$config['date_formats'] = array('Y-m-d', 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y');
// use this format for time display (date or strftime format)
$config['time_format'] = 'H:i';
// give this choice of time formats to the user to select from
$config['time_formats'] = array('G:i', 'H:i', 'g:i a', 'h:i A');
// use this format for short date display (derived from date_format and time_format)
$config['date_short'] = 'D H:i';
// use this format for detailed date/time formatting (derived from date_format and time_format)
$config['date_long'] = 'Y-m-d H:i';
// store draft message is this mailbox
// leave blank if draft messages should not be stored
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['drafts_mbox'] = 'Drafts';
// store spam messages in this mailbox
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['junk_mbox'] = 'Junk';
// store sent message is this mailbox
// leave blank if sent messages should not be stored
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['sent_mbox'] = 'Sent';
// move messages to this folder when deleting them
// leave blank if they should be deleted directly
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['trash_mbox'] = 'Trash';
// automatically create the above listed default folders on user login
$config['create_default_folders'] = false;
// protect the default folders from renames, deletes, and subscription changes
$config['protect_default_folders'] = true;
// Disable localization of the default folder names listed above
$config['show_real_foldernames'] = false;
// if in your system 0 quota means no limit set this option to true
$config['quota_zero_as_unlimited'] = false;
// Make use of the built-in spell checker. It is based on GoogieSpell.
$config['enable_spellcheck'] = true;
// Enables spellchecker exceptions dictionary.
// Setting it to 'shared' will make the dictionary shared by all users.
$config['spellcheck_dictionary'] = false;
// Set the spell checking engine. Possible values:
// - 'googie' - the default (also used for connecting to Nox Spell Server, see 'spellcheck_uri' setting)
// - 'pspell' - requires the PHP Pspell module and aspell installed
// - 'enchant' - requires the PHP Enchant module
// - 'atd' - install your own After the Deadline server or check with the people at http://www.afterthedeadline.com before using their API
// Since Google shut down their public spell checking service, the default settings
// connect to http://spell.roundcube.net which is a hosted service provided by Roundcube.
// You can connect to any other googie-compliant service by setting 'spellcheck_uri' accordingly.
$config['spellcheck_engine'] = 'googie';
// For locally installed Nox Spell Server or After the Deadline services,
// please specify the URI to call it.
// Get Nox Spell Server from http://orangoo.com/labs/?page_id=72 or
// the After the Deadline package from http://www.afterthedeadline.com.
// Leave empty to use the public API of service.afterthedeadline.com
$config['spellcheck_uri'] = '';
// These languages can be selected for spell checking.
// Configure as a PHP style hash array: array('en'=>'English', 'de'=>'Deutsch');
// Leave empty for default set of available language.
$config['spellcheck_languages'] = NULL;
// Makes that words with all letters capitalized will be ignored (e.g. GOOGLE)
$config['spellcheck_ignore_caps'] = false;
// Makes that words with numbers will be ignored (e.g. g00gle)
$config['spellcheck_ignore_nums'] = false;
// Makes that words with symbols will be ignored (e.g. g@@gle)
$config['spellcheck_ignore_syms'] = false;
// Number of lines at the end of a message considered to contain the signature.
// Increase this value if signatures are not properly detected and colored
$config['sig_max_lines'] = 15;
// don't let users set pagesize to more than this value if set
$config['max_pagesize'] = 200;
// Minimal value of user's 'refresh_interval' setting (in seconds)
$config['min_refresh_interval'] = 60;
// Specifies for how many seconds the Undo button will be available
// after object delete action. Currently used with supporting address book sources.
// Setting it to 0, disables the feature.
$config['undo_timeout'] = 0;
// A static list of canned responses which are immutable for the user
$config['compose_responses_static'] = array(
// array('name' => 'Canned Response 1', 'text' => 'Static Response One'),
// array('name' => 'Canned Response 2', 'text' => 'Static Response Two'),
);
// List of HKP key servers for PGP public key lookups in Enigma/Mailvelope
// Note: Lookup is client-side, so the server must support Cross-Origin Resource Sharing
$config['keyservers'] = array('keys.openpgp.org');
// Enables use of the Main Keyring in Mailvelope? If disabled, a per-site keyring
// will be used. This is set to false for backwards compatibility.
$config['mailvelope_main_keyring'] = false;
// Mailvelope RSA bit size for newly generated keys, either 2048 or 4096.
// It maybe desirable to use 2048 for sites with many mobile users.
$config['mailvelope_keysize'] = 4096;
// ----------------------------------
// ADDRESSBOOK SETTINGS
// ----------------------------------
// This indicates which type of address book to use. Possible choises:
// 'sql' - built-in sql addressbook enabled (default),
// '' - built-in sql addressbook disabled.
// Still LDAP or plugin-added addressbooks will be available.
// BC Note: The value can actually be anything except 'sql', it does not matter.
$config['address_book_type'] = 'sql';
// In order to enable public ldap search, configure an array like the Verisign
// example further below. if you would like to test, simply uncomment the example.
// Array key must contain only safe characters, ie. a-zA-Z0-9_
$config['ldap_public'] = array();
// If you are going to use LDAP for individual address books, you will need to
// set 'user_specific' to true and use the variables to generate the appropriate DNs to access it.
//
// The recommended directory structure for LDAP is to store all the address book entries
// under the users main entry, e.g.:
//
// o=root
// ou=people
// uid=user@domain
// mail=contact@contactdomain
//
// So the base_dn would be uid=%fu,ou=people,o=root
// The bind_dn would be the same as based_dn or some super user login.
/*
* example config for Verisign directory
*
$config['ldap_public']['Verisign'] = array(
'name' => 'Verisign.com',
// Replacement variables supported in host names:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
// Note: Host can also be a full URI e.g. ldaps://hostname.local:636 (for SSL)
'hosts' => array('directory.verisign.com'),
'port' => 389,
'use_tls' => false,
'ldap_version' => 3, // using LDAPv3
'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
'user_specific' => false, // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login.
// When 'user_specific' is enabled following variables can be used in base_dn/bind_dn config:
// %fu - The full username provided, assumes the username is an email
// address, uses the username_domain value if not an email address.
// %u - The username prior to the '@'.
// %d - The domain name after the '@'.
// %dc - The domain name hierarchal string e.g. "dc=test,dc=domain,dc=com"
// %dn - DN found by ldap search when search_filter/search_base_dn are used
'base_dn' => '',
'bind_dn' => '',
'bind_pass' => '',
// It's possible to bind for an individual address book
// The login name is used to search for the DN to bind with
'search_base_dn' => '',
'search_filter' => '', // e.g. '(&(objectClass=posixAccount)(uid=%u))'
// DN and password to bind as before searching for bind DN, if anonymous search is not allowed
'search_bind_dn' => '',
'search_bind_pw' => '',
// Base DN and filter used for resolving the user's domain root DN which feeds the %dc variables
// Leave empty to skip this lookup and derive the root DN from the username domain
'domain_base_dn' => '',
'domain_filter' => '',
// Optional map of replacement strings => attributes used when binding for an individual address book
'search_bind_attrib' => array(), // e.g. array('%udc' => 'ou')
// Default for %dn variable if search doesn't return DN value
'search_dn_default' => '',
// Optional authentication identifier to be used as SASL authorization proxy
// bind_dn need to be empty
'auth_cid' => '',
// SASL authentication method (for proxy auth), e.g. DIGEST-MD5
'auth_method' => '',
// Indicates if the addressbook shall be hidden from the list.
// With this option enabled you can still search/view contacts.
'hidden' => false,
// Indicates if the addressbook shall not list contacts but only allows searching.
'searchonly' => false,
// Indicates if we can write to the LDAP directory or not.
// If writable is true then these fields need to be populated:
// LDAP_Object_Classes, required_fields, LDAP_rdn
'writable' => false,
// To create a new contact these are the object classes to specify
// (or any other classes you wish to use).
'LDAP_Object_Classes' => array('top', 'inetOrgPerson'),
// The RDN field that is used for new entries, this field needs
// to be one of the search_fields, the base of base_dn is appended
// to the RDN to insert into the LDAP directory.
'LDAP_rdn' => 'cn',
// The required fields needed to build a new contact as required by
// the object classes (can include additional fields not required by the object classes).
'required_fields' => array('cn', 'sn', 'mail'),
'search_fields' => array('mail', 'cn'), // fields to search in
// mapping of contact fields to directory attributes
// 1. for every attribute one can specify the number of values (limit) allowed.
// default is 1, a wildcard * means unlimited
// 2. another possible parameter is separator character for composite fields
// 3. it's possible to define field format for write operations, e.g. for date fields
// example: 'birthday:date[YmdHis\\Z]'
'fieldmap' => array(
// Roundcube => LDAP:limit
'name' => 'cn',
'surname' => 'sn',
'firstname' => 'givenName',
'jobtitle' => 'title',
'email' => 'mail:*',
'phone:home' => 'homePhone',
'phone:work' => 'telephoneNumber',
'phone:mobile' => 'mobile',
'phone:pager' => 'pager',
'phone:workfax' => 'facsimileTelephoneNumber',
'street' => 'street',
'zipcode' => 'postalCode',
'region' => 'st',
'locality' => 'l',
// if you country is a complex object, you need to configure 'sub_fields' below
'country' => 'c',
'organization' => 'o',
'department' => 'ou',
'jobtitle' => 'title',
'notes' => 'description',
'photo' => 'jpegPhoto',
// these currently don't work:
// 'manager' => 'manager',
// 'assistant' => 'secretary',
),
// Map of contact sub-objects (attribute name => objectClass(es)), e.g. 'c' => 'country'
'sub_fields' => array(),
// Generate values for the following LDAP attributes automatically when creating a new record
'autovalues' => array(
// 'uid' => 'md5(microtime())', // You may specify PHP code snippets which are then eval'ed
// 'mail' => '{givenname}.{sn}@mydomain.com', // or composite strings with placeholders for existing attributes
),
'sort' => 'cn', // The field to sort the listing by.
'scope' => 'sub', // search mode: sub|base|list
'filter' => '(objectClass=inetOrgPerson)', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
'fuzzy_search' => true, // server allows wildcard search
'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
'vlv_search' => false, // Use Virtual List View functions for autocompletion searches (if server supports it)
'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
'config_root_dn' => 'cn=config', // Root DN to search config entries (e.g. vlv indexes)
'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
'referrals' => false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
'dereference' => 0, // Sets the LDAP_OPT_DEREF option. One of: LDAP_DEREF_NEVER, LDAP_DEREF_SEARCHING, LDAP_DEREF_FINDING, LDAP_DEREF_ALWAYS
// Used where addressbook contains aliases to objects elsewhere in the LDAP tree.
// definition for contact groups (uncomment if no groups are supported)
// for the groups base_dn, the user replacements %fu, %u, %d and %dc work as for base_dn (see above)
// if the groups base_dn is empty, the contact base_dn is used for the groups as well
// -> in this case, assure that groups and contacts are separated due to the concernig filters!
'groups' => array(
'base_dn' => '',
'scope' => 'sub', // Search mode: sub|base|list
'filter' => '(objectClass=groupOfNames)',
'object_classes' => array('top', 'groupOfNames'), // Object classes to be assigned to new groups
'member_attr' => 'member', // Name of the default member attribute, e.g. uniqueMember
'name_attr' => 'cn', // Attribute to be used as group name
'email_attr' => 'mail', // Group email address attribute (e.g. for mailing lists)
'member_filter' => '(objectclass=*)', // Optional filter to use when querying for group members
'vlv' => false, // Use VLV controls to list groups
'class_member_attr' => array( // Mapping of group object class to member attribute used in these objects
'groupofnames' => 'member',
'groupofuniquenames' => 'uniquemember'
),
),
// this configuration replaces the regular groups listing in the directory tree with
// a hard-coded list of groups, each listing entries with the configured base DN and filter.
// if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups'
'group_filters' => array(
'departments' => array(
'name' => 'Company Departments',
'scope' => 'list',
'base_dn' => 'ou=Groups,dc=mydomain,dc=com',
'filter' => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))',
'name_attr' => 'cn',
),
'customers' => array(
'name' => 'Customers',
'scope' => 'sub',
'base_dn' => 'ou=Customers,dc=mydomain,dc=com',
'filter' => '(objectClass=inetOrgPerson)',
'name_attr' => 'sn',
),
),
);
*/
// An ordered array of the ids of the addressbooks that should be searched
// when populating address autocomplete fields server-side. ex: array('sql','Verisign');
$config['autocomplete_addressbooks'] = array('sql');
// The minimum number of characters required to be typed in an autocomplete field
// before address books will be searched. Most useful for LDAP directories that
// may need to do lengthy results building given overly-broad searches
$config['autocomplete_min_length'] = 1;
// Number of parallel autocomplete requests.
// If there's more than one address book, n parallel (async) requests will be created,
// where each request will search in one address book. By default (0), all address
// books are searched in one request.
$config['autocomplete_threads'] = 0;
// Max. numer of entries in autocomplete popup. Default: 15.
$config['autocomplete_max'] = 15;
// show address fields in this order
// available placeholders: {street}, {locality}, {zipcode}, {country}, {region}
$config['address_template'] = '{street}<br/>{locality} {zipcode}<br/>{country} {region}';
// Matching mode for addressbook search (including autocompletion)
// 0 - partial (*abc*), default
// 1 - strict (abc)
// 2 - prefix (abc*)
// Note: For LDAP sources fuzzy_search must be enabled to use 'partial' or 'prefix' mode
$config['addressbook_search_mode'] = 0;
// List of fields used on contacts list and for autocompletion searches
// Warning: These are field names not LDAP attributes (see 'fieldmap' setting)!
$config['contactlist_fields'] = array('name', 'firstname', 'surname', 'email');
// Template of contact entry on the autocompletion list.
// You can use contact fields as: name, email, organization, department, etc.
// See program/steps/addressbook/func.inc for a list
$config['contact_search_name'] = '{name} <{email}>';
// ----------------------------------
// USER PREFERENCES
// ----------------------------------
// Use this charset as fallback for message decoding
$config['default_charset'] = 'ISO-8859-1';
// Skin name: folder from skins/
$config['skin'] = 'elastic';
// Limit skins available for the user.
// Note: When not empty, it should include the default skin set in 'skin' option.
$config['skins_allowed'] = array();
// Enables using standard browser windows (that can be handled as tabs)
// instead of popup windows
$config['standard_windows'] = false;
// show up to X items in messages list view
$config['mail_pagesize'] = 50;
// show up to X items in contacts list view
$config['addressbook_pagesize'] = 50;
// sort contacts by this col (preferably either one of name, firstname, surname)
$config['addressbook_sort_col'] = 'surname';
// The way how contact names are displayed in the list.
// 0: prefix firstname middlename surname suffix (only if display name is not set)
// 1: firstname middlename surname
// 2: surname firstname middlename
// 3: surname, firstname middlename
$config['addressbook_name_listing'] = 0;
// use this timezone to display date/time
// valid timezone identifiers are listed here: php.net/manual/en/timezones.php
// 'auto' will use the browser's timezone settings
$config['timezone'] = 'auto';
// prefer displaying HTML messages
$config['prefer_html'] = true;
// display remote resources (inline images, styles)
// 0 - Never, always ask
// 1 - Ask if sender is not in address book
// 2 - Always allow
$config['show_images'] = 0;
// open messages in new window
$config['message_extwin'] = false;
// open message compose form in new window
$config['compose_extwin'] = false;
// compose html formatted messages by default
// 0 - never,
// 1 - always,
// 2 - on reply to HTML message,
// 3 - on forward or reply to HTML message
// 4 - always, except when replying to plain text message
$config['htmleditor'] = 0;
// save copies of compose messages in the browser's local storage
// for recovery in case of browser crashes and session timeout.
$config['compose_save_localstorage'] = true;
// show pretty dates as standard
$config['prettydate'] = true;
// save compose message every 300 seconds (5min)
$config['draft_autosave'] = 300;
// Interface layout. Default: 'widescreen'.
// 'widescreen' - three columns
// 'desktop' - two columns, preview on bottom
// 'list' - two columns, no preview
$config['layout'] = 'widescreen';
// Mark as read when viewing a message (delay in seconds)
// Set to -1 if messages should not be marked as read
$config['mail_read_time'] = 0;
// Clear Trash on logout
$config['logout_purge'] = false;
// Compact INBOX on logout
$config['logout_expunge'] = false;
// Display attached images below the message body
$config['inline_images'] = true;
// Encoding of long/non-ascii attachment names:
// 0 - Full RFC 2231 compatible
// 1 - RFC 2047 for 'name' and RFC 2231 for 'filename' parameter (Thunderbird's default)
// 2 - Full 2047 compatible
$config['mime_param_folding'] = 1;
// Set true if deleted messages should not be displayed
// This will make the application run slower
$config['skip_deleted'] = false;
// Set true to Mark deleted messages as read as well as deleted
// False means that a message's read status is not affected by marking it as deleted
$config['read_when_deleted'] = true;
// Set to true to never delete messages immediately
// Use 'Purge' to remove messages marked as deleted
$config['flag_for_deletion'] = false;
// Default interval for auto-refresh requests (in seconds)
// These are requests for system state updates e.g. checking for new messages, etc.
// Setting it to 0 disables the feature.
$config['refresh_interval'] = 60;
// If true all folders will be checked for recent messages
$config['check_all_folders'] = false;
// If true, after message/contact delete/move, the next message/contact will be displayed
$config['display_next'] = true;
// Default messages listing mode. One of 'threads' or 'list'.
$config['default_list_mode'] = 'list';
// 0 - Do not expand threads
// 1 - Expand all threads automatically
// 2 - Expand only threads with unread messages
$config['autoexpand_threads'] = 0;
// When replying:
// -1 - don't cite the original message
// 0 - place cursor below the original message
// 1 - place cursor above original message (top posting)
// 2 - place cursor above original message (top posting), but do not indent the quote
$config['reply_mode'] = 0;
// When replying strip original signature from message
$config['strip_existing_sig'] = true;
// Show signature:
// 0 - Never
// 1 - Always
// 2 - New messages only
// 3 - Forwards and Replies only
$config['show_sig'] = 1;
// By default the signature is placed depending on cursor position (reply_mode).
// Sometimes it might be convenient to start the reply on top but keep
// the signature below the quoted text (sig_below = true).
$config['sig_below'] = false;
// Enables adding of standard separator to the signature
$config['sig_separator'] = true;
// Use MIME encoding (quoted-printable) for 8bit characters in message body
$config['force_7bit'] = false;
// Defaults of the search field configuration.
// The array can contain a per-folder list of header fields which should be considered when searching
// The entry with key '*' stands for all folders which do not have a specific list set.
// Please note that folder names should to be in sync with $config['*_mbox'] options
$config['search_mods'] = null; // Example: array('*' => array('subject'=>1, 'from'=>1), 'Sent' => array('subject'=>1, 'to'=>1));
// Defaults of the addressbook search field configuration.
$config['addressbook_search_mods'] = null; // Example: array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1);
// Directly delete messages in Junk instead of moving to Trash
$config['delete_junk'] = false;
// Behavior if a received message requests a message delivery notification (read receipt)
// 0 = ask the user, 1 = send automatically, 2 = ignore (never send or ask)
// 3 = send automatically if sender is in addressbook, otherwise ask the user
// 4 = send automatically if sender is in addressbook, otherwise ignore
$config['mdn_requests'] = 0;
// Return receipt checkbox default state
$config['mdn_default'] = 0;
// Delivery Status Notification checkbox default state
// Note: This can be used only if smtp_server is non-empty
$config['dsn_default'] = 0;
// Place replies in the folder of the message being replied to
$config['reply_same_folder'] = false;
// Sets default mode of Forward feature to "forward as attachment"
$config['forward_attachment'] = false;
// Defines address book (internal index) to which new contacts will be added
// By default it is the first writeable addressbook.
// Note: Use '0' for built-in address book.
$config['default_addressbook'] = null;
// Enables spell checking before sending a message.
$config['spellcheck_before_send'] = false;
// Skip alternative email addresses in autocompletion (show one address per contact)
$config['autocomplete_single'] = false;
// Default font for composed HTML message.
// Supported values: Andale Mono, Arial, Arial Black, Book Antiqua, Courier New,
// Georgia, Helvetica, Impact, Tahoma, Terminal, Times New Roman, Trebuchet MS, Verdana
$config['default_font'] = 'Verdana';
// Default font size for composed HTML message.
// Supported sizes: 8pt, 10pt, 12pt, 14pt, 18pt, 24pt, 36pt
$config['default_font_size'] = '10pt';
// Enables display of email address with name instead of a name (and address in title)
$config['message_show_email'] = false;
// Default behavior of Reply-All button:
// 0 - Reply-All always
// 1 - Reply-List if mailing list is detected
$config['reply_all_mode'] = 0;
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index b60efff57..f3d44f6e1 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2548 +1,2555 @@
<?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)) {
// If the skin does not exist (could be removed or invalid),
// fallback to the skin set in the system configuration (#7271)
$skin = $this->config->system_skin;
}
$skin_path = 'skins/' . $skin;
$this->config->set('skin_path', $skin_path);
$this->base_path = $skin_path;
// register skin path(s)
$this->skin_paths = array();
$this->skins = array();
$this->load_skin($skin_path);
$this->skin_name = $skin;
$this->set_env('skin', $skin);
}
/**
* Check skin validity/existence
*
* @param string $skin Skin name
*
* @return bool True if the skin exist and is readable, False otherwise
*/
public function check_skin($skin)
{
// Sanity check to prevent from path traversal vulnerability (#1490620)
if (strpos($skin, '/') !== false || strpos($skin, "\\") !== false) {
rcube::raise_error(array(
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Invalid skin name'
), true, false);
return false;
}
$skins_allowed = $this->config->get('skins_allowed');
if (!empty($skins_allowed) && !in_array($skin, (array) $skins_allowed)) {
return false;
}
$path = RCUBE_INSTALL_PATH . 'skins/';
return !empty($skin) && is_dir($path . $skin) && is_readable($path . $skin);
}
/**
* Helper method to recursively read skin meta files and register search paths
*/
private function load_skin($skin_path)
{
$this->skin_paths[] = $skin_path;
// read meta file and check for dependencies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
$meta['path'] = $skin_path;
$path_elements = explode('/', $skin_path);
$skin_id = end($path_elements);
if (!$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) {
+ // 'type' attribute added in 1.4 was renamed 'logo-type' in 1.5
+ // check both for backwards compatibility
+ if (($template_logo = $this->get_template_logo($attrib['logo-type'] ?: $attrib['type'], $attrib['logo-match'])) !== 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;
+ $additional_logos = array();
+ $logo_types = (array) $this->config->get('additional_logo_types');
+ foreach ($logo_types as $type) {
+ if (($template_logo = $this->get_template_logo($type)) !== null) {
+ $additional_logos[$type] = $this->abs_url($template_logo);
}
}
+ if (!empty($additional_logos)) {
+ $this->set_env('additional_logos', $additional_logos);
+ }
+
if ($attrib['src']) {
$content = html::img($attrib);
}
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle(false));
}
else if ($object == 'pagetitle') {
// Deprecated, <title> will be added automatically
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'contentframe') {
if (empty($attrib['id'])) {
$attrib['id'] = 'rcm' . $this->env['task'] . 'frame';
}
// parse variables
if (preg_match('/^(config|env):([a-z0-9_]+)$/i', $attrib['src'], $matches)) {
$attrib['src'] = $this->parse_variable($matches[1], $matches[2]);
}
$content = $this->frame($attrib, true);
}
else if ($object == 'meta' || $object == 'links') {
if ($object == 'meta') {
$source = 'meta_tags';
$tag = 'meta';
$key = 'name';
$param = 'content';
}
else {
$source = 'link_tags';
$tag = 'link';
$key = 'rel';
$param = 'href';
}
foreach ($this->$source as $name => $vars) {
// $vars can be in many forms:
// - string
// - array('key' => 'val')
// - array(string, string)
// - array(array(), string)
// - array(array('key' => 'val'), array('key' => 'val'))
// normalise this for processing by checking for string array keys
$vars = is_array($vars) ? (count(array_filter(array_keys($vars), 'is_string')) > 0 ? array($vars) : $vars) : array($vars);
foreach ($vars as $args) {
// skip unset headers e.g. when extending a skin and removing a header defined in the parent
if ($args === false) {
continue;
}
$args = is_array($args) ? $args : array($param => $args);
// special handling for favicon
if ($object == 'links' && $name == 'shortcut icon' && empty($args[$param])) {
if ($href = $this->get_template_logo('favicon')) {
$args[$param] = $href;
}
else if ($href = $this->config->get('favicon', '/images/favicon.ico')) {
$args[$param] = $href;
}
}
$content .= html::tag($tag, array($key => $name, 'nl' => true) + $args);
}
}
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
if (strlen($hook['content']) && !empty($external)) {
$object_id = uniqid('TEMPLOBJECT:', true);
$this->objects[$object_id] = $hook['content'];
$hook['content'] = $object_id;
}
return $hook['content'];
// return <link> element
case 'link':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
unset($attrib['condition']);
return html::tag('link', $attrib);
// return code for a specified eval expression
case 'exp':
return html::quote($this->eval_expression($attrib['expression']));
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$value = $this->parse_variable($var[0], $var[1]);
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
case 'form':
return $this->form_tag($attrib);
}
return '';
}
/**
* Prepares template object attributes
*
* @param array &$attribs Attributes
*/
protected function prepare_object_attribs(&$attribs)
{
foreach ($attribs as $key => &$value) {
if (strpos($key, 'data-label-') === 0) {
// Localize data-label-* attributes
$value = $this->app->gettext($value);
}
elseif ($key[0] == ':') {
// Evaluate attributes with expressions and remove special character from attribute name
$attribs[substr($key, 1)] = $this->eval_expression($value);
unset($attribs[$key]);
}
}
}
/**
* Include a specific file and return it's contents
*
* @param string $file File path
*
* @return string Contents of the processed file
*/
protected function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Put objects' content back into template output
*/
protected function postrender($output)
{
// insert objects' contents
foreach ($this->objects as $key => $val) {
$output = str_replace($key, $val, $output, $count);
if ($count) {
$this->objects[$key] = null;
}
}
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
return $output;
}
/**
* Create and register a button
*
* @param array $attrib Named button attributes
*
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $s_button_count = 100;
static $disabled_actions = null;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (!($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 (!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) && 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();
}
$username = rcube_utils::idn_to_utf8($username);
return html::quote($username);
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
protected function login_form($attrib)
{
$default_host = $this->config->get('default_host');
$autocomplete = (int) $this->config->get('login_autocomplete');
$username_filter = $this->config->get('login_username_filter');
$_SESSION['temp'] = true;
// save original url
$url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING'])) {
$url = $_SERVER['QUERY_STRING'];
}
// Disable autocapitalization on iPad/iPhone (#1488609)
$attrib['autocapitalize'] = 'off';
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
// set atocomplete attribute
$user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');
if ($username_filter && strtolower($username_filter) == 'email') {
$user_attrib['type'] = 'email';
}
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
$input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required')
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'required' => 'required')
+ $attrib + $pass_attrib);
$input_host = null;
$hide_host = false;
if (is_array($default_host) && count($default_host) > 1) {
$input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost', 'class' => 'custom-select'));
foreach ($default_host as $key => $value) {
if (!is_array($value)) {
$input_host->add($value, (is_numeric($key) ? $value : $key));
}
else {
$input_host = null;
break;
}
}
}
else if (is_array($default_host) && ($host = key($default_host)) !== null) {
$hide_host = true;
$input_host = new html_hiddenfield(array(
'name' => '_host', 'id' => 'rcmloginhost', 'value' => is_numeric($host) ? $default_host[$host] : $host) + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost', 'class' => 'form-control')
+ $attrib + $host_attrib);
}
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', html::quote($this->app->gettext('username'))));
$table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
$table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
$table->add('input', $input_pass->show());
// add host selection row
if (is_object($input_host) && !$hide_host) {
$table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
$table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
if ($hide_host) {
$out .= $input_host->show();
}
if (rcube_utils::get_boolean($attrib['submit'])) {
$button_attr = array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit');
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// 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'])) {
$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)
+ * @param string $type Type of the logo to check for (e.g. 'print' or 'small')
+ * default is null (no special type)
+ * @param string $match (optional) 'all' = type, template or wildcard, 'template' = type or template
+ * Note: when type is specified matches are limited to type only unless $match is defined
*
* @return string image URL
*/
- protected function get_template_logo($type = null)
+ protected function get_template_logo($type = null, $match = null)
{
$template_logo = null;
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
$template_names = array(
$this->skin_name . ':' . $this->template_name . '[' . $type . ']',
$this->skin_name . ':' . $this->template_name,
$this->skin_name . ':*[' . $type . ']',
$this->skin_name . ':[' . $type . ']',
$this->skin_name . ':*',
'*:' . $this->template_name . '[' . $type . ']',
'*:' . $this->template_name,
'*:*[' . $type . ']',
'*:[' . $type . ']',
$this->template_name . '[' . $type . ']',
$this->template_name,
'*[' . $type . ']',
'[' . $type . ']',
'*',
);
- if (!empty($type)) {
- // Use strict matching, remove wild card options
- $template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
+ if (empty($type)) {
+ // If no type provided then remove those options from the list
+ $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);
+ elseif ($match === null) {
+ // Type specified with no special matching requirements so remove all none type specific options from the list
+ $template_names = preg_grep("/\]$/", $template_names);
+ }
+
+ if ($match == 'template') {
+ // Match only specific type or template name
+ $template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
}
foreach ($template_names as $key) {
if (isset($logo[$key])) {
$template_logo = $logo[$key];
break;
}
}
}
else {
$template_logo = $logo;
}
}
return $template_logo;
}
}
diff --git a/skins/classic/templates/contactprint.html b/skins/classic/templates/contactprint.html
index ec7c00c67..49dbfdcc7 100644
--- a/skins/classic/templates/contactprint.html
+++ b/skins/classic/templates/contactprint.html
@@ -1,20 +1,20 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<roundcube:object name="meta" />
<roundcube:object name="links" />
<link rel="stylesheet" type="text/css" href="/print.css" />
</head>
<body>
-<roundcube:object name="logo" src="/images/roundcube_logo.png" id="logo" border="0" type="print" />
+<roundcube:object name="logo" src="/images/roundcube_logo.png" id="logo" border="0" logo-type="print" logo-match="all" />
<div id="contact-details">
<div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
<roundcube:object name="contacthead" id="contacthead" />
<div style="clear:both"></div>
<roundcube:object name="contactdetails" />
</div>
</body>
</html>
diff --git a/skins/classic/templates/messageprint.html b/skins/classic/templates/messageprint.html
index 29740664c..31acdde87 100644
--- a/skins/classic/templates/messageprint.html
+++ b/skins/classic/templates/messageprint.html
@@ -1,19 +1,19 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<roundcube:object name="meta" />
<roundcube:object name="links" />
<link rel="stylesheet" type="text/css" href="/print.css" />
</head>
<body>
-<roundcube:object name="logo" src="/images/roundcube_logo.png" id="logo" border="0" type="print" />
+<roundcube:object name="logo" src="/images/roundcube_logo.png" id="logo" border="0" logo-type="print" logo-match="all" />
<div id="printmessageframe">
<roundcube:object name="messageHeaders" class="headers-table" cellspacing="0" cellpadding="2" max="10" />
<roundcube:object name="messageAttachments" id="attachment-list" />
<roundcube:object name="messageBody" id="messagebody" showImages="false" />
</div>
</body>
</html>
diff --git a/skins/elastic/meta.json b/skins/elastic/meta.json
index 4b21183de..4b3f0032a 100644
--- a/skins/elastic/meta.json
+++ b/skins/elastic/meta.json
@@ -1,19 +1,20 @@
{
"name": "Elastic",
"author": "Aleksander Machniak",
"license": "Creative Commons Attribution-ShareAlike",
"license-url": "http://creativecommons.org/licenses/by-sa/3.0/",
"config": {
"supported_layouts": ["widescreen"],
"jquery_ui_colors_theme": "bootstrap",
"embed_css_location": "/styles/embed.css",
"editor_css_location": "/styles/embed.css",
"dark_mode_support": true,
- "media_browser_css_location": "none"
+ "media_browser_css_location": "none",
+ "additional_logo_types": ["dark", "small", "small-dark"]
},
"meta": {
"viewport": "width=device-width, initial-scale=1.0, shrink-to-fit=no, maximum-scale=1.0",
"theme-color": "#f4f4f4",
"msapplication-navbutton-color": "#f4f4f4"
}
}
diff --git a/skins/elastic/templates/contactprint.html b/skins/elastic/templates/contactprint.html
index ce03ee50f..39ece44d6 100644
--- a/skins/elastic/templates/contactprint.html
+++ b/skins/elastic/templates/contactprint.html
@@ -1,19 +1,19 @@
<roundcube:include file="includes/layout.html" />
-<roundcube:object name="logo" id="logo" alt="Logo" type="print" class="float-sm-right" />
+<roundcube:object name="logo" id="logo" alt="Logo" logo-type="print" logo-match="template" class="float-sm-right" />
<div class="print-content formcontent" role="main">
<div class="contact-header">
<div id="contactphoto" class="contact-photo">
<roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.svg"
placeholderGroup="/images/contactgroup.svg" />
</div>
<roundcube:object name="contacthead" id="contacthead" class="contact-head readonly" with-source="true" />
</div>
<div id="contacttabs">
<roundcube:object name="contactdetails" fieldset-class="propform groupped readonly"
short-legend-labels="true" />
</div>
</div>
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/templates/includes/menu.html b/skins/elastic/templates/includes/menu.html
index 3cbf5daf3..352eeb58a 100644
--- a/skins/elastic/templates/includes/menu.html
+++ b/skins/elastic/templates/includes/menu.html
@@ -1,42 +1,42 @@
<div id="layout-menu">
<h2 id="aria-label-tasknav" class="voice"><roundcube:label name="arialabeltasknav" /></h2>
<div class="popover-header">
- <roundcube:object name="logo" src="/images/logo.svg" data-src-small="0" id="logo" alt="Logo" />
+ <roundcube:object name="logo" src="/images/logo.svg" id="logo" alt="Logo" />
<a class="button icon cancel"><span class="inner"><roundcube:label name="close" /></span></a>
</div>
<div id="taskmenu" class="menu toolbar" role="navigation" aria-labelledby="aria-label-tasknav">
<span class="action-buttons">
<roundcube:if condition="env:action == 'compose' and env:task == 'mail'" />
<roundcube:button command="compose" type="link" class="compose selected"
label="compose" title="writenewmessage" innerclass="inner" />
<roundcube:else />
<roundcube:button command="compose" type="link" class="compose"
label="compose" title="writenewmessage" innerclass="inner"
data-fab="true" data-fab-task="mail" data-fab-action="none" />
<roundcube:endif />
</span>
<roundcube:if condition="env:action == 'compose' and env:task == 'mail'" />
<roundcube:button command="mail" label="mail" type="link" innerClass="inner"
class="mail" />
<roundcube:else />
<roundcube:button command="mail" label="mail" type="link" innerClass="inner"
class="mail" classSel="mail selected" />
<roundcube:endif />
<roundcube:button command="addressbook" label="contacts" type="link" innerClass="inner"
class="contacts" classSel="contacts selected" />
<roundcube:container name="taskbar" id="taskmenu" />
<roundcube:button command="settings" label="settings" type="link" innerClass="inner"
class="settings" classSel="settings selected" />
<span class="special-buttons">
<roundcube:if condition="config:dark_mode_support" />
<roundcube:add_label name="darkmode" />
<roundcube:add_label name="lightmode" />
<roundcube:button name="theme" label="darkmode" type="link" innerClass="inner" class="theme dark" />
<roundcube:endif />
<roundcube:button name="about" label="about" type="link"
class="about" innerClass="inner" onclick="UI.about_dialog(this)" />
<roundcube:button command="logout" label="logout" type="link"
class="logout" innerClass="inner" />
</span>
</div>
</div>
diff --git a/skins/elastic/templates/login.html b/skins/elastic/templates/login.html
index 17e8f5054..0279481da 100644
--- a/skins/elastic/templates/login.html
+++ b/skins/elastic/templates/login.html
@@ -1,24 +1,24 @@
<roundcube:include file="includes/layout.html" />
<h1 class="voice"><roundcube:object name="productname" /> <roundcube:label name="login" /></h1>
<div id="layout-content" class="selected no-navbar" role="main">
- <roundcube:object name="logo" src="/images/logo.svg" data-src-small="0" id="logo" alt="Logo" />
+ <roundcube:object name="logo" src="/images/logo.svg" id="logo" alt="Logo" />
<roundcube:form id="login-form" name="login-form" method="post" class="propform">
<roundcube:object name="loginform" form="login-form" size="40" submit=true class="form-control" />
<div id="login-footer" role="contentinfo">
<roundcube:object name="productname" condition="config:display_product_info &gt; 0" />
<roundcube:object name="version" condition="config:display_product_info == 2" />
<roundcube:if condition="config:support_url" />
&nbsp;&bull;&nbsp; <a href="<roundcube:var name='config:support_url' />" target="_blank" class="support-link"><roundcube:label name="support" /></a>
<roundcube:endif />
<roundcube:container name="loginfooter" id="login-footer" />
</div>
</form>
</div>
<noscript>
<p class="noscriptwarning"><roundcube:label name="noscriptwarning" /></p>
</noscript>
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/templates/messageprint.html b/skins/elastic/templates/messageprint.html
index 6d2135c62..7764d3f4f 100644
--- a/skins/elastic/templates/messageprint.html
+++ b/skins/elastic/templates/messageprint.html
@@ -1,27 +1,27 @@
<roundcube:include file="includes/layout.html" />
-<roundcube:object name="logo" id="logo" alt="Logo" type="print" class="float-sm-right" />
+<roundcube:object name="logo" id="logo" alt="Logo" logo-type="print" logo-match="template" class="float-sm-right" />
<div class="print-content" role="main">
<div id="message-header">
<h2 class="subject">
<roundcube:object name="messageHeaders" valueOf="subject" />
</h2>
<div class="header">
<roundcube:object name="contactphoto" class="contactphoto" placeholder="/images/contactpic.svg" />
<div class="header-content details-view">
<roundcube:object name="messageHeaders" class="header-headers" addicon="virtual" exclude="subject" max="10" />
</div>
</div>
</div>
<div id="message-content">
<div role="region">
<roundcube:object name="messageAttachments" id="attachment-list" class="attachmentslist" />
</div>
<div role="region">
<roundcube:object name="messageBody" id="messagebody" headertableclass="headers-table" />
</div>
</div>
</div>
<roundcube:include file="includes/footer.html" />
diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js
index 4fba2524f..b768ca81a 100644
--- a/skins/elastic/ui.js
+++ b/skins/elastic/ui.js
@@ -1,4258 +1,4268 @@
/**
* Roundcube webmail functions for the Elastic skin
*
* Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
"use strict";
function rcube_elastic_ui()
{
var prefs, ref = this,
mode = 'normal', // one of: large, normal, small, phone
color_mode = 'light', // 'light' or 'dark'
touch = false,
ios = false,
popups_close_lock,
is_framed = rcmail.is_framed(),
env = {
config: {
standard_windows: rcmail.env.standard_windows,
message_extwin: rcmail.env.message_extwin,
compose_extwin: rcmail.env.compose_extwin,
help_open_extwin: rcmail.env.help_open_extwin
},
checkboxes: 0,
small_screen_config: {
standard_windows: true,
message_extwin: false,
compose_extwin: false,
help_open_extwin: false
}
},
menus = {},
content_buttons = [],
frame_buttons = [],
layout = {
menu: $('#layout-menu'),
sidebar: $('#layout-sidebar'),
list: $('#layout-list'),
content: $('#layout-content'),
},
buttons = {
menu: $('a.task-menu-button'),
back_sidebar: $('a.back-sidebar-button'),
back_list: $('a.back-list-button'),
back_content: $('a.back-content-button'),
};
// Public methods
this.register_content_buttons = register_content_buttons;
this.menu_hide = menu_hide;
this.menu_toggle = menu_toggle;
this.menu_destroy = menu_destroy;
this.popup_init = popup_init;
this.about_dialog = about_dialog;
this.headers_dialog = headers_dialog;
this.import_dialog = import_dialog;
this.headers_show = headers_show;
this.spellmenu = spellmenu;
this.searchmenu = searchmenu;
this.headersmenu = headersmenu;
this.header_reset = header_reset;
this.compose_status = compose_status;
this.attachmentmenu = attachmentmenu;
this.mailtomenu = mailtomenu;
this.recipient_selector = recipient_selector;
this.show_list = show_list;
this.show_sidebar = show_sidebar;
this.smart_field_init = smart_field_init;
this.smart_field_reset = smart_field_reset;
this.form_errors = form_errors;
this.switch_nav_list = switch_nav_list;
this.searchbar_init = searchbar_init;
this.pretty_checkbox = pretty_checkbox;
this.pretty_select = pretty_select;
this.datepicker_init = datepicker_init;
this.bootstrap_style = bootstrap_style;
this.toggle_list_selection = toggle_list_selection;
this.get_screen_mode = get_screen_mode;
this.is_mobile = is_mobile;
this.is_touch = is_touch;
// Detect screen size/mode
screen_mode();
// Initialize layout
layout_init();
// Convert some elements to Bootstrap style
bootstrap_style();
// Initialize responsive toolbars (have to be before popups init)
toolbar_init();
// Initialize content frame and list handlers
content_frame_init();
// Initialize menu dropdowns
dropdowns_init();
// Setup various UI elements
setup();
// Update layout after initialization
resize();
/**
* Setup procedure
*/
function setup()
{
var title, form, content_buttons = [];
// Intercept jQuery-UI dialogs...
$.ui && $.widget('ui.dialog', $.ui.dialog, {
open: function() {
// ... to unify min width for iframe'd dialogs
if ($(this.element).is('.iframe')) {
this.options.width = Math.max(576, this.options.width);
}
this._super();
// ... to re-style them on dialog open
dialog_open(this);
return this;
},
close: function() {
this._super();
// ... to close custom select dropdowns on dialog close
$('.select-menu:visible').remove();
return this;
}
});
// menu/sidebar/list button
buttons.menu.on('click', function() { app_menu(true); return false; });
buttons.back_sidebar.on('click', function() { show_sidebar(); return false; });
buttons.back_list.on('click', function() { show_list(); return false; });
buttons.back_content.on('click', function() { show_content(true); return false; });
// Initialize search forms
$('.searchbar').each(function() { searchbar_init(this); });
// Set content frame title in parent window (exclude ext-windows and dialog frames)
if (is_framed && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) {
if (title = $('h1.voice').first().text()) {
parent.$('#layout-content > .header > .header-title:not(.constant)').text(title);
}
}
else if (!is_framed) {
title = layout.content.find('.boxtitle').first().detach().text();
if (!title) {
title = $('h1.voice').first().text();
}
if (title) {
layout.content.find('.header > .header-title').text(title);
}
}
// Add content frame toolbar in the footer, for content buttons and navigation
if (!is_framed && layout.content.length && !layout.content.is('.no-navbar')
&& !layout.content.children('.frame-content').length
) {
env.frame_nav = $('<div class="footer menu toolbar content-frame-navigation hide-nav-buttons">')
.append($('<a class="button prev">')
.append($('<span class="inner"></span>').text(rcmail.gettext('previous'))))
.append($('<span class="buttons">'))
.append($('<a class="button next">')
.append($('<span class="inner"></span>').text(rcmail.gettext('next'))))
.appendTo(layout.content);
}
// Move some buttons to the frame footer toolbar
$('a[data-content-button]').each(function() {
content_buttons.push(create_cloned_button($(this)));
});
// Move form buttons from the content frame into the frame footer (on parent window)
$('.formbuttons').filter(function() { return !$(this).parent('.searchoptions').length; }).children().each(function() {
var target = $(this);
// skip non-content buttons
if (!is_framed && !target.parents('#layout-content').length) {
return;
}
if (target.is('.cancel')) {
target.addClass('hidden');
return;
}
content_buttons.push(create_cloned_button(target));
});
(is_framed ? parent.UI : ref).register_content_buttons(content_buttons);
// Mail compose features
if (form = rcmail.gui_objects.messageform) {
form = $('form[name="' + form + '"]');
// Show input elements with non-empty value
// These event handlers need to be registered before rcmail 'init' event
$('#_cc, #_bcc, #_replyto, #_followupto', $('.compose-headers')).each(function() {
$(this).on('change', function() {
$('#compose' + $(this).attr('id'))[this.value ? 'removeClass' : 'addClass']('hidden');
});
});
// We put compose options outside of the main form
// Because IE/Edge (<16) does not support 'form' attribute we'll copy
// inputs into the main form as hidden fields
// TODO: Consider doing this for IE/Edge only, just set the 'form' attribute on others
$('#compose-options').find('textarea,input,select').each(function() {
var hidden = $('<input>')
.attr({type: 'hidden', name: $(this).attr('name')})
.appendTo(form);
$(this).attr('tabindex', 2)
.on('change', function() {
hidden.val(this.type != 'checkbox' || this.checked ? $(this).val() : '');
})
.change();
});
}
// Use smart recipient inputs
// This have to be after mail compose feature above
$('[data-recipient-input]').each(function() { recipient_input(this); });
// Image upload widget
$('.image-upload').each(function() { image_upload_input(this); });
// Add HTML/Plain switcher on top of textarea with TinyMCE editor
$('textarea[data-html-editor]').each(function() { html_editor_init(this); });
$('#dragmessage-menu,#dragcontact-menu').each(function() {
rcmail.gui_object('dragmenu', this.id);
});
// Taskmenu items added by plugins do not use elastic classes (e.g help plugin)
// it's for larry skin compat. We'll assign 'selected' and icon-specific class.
$('#taskmenu > a').each(function() {
if (/button-([a-z]+)/.test(this.className)) {
var data, name = RegExp.$1,
button = find_button(this.id);
if (button && (data = button.data)) {
if (data.sel) {
data.sel = data.sel.replace('button-selected', 'selected') + ' ' + name;
}
if (data.act) {
data.act += ' ' + name;
}
rcmail.buttons[button.command][button.index] = data;
rcmail.init_button(button.command, data);
}
$(this).addClass(name);
$('.button-inner', this).addClass('inner');
}
$(this).on('mouseover', function() { rcube_webmail.long_subject_title(this, 0, $('span.inner', this)); });
});
// Some plugins use 'listbubtton' class, we'll replace it with 'button'
$('.listbutton').each(function() {
var button = find_button(this.id);
$(this).addClass('button').removeClass('listbutton');
if (button.data.sel) {
button.data.sel = button.data.sel.replace('listbutton', 'button');
}
if (button.data.act) {
button.data.act = button.data.act.replace('listbutton', 'button');
}
rcmail.buttons[button.command][button.index] = button.data;
rcmail.init_button(button.command, button.data);
});
// buttons that should be hidden on small screen devices
$('[data-hidden]').each(function() {
var m, v = $(this).data('hidden'),
parent = $(this).parent('li'),
re = /(large|big|small|phone|lbs)/g;
while (m = re.exec(v)) {
$(parent.length ? parent : this).addClass('hidden-' + m[1]);
}
});
// Modify normal checkboxes on lists so they are different
// than those used for row selection, i.e. use icons
$('[data-list]').each(function() {
$('input[type=checkbox]', this).each(function() { pretty_checkbox(this); });
});
// Assign .formcontainer class to the iframe body, when it
// contains .formcontent and .formbuttons.
if (is_framed) {
$('.formcontent').each(function() {
if ($(this).next('.formbuttons').length) {
$(this).parent().addClass('formcontainer');
}
});
}
// move "Download all attachments" button into a better location
$('#attachment-list + a.zipdownload').appendTo('.header-links');
if (ios = $('html').is('.ipad,.iphone')) {
$('.iframe-wrapper, .scroller').addClass('ios-scroll');
}
if ($('html').filter('.ipad,.iphone,.webkit.mobile,.webkit.tablet').addClass('webkit-scroller').length) {
$(layout.menu).addClass('webkit-scroller');
}
// Set .notree class on treelist widget update
$('.treelist').each(function() {
var list = this, callback = function() {
$(list)[$('.treetoggle', list).length > 0 ? 'removeClass' : 'addClass']('notree');
};
if (window.MutationObserver) {
(new MutationObserver(callback)).observe(list, {childList: true, subtree: true});
}
callback();
// Add title with full folder name on hover
// TODO: This should be done in another way, so if an entry is
// added after page load it also works there.
$('li.mailbox > a').on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
});
-
- // Store default logo path if not already set
- if (!$('#logo').data('src-default')) {
- $('#logo').data('src-default', $('#logo').attr('src'));
- }
};
/**
* Moves form buttons into the content frame actions toolbar (for mobile)
*/
function register_content_buttons(buttons)
{
// we need these buttons really only in phone mode
if (/*mode == 'phone' && */ env.frame_nav && buttons && buttons.length) {
var toolbar = env.frame_nav.children('.buttons');
content_buttons = [];
$.each(buttons, function() {
if (this.data('target')) {
content_buttons.push(this.data('target'));
}
});
toolbar.html('').append(buttons);
}
};
/**
* Registers cloned button
*/
function register_cloned_button(old_id, new_id, active_class)
{
var button = find_button(old_id);
if (button) {
rcmail.register_button(button.command, new_id, button.data.type, active_class, button.data.sel);
}
};
/**
* Create a button clone for use in toolbar
*/
function create_cloned_button(target, menu_button, add_class, always_active)
{
var popup, click = true,
button = $('<a>'),
target_id = target.attr('id') || new Date().getTime(),
button_id = target_id + '-clone',
btn_class = target[0].className + (add_class ? ' ' + add_class : '');
if (!menu_button) {
btn_class = btn_class.replace('btn-primary', 'primary').replace(/(btn[a-z-]*|button|disabled)/g, '').trim()
btn_class += ' button' + (!always_active ? ' disabled' : '');
}
else if (popup = target.data('popup')) {
button.data({popup: popup, 'toggle-button': target.data('toggle-button')});
popup_init(button[0]);
click = false;
rcmail.register_menu_button(button[0], popup);
}
button.attr({id: button_id, href: '#', 'class': btn_class})
.append($('<span class="inner">').text(target.text()));
if (click) {
button.on('click', function(e) { target.click(); });
}
if (is_framed && !menu_button) {
button.data('target', target);
frame_buttons.push($.extend({button_id: button_id}, find_button(target[0].id)));
}
else {
// Register the button to get active state updates
register_cloned_button(target_id, button_id, btn_class.replace(' disabled', ''));
}
return button;
};
/**
* Finds an rcmail button
*/
function find_button(id)
{
var i, button, command;
for (command in rcmail.buttons) {
for (i = 0; i < rcmail.buttons[command].length; i++) {
button = rcmail.buttons[command][i];
if (button.id == id) {
return {
command: command,
index: i,
data: button
};
}
}
}
};
/**
* Setup environment
*/
function layout_init()
{
// Initialize light/dark mode
color_mode_init();
// Select current layout element
env.last_selected = $('#layout > div.selected')[0];
if (!env.last_selected && layout.content.length) {
$.each(['sidebar', 'list', 'content'], function() {
if (layout[this].length) {
env.last_selected = layout[this][0];
layout[this].addClass('selected');
return false;
}
});
}
// Register resize handler
$(window).on('resize', function() {
clearTimeout(env.resize_timeout);
env.resize_timeout = setTimeout(function() { resize(); }, 25);
});
// Enable rcmail.open_window intercepting
env.open_window = rcmail.open_window;
rcmail.open_window = window_open;
rcmail
.addEventListener('message', message_displayed)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('editor-init', tinymce_init)
.addEventListener('autocomplete_create', rcmail_popup_init)
.addEventListener('googiespell_create', rcmail_popup_init)
.addEventListener('setquota', update_quota)
.addEventListener('enable-command', enable_command_handler)
.addEventListener('clonerow', pretty_checkbox_fix)
.addEventListener('init', init);
// Create floating action button(s)
if ((layout.list.length || layout.content.length) && is_mobile()) {
var fabuttons = [];
$('[data-fab]').each(function() {
var button = $(this),
task = button.data('fab-task') || '*',
action = button.data('fab-action') || '*';
if ((task == '*' || task == rcmail.env.task)
&& (action == '*' || action == rcmail.env.action || (action == 'none' && !rcmail.env.action))
) {
fabuttons.push(create_cloned_button(button, false, false, true));
}
});
if (fabuttons.length) {
$('<div class="floating-action-buttons">').append(fabuttons)
.appendTo(layout.list.length ? layout.list : layout.content);
}
}
// Initialize column resizers (must be after floating buttons)
if (layout.sidebar.length) {
splitter_init(layout.sidebar);
}
if (layout.list.length) {
splitter_init(layout.list);
}
};
/**
* rcmail 'init' event handler
*/
function init()
{
// Additional functionality on list widgets
$('[data-list]').filter('ul,table').each(function() {
var button,
table = $(this),
list = table.data('list');
if (rcmail[list] && rcmail[list].multiselect) {
var repl, button,
parent = table.parents('layout-sidebar,#layout-list,#layout-content').last(),
header = parent.find('.header'),
toolbar = header.find('ul');
if (!toolbar.length) {
toolbar = header;
}
else if (button = toolbar.find('a.select').data('toggle-button')) {
button = $('#' + button);
}
// Enable checkbox selection on list widgets
rcmail[list].enable_checkbox_selection();
// Add Select button to the list navigation bar
if (!button) {
button = $('<a>').attr({'class': 'button selection disabled', role: 'button', title: rcmail.gettext('select')})
.on('click', function() { UI.toggle_list_selection(this, table.attr('id')); })
.append($('<span class="inner">').text(rcmail.gettext('select')));
if (toolbar.is('.menu')) {
button.prependTo(toolbar).wrap('<li role="menuitem">');
// Add a button to the content toolbar menu too
if (layout.content) {
var button2 = create_cloned_button(button, true, 'hidden-big hidden-large');
$('<li role="menuitem">').append(button2).appendTo('#toolbar-menu');
button = button.add(button2);
}
}
else {
if (repl = table.data('list-select-replace')) {
$(repl).replaceWith(button);
}
else {
button.appendTo(toolbar).addClass('icon');
if (!parent.is('#layout-sidebar')) {
button.addClass('toolbar-button');
}
}
}
}
// Update Select button state on list update
rcmail.addEventListener('listupdate', function(prop) {
if (prop.list && prop.list == rcmail[list]) {
if (prop.rowcount) {
button.addClass('active').removeClass('disabled').attr('tabindex', 0);
}
else {
button.removeClass('active').addClass('disabled').attr('tabindex', -1);
}
}
});
}
// https://github.com/roundcube/elastic/issues/45
// Draggable blocks scrolling on touch devices, we'll disable it there
if (touch && rcmail[list]) {
if (typeof rcmail[list].draggable == 'function') {
rcmail[list].draggable('destroy');
}
else if (typeof rcmail[list].draggable == 'boolean') {
rcmail[list].draggable = false;
}
}
});
// Display "List is empty..." on the list
if (window.MutationObserver) {
$('[data-label-msg]').filter('ul,table').each(function() {
var fn, observer, callback,
info = $('<div class="listing-info hidden">').insertAfter(this),
table = $(this),
fn = function() {
var ext, command,
msg = table.data('label-msg'),
list = table.is('ul') ? table : table.children('tbody');
if (!rcmail.env.search_request && !rcmail.env.qsearch
&& msg && !list.children(':visible').length
) {
ext = table.data('label-ext');
command = table.data('create-command');
if (ext && (!command || rcmail.commands[command])) {
msg += ' ' + ext;
}
info.text(msg).removeClass('hidden');
return;
}
info.addClass('hidden');
};
callback = function() {
// wait until the UI stops loading and the list is visible
if (rcmail.busy || !table.is(':visible')) {
return setTimeout(callback, 250);
}
clearTimeout(env.list_timer);
env.list_timer = setTimeout(fn, 50);
};
// show/hide the message when something changes on the list
observer = new MutationObserver(callback);
observer.observe(table[0], {childList: true, subtree: true, attributes: true, attributeFilter: ['style']});
// initialize the message
callback();
});
}
// Add menu link for each attachment
if (rcmail.env.action != 'print') {
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
var phone_confirmation = function(label) {
if (mode == 'phone') {
rcmail.display_message(rcmail.gettext(label), 'confirmation');
}
};
rcmail.addEventListener('fileappended', function(e) {
if (e.attachment.complete) {
attachmentmenu_append(e.item);
if (e.attachment.mimetype == 'text/vcard' && rcmail.commands['attach-vcard']) {
phone_confirmation('vcard_attachments.vcardattached');
}
}
})
.addEventListener('managesieve.insertrow', function(o) { bootstrap_style(o.obj); })
.addEventListener('add-recipient', function() { phone_confirmation('recipientsadded'); });
rcmail.init_pagejumper('.pagenav > input');
if (rcmail.task == 'mail') {
if (rcmail.env.action == 'compose') {
rcmail.addEventListener('compose-encrypted', function(e) {
$("a.mode-html, button.attach").prop('disabled', e.active);
$('a.attach, a.responses:not(.edit)')[e.active ? 'addClass' : 'removeClass']('disabled');
});
$('#layout-sidebar > .footer:not(.pagenav) > a.button').click(function() {
if ($(this).is('.disabled')) {
rcmail.display_message(rcmail.gettext('nocontactselected'), 'warning');
}
});
// Update compose status bar on attachments list update
if (window.MutationObserver) {
var observer, list = $('#attachment-list'),
status_callback = function() { compose_status('attach', list.children().length > 0); };
observer = new MutationObserver(status_callback);
observer.observe(list[0], {childList: true});
status_callback();
}
}
// In compose/preview window we do not provide "Back" button, instead
// we modify the "Mail" button in the task menu to act like it (i.e. calls 'list' command)
if (!rcmail.env.extwin && (rcmail.env.action == 'compose' || rcmail.env.action == 'show')) {
$('a.mail', layout.menu).attr({
'aria-disabled': false,
onclick: "return rcmail.command('list','',this,event);"
});
}
// Append contact menu to all mailto: links
if (rcmail.env.action == 'preview' || rcmail.env.action == 'show') {
$('a').filter('[href^="mailto:"]').each(function() {
mailtomenu_append(this);
});
// restore headers view to last state
headers_show();
}
}
else if (rcmail.task == 'settings') {
rcmail.addEventListener('identity-encryption-show', function(p) {
bootstrap_style(p.container);
});
rcmail.addEventListener('identity-encryption-update', function(p) {
bootstrap_style(p.container);
});
}
rcmail.set_env({
thread_padding: '1.5rem',
// increase popup windows, so they do not switch to tablet mode
popup_width_small: 1025,
popup_width: 1200
});
// Update layout after initialization (again)
// In devel mode we have to wait until all styles are applied by less
if (rcmail.env.devel_mode && window.less) {
less.pageLoadFinished.then(function() {
resize();
// Re-focus the focused input field on mail compose
if (rcmail.env.compose_focus_elem) {
$(rcmail.env.compose_focus_elem).focus();
}
});
}
else {
resize();
}
// Add date format placeholder to datepicker inputs
var func, format = rcmail.env.date_format_localized;
if (format) {
func = function(input) {
$(input).filter('.datepicker').attr('placeholder', format);
// also make selects pretty
$(input).parent().find('select').each(function() { pretty_select(this); });
};
$('input.datepicker').each(function() { func(this); });
rcmail.addEventListener('insert-edit-field', func);
}
};
/**
* Initializes light/dark mode
*/
function color_mode_init()
{
if (rcmail.env.action == 'print') {
return;
}
var pref,
color_scheme = window.matchMedia('(prefers-color-scheme: dark)'),
switch_iframe_color_mode = function() {
try {
$(this.contentWindow.document).find('html')[color_mode == 'dark' ? 'addClass' : 'removeClass']('dark-mode');
}
catch(e) { /* ignore */ }
},
switch_color_mode = function() {
if (color_mode == 'dark') {
$('#taskmenu a.theme').removeClass('dark').addClass('light').find('span').text(rcmail.gettext('lightmode'));
$('html').addClass('dark-mode');
}
else {
$('#taskmenu a.theme').removeClass('light').addClass('dark').find('span').text(rcmail.gettext('darkmode'));
$('html').removeClass('dark-mode');
}
+ screen_logo(mode);
$('iframe').each(switch_iframe_color_mode);
};
// Add onclick action to the menu button
$('#taskmenu a.theme').on('click', function() {
color_mode = $(this).is('.dark') ? 'dark' : 'light';
switch_color_mode();
rcmail.set_cookie('colorMode', color_mode);
});
// Note: this does not work in IE and Safari
color_scheme.addListener(function(e) {
color_mode = e.matches ? 'dark' : 'light';
switch_color_mode();
rcmail.set_cookie('colorMode', null);
});
// We deliberately use only cookies here, not local storage
if (pref = rcmail.get_cookie('colorMode')) {
color_mode = pref;
}
else if (color_scheme.matches) {
color_mode = 'dark';
}
switch_color_mode();
$('iframe').on('load', switch_iframe_color_mode);
};
/**
* Apply bootstrap classes to html elements
*/
function bootstrap_style(context)
{
if (!context) {
context = document;
}
// Buttons
$('input.button,button', context).not('.btn').addClass('btn').not('.btn-primary,.primary,.mainaction').addClass('btn-secondary');
$('input.button.mainaction,button.primary,button.mainaction', context).addClass('btn-primary');
$('button.btn.delete,button.btn.discard', context).addClass('btn-danger');
$.each(['warning', 'error', 'information', 'confirmation'], function() {
var type = this;
$('.box' + type + ':not(.ui.alert)', context).each(function() {
alert_style(this, type, true);
});
});
// Convert structure of single dialogs (one input or just an image),
// e.g. group create, attachment rename where we use <label>Label<input></label>
if (context != document && $('.popup', context).children().length == 1) {
var content = $('.popup', context).children().first();
if (content.is('img')) {
$('.popup', context).addClass('justified');
}
else if (content.is('label')) {
var input = content.find('input').detach(),
label = content.detach(),
id = input.attr('id');
if (!id) {
input.attr('id', id = 'dialog-input-elastic');
}
$('.popup', context).addClass('formcontent').append(
$('<div class="form-group row">')
.append(label.attr('for', id).addClass('col-sm-2 col-form-label'))
.append($('<div class="col-sm-10">').append(input))
);
input.focus();
}
}
// Forms
var supported_controls = 'input:not(.button,.no-bs,[type=button],[type=radio],[type=checkbox]),textarea';
$(supported_controls, $('.propform', context)).addClass('form-control');
$('[type=checkbox]', $('.propform', context)).addClass('form-check-input');
// Note: On selects we add form-control to get consistent focus
// and to not have to create separate rules for selects and inputs
$('select', context).addClass('form-control custom-select');
if (context != document) {
$(supported_controls, context).addClass('form-control');
}
$('table.propform', context).each(function() {
var text_rows = 0, form_rows = 0;
var col_sizes = ['sm', 4, 8];
if ($(this).attr('class').match(/cols-([a-z]+)-(\d)-(\d)/)) {
col_sizes = [RegExp.$1, RegExp.$2, RegExp.$3];
}
$(this).find('> tbody > tr, > tr').each(function() {
var first, last, row = $(this),
row_classes = ['form-group', 'row'],
cells = row.children('td');
if (cells.length == 2) {
first = cells.first();
last = cells.last();
$('label', first).addClass('col-form-label');
first.addClass('col-' + col_sizes[0] + '-' + col_sizes[1]);
last.addClass('col-' + col_sizes[0] + '-' + col_sizes[2]);
if (last.find('[type=checkbox]').length == 1 && !last.find('.proplist').length) {
row_classes.push('form-check');
if (last.find('a').length) {
row_classes.push('with-link');
}
form_rows++;
}
else if (!last.find('input:not([type=hidden]),textarea,radio,select').length) {
last.addClass('form-control-plaintext');
text_rows++;
}
else {
form_rows++;
}
// style some multi-input fields
if (last.children('.datepicker') && last.children('input').length == 2) {
last.addClass('datetime');
}
}
else if (cells.length == 1) {
cells.css('width', '100%');
}
row.addClass(row_classes.join(' '));
});
if (text_rows > form_rows) {
$(this).addClass('text-only');
}
});
// Special input + anything entry
$('td.input-group', context).each(function() {
$(this).children().slice(1).addClass('input-group-append');
});
// Other forms, e.g. Contact advanced search
$('fieldset.propform:not(.groupped) div.row', context).each(function() {
var has_input = $('input:not([type=hidden]),select,textarea', this).length > 0;
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
$(this).children().last().addClass('col-sm-8' + (!has_input ? ' form-control-plaintext' : ''));
$(this).children().first().addClass('col-sm-4 col-form-label');
$(this).addClass('form-group');
});
// Contact info/edit form
$('fieldset.propform.groupped fieldset', context).each(function() {
$('.row', this).each(function() {
var label, first,
has_input = $('input,select,textarea', this).length > 0,
items = $(this).children();
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
if (items.length < 2) {
return;
}
first = items.first();
if (first.is('select')) {
first.addClass('input-group-prepend');
}
else {
first.wrap('<span class="input-group-prepend">').addClass('input-group-text');
}
if (!has_input) {
items.last().addClass('form-control-plaintext');
}
$('.content', this).addClass('input-group-prepend input-group-append input-group-text');
$('a.deletebutton', this).addClass('input-group-text icon delete').wrap('<span class="input-group-append">');
$(this).addClass('input-group');
});
});
// Advanced options form
$('fieldset.advanced', context).each(function() {
var table = $(this).children('.propform').first();
table.wrap($('<div>').addClass('collapse'));
$(this).children('legend').first().addClass('closed').on('click', function() {
table.parent().collapse('toggle');
$(this).toggleClass('closed');
});
});
// Other forms, e.g. Insert response
$('.propform > .prop.block:not(.row)', context).each(function() {
$(this).addClass('form-group row').each(function() {
$('label', this).addClass('col-form-label').wrap($('<div class="col-sm-4">'));
$('input,select,textarea', this).wrap($('<div class="col-sm-8">'));
$(supported_controls, this).addClass('form-control');
});
});
$('td.rowbuttons > a', context).addClass('btn');
// Testing Bootstrap Tabs on contact info/edit page
// Tabs do not scale nicely on very small screen, so can be used
// only with small number of tabs with short text labels
$('form.tabbed,div.tabbed', context).each(function(idx, item) {
var tabs = [], nav = $('<ul>').attr({'class': 'nav nav-tabs', role: 'tablist'});
$(this).addClass('tab-content').children('fieldset').each(function(i, fieldset) {
var tab, id = fieldset.id || ('tab' + idx + '-' + i),
tab_class = $(fieldset).data('navlink-class');
$(fieldset).addClass('tab-pane').attr({id: id, role: 'tabpanel'});
tab = $('<li>').addClass('nav-item').append(
$('<a>').addClass('nav-link' + (tab_class ? ' ' + tab_class : ''))
.attr({role: 'tab', 'href': '#' + id})
.text($('legend', fieldset).first().text())
.click(function(e) {
$(this).tab('show');
// Because we return false we have to close popups
popups_close(e);
// Returning false here prevents from strange scrolling issue
// when the form is in an iframe, e.g. contact edit form
return false;
})
);
$('legend', fieldset).first().hide();
tabs.push(tab);
});
// create the navigation bar
nav.append(tabs).insertBefore(item);
// activate the first tab
$('a.nav-link', nav).first().click();
});
$('input[type=file]:not(.custom-file-input)', context).each(function() {
var label_text = rcmail.gettext('choosefile' + (this.multiple ? 's' : '')),
label = $('<label>').attr({'class': 'custom-file-label',
'data-browse': rcmail.gettext('browse')}).text(label_text);
$(this).addClass('custom-file-input').wrap('<div class="custom-file">');
$(this).on('change', function() {
var text = label_text;
if (this.files.length) {
text = this.files[0].name;
if (this.files.length > 1) {
text += ', ...';
}
}
// Note: We don't use label variable to allow cloning of the input
$(this).next().text(text);
})
.parent().append(label);
});
// Make tables pretier
$('table:not(.table,.compact-table,.propform,.listing,.ui-datepicker-calendar)', context)
.filter(function() {
// exclude direct propform children and external content
return !$(this).parent().is('.propform')
&& !$(this).parents('#message-header,.message-htmlpart,.message-partheaders,.boxinformation,.raw-tables').length;
})
.each(function() {
// TODO: Consider implementing automatic setting of table-responsive on window resize
var table = $(this).addClass('table');
table.parent().addClass('table-responsive-sm');
table.find('thead').addClass('thead-default');
});
// The same for some other checkboxes
// We do this here, not in setup() because we want to cover dialogs
$('input.pretty-checkbox, .propform input[type=checkbox], .form-check input[type=checkbox], .popupmenu.form input[type=checkbox], .menu input[type=checkbox]', context)
.each(function() { pretty_checkbox(this); });
// Also when we add action-row of the form, e.g. Managesieve plugin adds them after the page is ready
if ($(context).is('.actionrow')) {
$('input[type=checkbox]', context).each(function() { pretty_checkbox(this); });
}
// Input-group combo is an element with a select field on the left
// and input(s) on right, and where the whole right side can be hidden
// depending on the select position. This code fixes border radius on select
$('.input-group-combo > select', context).first().on('change', function() {
var select = $(this),
fn = function() {
select[select.next().is(':visible') ? 'removeClass' : 'addClass']('alone');
};
setTimeout(fn, 50);
setTimeout(fn, 2000); // for devel mode
}).trigger('change');
// Make message-objects alerts pretty (the same as UI alerts)
$('#message-objects', context).children(':not(.ui.alert)').add('.part-notice').each(function() {
// message objects with notice class are really warnings
var cl = String($(this).removeClass('notice part-notice').attr('class')).split(/\s/)[0] || 'warning';
alert_style(this, cl);
$(this).addClass('box' + cl);
$('a', this).addClass('btn btn-primary btn-sm');
});
// Form validation errors (managesieve plugin)
$('.error', context).addClass('is-invalid');
// Make logon form prettier
if (rcmail.env.task == 'login' && context == document) {
$('#rcmloginsubmit').addClass('btn-lg text-uppercase w-100');
$('#login-form table tr').each(function() {
var input = $('input,select', this),
label = $('label', this),
icon_name = input.data('icon'),
icon = $('<i>').attr('class', 'input-group-text icon ' + input.attr('name').replace('_', ''));
if (icon_name) {
icon.addClass(icon_name);
}
$(this).addClass('form-group row');
label.parent().css('display', 'none');
input.addClass(input.is('select') ? 'custom-select' : 'form-control')
.attr('placeholder', label.text())
.before($('<span class="input-group-prepend">').append(icon))
.parent().addClass('input-group input-group-lg');
});
}
$('select:not([multiple])', context).each(function() { pretty_select(this); });
};
/**
* Initializes popup menus
*/
function dropdowns_init()
{
$('[data-popup]').each(function() { popup_init(this); });
$(document).on('click', popups_close);
rcube_webmail.set_iframe_events({mousedown: popups_close, touchstart: popups_close});
};
/**
* Init content frame
*/
function content_frame_init()
{
if (!layout.list.length) {
return;
}
var last_selected = env.last_selected,
title_reset = function(title) {
if (typeof title !== 'string' || !title.length) {
title = $('h1.voice').text() || $('title').text() || '';
}
layout.content.find('.header > .header-title').text(title);
};
// display or reset the content frame
var common_content_handler = function(e, href, show, title)
{
if (is_mobile() && env.frame_nav) {
content_frame_navigation(href, e);
}
if (show && !layout.content.is(':visible')) {
env.last_selected = layout.content[0];
}
else if (!show && env.last_selected != last_selected && !env.content_lock) {
env.last_selected = last_selected;
}
screen_resize();
title_reset(title && show ? title : null);
env.content_lock = false;
};
var common_list_handler = function(e) {
if (mode != 'large' && !env.content_lock && e.force) {
show_list();
}
env.content_lock = false;
// display current folder name in list header
if (e.title) {
$('.header > .header-title', layout.list).text(e.title);
}
};
var list_handler = function(e) {
var args = {};
if (rcmail.env.task == 'addressbook' || rcmail.env.task == 'mail') {
args.force = true;
}
// display current folder name in list header
if (rcmail.env.task == 'mail' && !rcmail.env.action) {
var name = $.type(e) == 'string' ? e : rcmail.env.mailbox,
folder = rcmail.env.mailboxes[name];
args.title = folder ? folder.name : '';
}
common_list_handler(args);
};
// when loading content-frame in small-screen mode display it
layout.content.find('iframe').on('load', function(e) {
var win, href = '', show = true;
// Reset the scroll position of the iframe-wrapper
$(this).parent('.iframe-wrapper').scrollTop(0);
try {
win = e.target.contentWindow;
href = win.location.href;
show = !href.endsWith(rcmail.env.blankpage);
// Reset title back to the default
$(win).on('unload', title_reset);
}
catch(e) { /* ignore */ }
common_content_handler(e, href, show);
});
rcmail
.addEventListener('afterlist', list_handler)
.addEventListener('afterlistgroup', list_handler)
.addEventListener('afterlistsearch', list_handler)
// plugins
.addEventListener('show-list', function(e) {
e.force = true;
common_list_handler(e);
})
.addEventListener('show-content', function(e) {
if (e.obj && !$(e.obj).is('iframe')) {
$(e.scrollElement || e.obj).scrollTop(0);
if (is_mobile()) {
iframe_loader(e.obj);
}
}
common_content_handler(e.event || new Event, '_action=' + (e.mode || 'edit'), true, e.title);
});
};
/**
* Content frame navigation
*/
function content_frame_navigation(href, event)
{
// Don't display navigation for create/add action frames
if (href.match(/_action=(create|add)/) || href.match(/_nav=hide/)) {
$(env.frame_nav).addClass('hide-nav-buttons');
return;
}
var node, uid, list, _list = $('[data-list]', layout.list).data('list');
if (!_list || !(list = rcmail[_list])) {
// hide navbar if there are no visible buttons, e.g. Help plugin UI
if ($(env.frame_nav).is('.hide-nav-buttons') && !$('.buttons', env.frame_nav).children().length) {
$(env.frame_nav).addClass('hidden');
}
return;
}
$(env.frame_nav).removeClass('hide-nav-buttons hidden');
// expand collapsed row so we do not skip the whole thread
// TODO: Unified interface for list and treelist widgets
if (uid = list.get_single_selection()) {
if (list.rows && list.rows[uid] && !list.rows[uid].expanded) {
list.expand_row(event, uid);
}
else if (list.get_node && (node = list.get_node(uid)) && node.collapsed) {
list.expand(uid);
}
}
var prev, next,
frame = $('#' + rcmail.env.contentframe),
next_button = $('a.button.next', env.frame_nav).off('click').addClass('disabled'),
prev_button = $('a.button.prev', env.frame_nav).off('click').addClass('disabled');
if ((next = list.get_next()) || rcmail.env.current_page < rcmail.env.pagecount) {
next_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (next) {
list.select(next);
}
else {
rcmail.env.list_uid = 'FIRST';
rcmail.command('nextpage');
}
});
}
if (((prev = list.get_prev()) && (prev != '*' || _list != 'subscription_list')) || rcmail.env.current_page > 1) {
prev_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (prev) {
list.select(prev);
}
else {
rcmail.env.list_uid = 'LAST';
rcmail.command('previouspage');
}
});
}
};
/**
* Handler for editor-init event
*/
function tinymce_init(o)
{
var onload = [],
is_editor = $('#' + o.id).parent().is('.html-editor');
// Enable autoresize plugin
o.config.plugins += ' autoresize';
if (is_touch()) {
// Use minimalistic toolbar
o.config.toolbar = 'undo redo | link image styleselect';
}
if (rcmail.task == 'mail' && rcmail.env.action == 'compose') {
var form = $('#compose-content > form'),
keypress = function(e) {
if (e.key == 'Tab' && e.shiftKey) {
$('#compose-content > form').scrollTop(0);
}
};
// Shift+Tab on mail compose editor scrolls the page to the top
onload.push(function(ed) {
ed.on('keypress', keypress);
});
$('#composebody').on('keypress', keypress);
// Keep the editor toolbar on top of the screen on scroll
form.on('scroll', function() {
var container = $('.tox-editor-container', form),
toolbar = container.find('.tox-toolbar-overlord'),
editor_offset = container.offset(),
header_top = form.offset().top;
if (editor_offset && (editor_offset.top - header_top < 0)) {
toolbar.css({position: 'fixed', top: header_top + 'px', width: container.width() + 'px'});
}
else {
toolbar.css({position: 'relative', top: 0, width: 'auto'})
}
});
$(window).resize(function() { form.trigger('scroll'); });
}
if (is_editor) {
o.config.toolbar = 'plaintext | ' + o.config.toolbar;
// Use setup_callback, we can't use editor-load event
o.config.setup_callback = function(ed) {
ed.ui.registry.addButton('plaintext', {
tooltip: rcmail.gettext('plaintoggle'),
icon: 'close',
onAction: function(e) {
if (rcmail.command('toggle-editor', {id: ed.id, html: false}, '', e.originalEvent)) {
$('#' + ed.id).parent().removeClass('ishtml');
}
}
});
};
}
// Add styling for TinyMCE dialogs
onload.push(function(ed) {
ed.on('OpenWindow', function(e) {
var dialog = $('.tox-dialog:last')[0],
callback = function(e) {
var body = $(dialog).find('.tox-dialog__body'),
foot = $(dialog).find('.tox-dialog__footer'),
buttons = foot.find('button');
if (!e) {
// Fix icons in Find and Replace dialog footer
if (buttons.length === 4) {
body.closest('.tox-dialog').addClass('tox-search-dialog');
}
// Switch Save and Cancel buttons order
else if (buttons.length == 2) {
buttons.first().insertAfter(buttons[1]);
}
// TODO: Styling form elements does not work well because of
// https://github.com/tinymce/tinymce/issues/4867
// also https://github.com/tinymce/tinymce/issues/4869
}
body.find('select').each(function() { pretty_select(this); });
body.find('.tox-checkbox > input').each(function() { pretty_checkbox(this); });
body.find('.tox-textarea,.tox-textfield').addClass('form-control');
};
// TODO: Maybe some day we'll not have to use MutationObserver
// https://github.com/tinymce/tinymce/issues/4869
if (window.MutationObserver) {
(new MutationObserver(callback)).observe($('.tox-dialog__body-content', dialog)[0], {childList: true});
}
callback();
});
});
rcmail.addEventListener('editor-load', function(e) {
$.each(onload, function() { this(e.ref.editor); });
});
};
function datepicker_init(datepicker)
{
// Datepicker widget improvements: overlay element, styling updates on calendar element update
// The widget does not provide any event system, so we use MutationObserver
if (window.MutationObserver) {
$(datepicker).not('[data-observed]').each(function() {
var overlay, hidden = true,
win = is_framed ? parent : window,
callback = function(data) {
$.each(data, function(i, v) {
// add/remove overlay on widget show/hide
if (v.type == 'attributes') {
var is_hidden = $(v.target).attr('aria-hidden') == 'true';
if (is_hidden != hidden) {
if (!is_hidden) {
overlay = $('<div>').attr('class', 'ui-widget-overlay datepicker')
.appendTo(win.document.body)
.click(function(e) {
$(this).remove();
if (is_framed) {
$.datepicker._hideDatepicker();
}
});
}
else if (overlay) {
overlay.remove();
}
hidden = is_hidden;
}
}
else if (v.addedNodes.length) {
// apply styles when widget content changed
win.UI.bootstrap_style(v.target);
// Month/Year change handlers do not work from parent, fix it
if (is_framed) {
win.$('select.ui-datepicker-month', v.target).on('change', function() {
$.datepicker._selectMonthYear($.datepicker._lastInput, this, "M");
});
win.$('select.ui-datepicker-year', v.target).on('change', function() {
$.datepicker._selectMonthYear($.datepicker._lastInput, this, "Y");
});
}
}
});
};
$(this).attr('data-observed', '1');
if (is_framed) {
// move the datepicker to parent window
$(this).detach().appendTo(parent.document.body);
// create fake element, so the valid one is not removed by datepicker code
$('<div id="ui-datepicker-div" class="hidden">').appendTo(document.body);
}
(new MutationObserver(callback)).observe(this, {childList: true, subtree: false, attributes: true, attributeFilter: ['aria-hidden']});
});
}
};
function toggle_list_selection(obj, list_id)
{
if ($(obj).is('.active')) {
$('#' + list_id).toggleClass('withselection');
}
};
/**
* Handler for some Roundcube core popups
*/
function rcmail_popup_init(o)
{
// Add some common styling to the autocomplete/googiespell popups
$('ul', o.obj).addClass('menu listing iconized');
$(o.obj).addClass('popupmenu popover');
bootstrap_style(o.obj);
// for googiespell list
$('input', o.obj).addClass('form-control');
// Modify the googiespell menu on mobile
if (is_mobile() && $(o.obj).is('.googie_window')) {
// Set popup Close title
var title = rcmail.gettext('close'),
class_name = 'button icon cancel',
close_link = $('<a>').attr('class', class_name).text(title)
.click(function(e) {
e.stopPropagation();
$('.popover-overlay').remove();
$(o.obj).hide();
});
$('<h3 class="popover-header">').append(close_link).prependTo(o.obj);
// add overlay element for phone layout
if (!$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('ul,button', o.obj).click(function(e) {
if (!$(e.target).is('input')) {
$('.popover-overlay').remove();
}
});
}
};
/**
* Handler for 'enable-command' event
*/
function enable_command_handler(args)
{
if (is_framed) {
$.each(frame_buttons, function(i, button) {
if (args.command == button.command) {
parent.$('#' + button.button_id)[args.status ? 'removeClass' : 'addClass']('disabled');
}
});
}
if (rcmail.task == 'mail') {
switch (args.command) {
case 'reply-list':
if (rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(args.status ? 'replylist' : 'replyall');
$('.toolbar a.reply-all').attr('title', label).find('.inner').text(label);
}
break;
case 'compose-encrypted':
// show the toolbar button for Mailvelope
$('.toolbar a.encrypt').parent().show();
break;
case 'compose-encrypted-signed':
// enable selector for encrypt and sign
$('#encryption-menu-button').show();
break;
}
}
};
/**
* screen mode
*/
function screen_mode()
{
var size, width = $(window).width();
if (width <= 480)
size = 'phone';
else if (width > 1200)
size = 'large';
else if (width > 768)
size = 'normal';
else
size = 'small';
touch = width <= 1024;
mode = size;
};
/**
* Get current screen mode
*/
function get_screen_mode()
{
return mode;
};
/**
* Window resize handler
* Does layout reflows e.g. on screen orientation change
*/
function resize()
{
var mobile;
screen_mode();
screen_resize();
screen_resize_html();
// disable ext-windows and other features
if (mobile = is_mobile()) {
rcmail.set_env(env.small_screen_config);
rcmail.enable_command('extwin', false);
}
else {
rcmail.set_env(env.config);
rcmail.enable_command('extwin', true);
}
// Hide content frame buttons on small devices (with frame toolbar in parent window)
$.each(content_buttons, function() { $(this)[mobile ? 'hide' : 'show'](); });
rcmail.triggerEvent('skin-resize', { mode: mode })
};
function screen_resize()
{
if (is_framed && !layout.sidebar.length && !layout.list.length) {
screen_resize_headers();
return;
}
switch (mode) {
case 'phone': screen_resize_phone(); break;
case 'small': screen_resize_small(); break;
case 'normal': screen_resize_normal(); break;
case 'large': screen_resize_large(); break;
}
- screen_resize_logo(mode);
+ screen_logo(mode);
screen_resize_headers();
// On iOS and Android the content frame height is never correct, fix it.
// Actually I observed the issue on my old iPad with iOS 9.3.
if (bw.webkit && bw.ipad && bw.agent.match(/OS 9/)) {
$('.iframe-wrapper').each(function() {
var h = $(this).height();
if (h) {
$(this).children('iframe').height(h);
}
});
}
};
/**
* Assigns layout-* and touch-mode class to the 'html' element
*
* If we're inside an iframe that is small we have to
* check if the parent window is also small (mobile).
* We use that e.g. to still display desktop-like popovers in dialogs
*/
function screen_resize_html()
{
var meta = layout_metadata(),
html = $(document.documentElement);
if (html[0].className.match(/layout-([a-z]+)/)) {
if (RegExp.$1 != meta.mode) {
html.removeClass('layout-' + RegExp.$1)
.addClass('layout-' + meta.mode);
}
}
else {
html.addClass('layout-' + meta.mode);
}
if (meta.touch && !html.is('.touch')) {
html.addClass('touch');
}
else if (!meta.touch && html.is('.touch')) {
html.removeClass('touch');
}
};
- function screen_resize_logo(mode)
+ function screen_logo(mode)
{
- if (mode == 'phone' && $('#logo').data('src-small')) {
- $('#logo').attr('src', $('#logo').data('src-small'));
- }
- else {
- $('#logo').attr('src', $('#logo').data('src-default'));
+ var logos = rcmail.env.additional_logos;
+ if (logos) {
+ // Store default logo path if not already set
+ if (!$('#logo').data('src-default')) {
+ $('#logo').data('src-default', $('#logo').attr('src'));
+ }
+
+ if (mode == 'phone' && logos['small']) {
+ $('#logo').attr('src', logos['small']);
+ }
+ else if (mode == 'phone' && color_mode == 'dark' && logos['small-dark']) {
+ $('#logo').attr('src', logos['small-dark']);
+ }
+ else if (color_mode == 'dark' && logos['dark']) {
+ $('#logo').attr('src', logos['dark']);
+ }
+ else {
+ $('#logo').attr('src', $('#logo').data('src-default'));
+ }
}
}
/**
* Sets left and right margin to the header title element to make it
* properly centered depending on the number of buttons on both sides
*/
function screen_resize_headers()
{
$('#layout > div > .header').each(function() {
var title, right = 0, left = 0, padding = 0,
sizes = {left: 0, right: 0};
$(this).children(':visible:not(.position-absolute)').each(function() {
if (!title && $(this).is('.header-title')) {
title = $(this);
return;
}
sizes[title ? 'right' : 'left'] += this.offsetWidth;
});
if (padding + sizes.right >= sizes.left) {
right = 0;
left = sizes.right + padding - sizes.left;
}
else {
left = 0;
right = sizes.left - (padding + sizes.right);
}
$(title).css({
'margin-right': right + 'px',
'margin-left': left + 'px',
'padding-right': padding + 'px'
});
});
};
function screen_resize_phone()
{
screen_resize_small_all();
app_menu(false);
};
function screen_resize_small()
{
screen_resize_small_all();
app_menu(true);
};
function screen_resize_normal()
{
var show;
if (layout.list.length) {
show = layout.list.is(env.last_selected) || (!layout.sidebar.is(env.last_selected) && !layout.sidebar.is('.layout-sticky'));
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.sidebar.length) {
show = !layout.list.length || layout.sidebar.is(env.last_selected) || layout.sidebar.is('.layout-sticky');
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
layout.content.removeClass('hidden');
app_menu(true);
screen_resize_small_none();
if (layout.list.length) {
$('.header > ul.menu', layout.list).addClass('popupmenu');
}
};
function screen_resize_large()
{
$.each(layout, function(name, item) { item.removeClass('hidden'); });
screen_resize_small_none();
if (layout.list) {
$('.header > ul.menu.popupmenu', layout.list).removeClass('popupmenu');
}
};
function screen_resize_small_all()
{
var show, got_content = false;
if (layout.content.length) {
show = got_content = layout.content.is(env.last_selected);
layout.content[show ? 'removeClass' : 'addClass']('hidden');
$('.header > ul.menu', layout.content).addClass('popupmenu');
}
if (layout.list.length) {
show = !got_content && layout.list.is(env.last_selected);
layout.list[show ? 'removeClass' : 'addClass']('hidden');
$('.header > ul.menu', layout.list).addClass('popupmenu');
}
if (layout.sidebar.length) {
show = !got_content && (layout.sidebar.is(env.last_selected) || !layout.list.length);
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
if (got_content) {
buttons.back_list.show();
}
};
function screen_resize_small_none()
{
buttons.back_list.filter(function() { return $(this).parents('#layout-sidebar').length == 0; }).hide();
$('ul.menu.popupmenu').removeClass('popupmenu');
};
function show_content(unsticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.addClass('hidden');
layout.content.removeClass('hidden');
if (unsticky) {
layout.sidebar.removeClass('layout-sticky');
}
screen_resize_headers();
env.last_selected = layout.content[0];
};
function show_sidebar(sticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.removeClass('hidden');
if (sticky) {
layout.sidebar.addClass('layout-sticky');
}
if (mode == 'small' || mode == 'phone') {
layout.content.addClass('hidden');
}
screen_resize_headers();
env.last_selected = layout.sidebar[0];
};
function show_list(scroll)
{
if (!layout.list.length && !layout.sidebar.length) {
history.back();
}
else {
// show list and hide sidebar and content
layout.sidebar.addClass('hidden').removeClass('layout-sticky');
layout.list.removeClass('hidden');
if (mode == 'small' || mode == 'phone') {
hide_content();
}
if (scroll) {
layout.list.children('.scroller').scrollTop(0);
}
env.last_selected = layout.list[0];
}
screen_resize_headers();
};
function hide_content()
{
// show sidebar or list, hide content frame
env.last_selected = layout.list[0] || layout.sidebar[0];
screen_resize();
// reset content frame, so we can load it again
rcmail.show_contentframe(false);
// now we have to unselect selected row on the list
$('[data-list]', layout.list).each(function() {
var list = $(this).data('list');
if (rcmail[list]) {
if (rcmail[list].clear_selection) {
rcmail[list].clear_selection(); // list widget
}
else if (rcmail[list].select) {
rcmail[list].select(); // treelist widget
}
}
});
};
// show menu widget
function app_menu(show)
{
if (show) {
if (mode == 'phone') {
$('<div id="menu-overlay" class="popover-overlay">')
.on('click', function() { app_menu(false); })
.appendTo('body');
if (!env.menu_initialized) {
env.menu_initialized = true;
$('a', layout.menu).on('click', function(e) { if (mode == 'phone') app_menu(); });
}
layout.menu.addClass('popover');
}
layout.menu.removeClass('hidden');
}
else {
$('#menu-overlay').remove();
layout.menu.addClass('hidden').removeClass('popover');
}
};
/**
* Triggered when a UI message is displayed
*/
function message_displayed(p)
{
if (p.type == 'loading' && $('.iframe-loader:visible').length) {
// hide original message object, we don't need two "loaders"
rcmail.hide_message(p.object);
return;
}
alert_style(p.object, p.type, true);
$(p.object).attr('role', 'alert');
};
/**
* Applies some styling and icon to an alert object
*/
function alert_style(object, type, wrap)
{
var tmp, classes = 'ui alert',
addicon = !$(object).is('.noicon'),
map = {
information: 'alert-info',
notice: 'alert-info',
confirmation: 'alert-success',
warning: 'alert-warning',
error: 'alert-danger',
loading: 'alert-info loading',
uploading: 'alert-info loading',
vcardattachment: 'alert-info' // vcard_attachments plugin
};
// we need the content to be non-text node for best alignment
if (wrap && addicon && !$(object).is('.aligned-buttons')) {
$(object).html($('<span>').html($(object).html()));
}
// Type can be e.g. 'notice chat'
type = type.split(' ')[0];
if (tmp = map[type]) {
classes += ' ' + tmp;
if (addicon) {
$('<i>').attr('class', 'icon').prependTo(object);
}
}
$(object).addClass(classes);
};
/**
* Set UI dialogs size/style depending on screen size
*/
function dialog_open(dialog)
{
var me = $(dialog.uiDialog),
width = me.width(),
height = me.height(),
maxWidth = $(window).width(),
maxHeight = $(window).height();
if (maxWidth <= 480) {
me.css({width: '100%', height: '100%'});
}
else {
if (height > maxHeight) {
me.css('height', '100%');
}
if (width > maxWidth) {
me.css('width', '100%');
}
}
// Close all popovers
$(document).click();
// Display loader when the dialog has an iframe
iframe_loader($('div.popup > iframe', me));
// TODO: style buttons/forms
bootstrap_style(dialog.uiDialog);
};
/**
* Initializes searchbar widget
*/
function searchbar_init(bar)
{
var unread_button = $(),
options_button = $('a.button.options', bar),
input = $('input:not([type=hidden])', bar),
placeholder = input.attr('placeholder'),
form = $('form', bar),
is_search_pending = function() {
if (input.val()) {
return true;
}
if (rcmail.task == 'mail' && $('#s_interval').val()) {
return true;
}
if (rcmail.gui_objects.search_filter && $(rcmail.gui_objects.search_filter).val() != 'ALL') {
return true;
}
if (rcmail.gui_objects.foldersfilter && $(rcmail.gui_objects.foldersfilter).val() != '---') {
return true;
}
},
close_func = function() {
if ($(bar).is('.open')) {
options_button.click();
}
},
update_func = function() {
$(bar)[is_search_pending() ? 'addClass' : 'removeClass']('active');
unread_button[rcmail.gui_objects.search_filter && $(rcmail.gui_objects.search_filter).val() == 'UNSEEN' ? 'addClass' : 'removeClass']('selected');
};
// Add Unread filter button
if (input.is('#mailsearchform')) {
unread_button = $('<a>')
.attr({'class': 'button unread', href: '#', role: 'button', title: rcmail.gettext('showunread')})
.on('click', function(e) {
$(rcmail.gui_objects.search_filter).val($(e.target).is('.selected') ? 'ALL' : 'UNSEEN');
rcmail.command('search');
})
.insertBefore(options_button);
}
options_button.on('click', function(e) {
var id = $(this).data('target'),
options = $('#' + id),
open = $(bar).is('.open');
if (options.length) {
if (!open) {
if (ref[id]) {
ref[id](options.get(0), this, e);
}
else if (typeof window[id] == 'function') {
window[id](options.get(0), this, e);
}
}
options.next()[open ? 'show' : 'hide']();
options.toggleClass('hidden');
$('.floating-action-buttons').toggleClass('hidden');
$(bar).toggleClass('open');
$('button.search', options).off('click.search').on('click.search', function() {
options_button.click();
update_func();
});
}
});
input.on('input change', update_func)
.on('focus blur', function(e) { input.attr('placeholder', e.type == 'blur' ? placeholder : ''); });
// Search reset action
$('a.reset', bar).on('click', function(e) {
// for treelist widget's search setting val and keyup.treelist is needed
// in normal search form reset-search command will do the trick
input.val('').change().trigger('keyup.treelist', {keyCode: 27});
if ($(bar).is('.open')) {
options_button.click();
}
// Reset filter
if (rcmail.gui_objects.search_filter) {
$(rcmail.gui_objects.search_filter).val('ALL');
}
if (rcmail.gui_objects.foldersfilter) {
$(rcmail.gui_objects.foldersfilter).val('---').change();
rcmail.folder_filter('---');
}
update_func();
});
rcmail.addEventListener('init', update_func)
.addEventListener('responsebeforesearch', update_func)
.addEventListener('beforelist', close_func)
.addEventListener('afterlist', update_func)
.addEventListener('beforesearch', close_func);
};
/**
* Converts toolbar menu into popup-menu for small screens
*/
function toolbar_init()
{
if (env.got_smart_toolbar) {
return;
}
env.got_smart_toolbar = true;
var list_mark, items = [],
list_items = [],
meta = layout_metadata(),
button_func = function(button, items, cloned) {
var item = $('<li role="menuitem">'),
button = cloned ? create_cloned_button($(button), true, 'hidden-big hidden-large') : $(button).detach();
// Remove empty text nodes that break alignment of text of the menu item
button.contents().filter(function() { if (this.nodeType == 3 && this.nodeValue.trim().length == 0) $(this).remove(); });
if (button.is('.spacer')) {
item.addClass('spacer');
}
else {
item.append(button);
}
items.push(item);
};
// convert content toolbar to a popup list
layout.content.find('.header > .menu').each(function() {
var toolbar = $(this);
toolbar.children().each(function() { button_func(this, items); });
toolbar.remove();
});
// convert list toolbar to a popup list
layout.list.find('.header > .menu').each(function() {
var toolbar = $(this);
list_mark = toolbar.next();
toolbar.children().each(function() {
if (meta.mode != 'large') {
// TODO: Would be better to set this automatically on submenu display
// i.e. in show/shown event (see popup_init()), if possible
$(this).data('popup-pos', 'right');
}
// add items to the content menu too
button_func(this, items, true);
button_func(this, list_items);
});
toolbar.remove();
});
// special elements to clone and add to the toolbar (mobile only)
$('ul[data-menu="toolbar-small"] > li > a').each(function() {
var button = $(this).clone();
button.attr('id', this.id + '_clone');
// TODO: rcmail.register_button()
items.push($('<li role="menuitem">').addClass('hidden-big').append(button));
});
// append the new list toolbar and menu button
if (list_items.length) {
var container = layout.list.children('.header'),
menu_attrs = {'class': 'menu toolbar popupmenu listing iconized', id: 'toolbar-list-menu'},
menu_button = $('<a class="button icon toolbar-list-button" href="#list-menu">')
.attr({'data-popup': 'toolbar-list-menu'}),
// TODO: copy original toolbar attributes (class, role, aria-*)
toolbar = $('<ul>').attr(menu_attrs).data('popup-parent', container).append(list_items);
if (list_mark.length) {
toolbar.insertBefore(list_mark);
}
else {
container.append(toolbar);
}
container.append(menu_button);
}
// append the new toolbar and menu button
if (items.length) {
var container = layout.content.children('.header'),
menu_attrs = {'class': 'menu toolbar popupmenu listing iconized', id: 'toolbar-menu'},
menu_button = $('<a class="button icon toolbar-menu-button" href="#menu">')
.attr({'data-popup': 'toolbar-menu'});
container
// TODO: copy original toolbar attributes (class, role, aria-*)
.append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(items))
.append(menu_button);
// bind toolbar menu with the menu button in the list header
layout.list.find('a.toolbar-menu-button').click(function(e) {
e.stopPropagation();
menu_button.click();
});
}
};
/**
* Initialize a popup for specified button element
*/
function popup_init(item, win)
{
// On mobile we display the menu from the frame in the parent window
if (is_framed && is_mobile()) {
return parent.UI.popup_init(item, win || window);
}
if (!win) win = window;
var level,
popup_id = $(item).data('popup'),
popup = $(win.$('#' + popup_id).get(0)), // a "hack" to support elements in frames
popup_orig = popup,
title = $(item).attr('title'),
content_element = function() {
// On mobile we display a menu from the frame in the parent window
// To make menu actions working we have to clone the menu
// and pass click events to it...
if (win != window) {
popup = popup_orig.clone(true, true);
popup.attr('id', popup_id + '-clone')
.appendTo(document.body)
.find('li > a').attr('onclick', '').off('click').on('click', function(e) {
if (!$(this).is('.disabled')) {
$(item).popover('hide');
win.$('#' + $(this).attr('id')).click();
}
return false;
});
}
return popup.get(0);
};
$(item).attr({
'aria-haspopup': 'true',
'aria-expanded': 'false',
'aria-owns': popup_id,
})
.popover({
content: content_element,
trigger: $(item).data('popup-trigger') || 'click',
placement: $(item).data('popup-pos') || 'bottom',
animation: true,
boundary: 'window', // fix for https://github.com/twbs/bootstrap/issues/25428
html: true
})
.on('show.bs.popover', function(event) {
var init_func = popup.data('popup-init');
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = true;
}
if (init_func && ref[init_func]) {
ref[init_func](popup.get(0), item, event);
}
else if (init_func && win[init_func]) {
win[init_func](popup.get(0), item, event);
}
level = $('div.popover:visible').length + 1;
popup.removeClass('hidden').attr('aria-hidden', false)
// Stop propagation on menu items that have popups
// to make a click on them not hide their parent menu(s)
.find('[aria-haspopup="true"]')
.data('level', level + 1)
.off('click.popup')
.on('click.popup', function(e) { e.stopPropagation(); });
if (!is_mobile()) {
// Set popup height so it is less than the window height
popup.css('max-height', Math.min(36 * 15 - 1, $(window).height() - 30));
}
})
.on('shown.bs.popover', function(event) {
var mobile = is_mobile(),
popover = $('#' + $(item).attr('aria-describedby'));
level = $(item).data('level') || 1;
// Set popup Back/Close title
if (mobile) {
var label = level > 1 ? 'back' : 'close',
title = rcmail.gettext(label),
class_name = 'button icon ' + (label == 'back' ? 'back' : 'cancel');
$('.popover-header', popover).empty()
.append($('<a>').attr('class', class_name).text(title)
.on('click', function(e) {
$(item).popover('hide');
if (level > 1) {
e.stopPropagation();
}
})
.on('mousedown', function(e) {
// stop propagation to i.e. do not close jQuery-UI dialogs below
e.stopPropagation();
})
);
}
// Hide other menus on the same level
$.each(menus, function(id, prop) {
if ($(prop.target).data('level') == level && id != popup_id) {
menu_hide(id);
}
});
// On keyboard event focus the first (active) entry and enable keyboard navigation
if ($(item).data('event') == 'key') {
popover.off('keydown.popup').on('keydown.popup', 'a.active', function(e) {
var entry, node, mode = 'next';
switch (e.which) {
case 27: // ESC
case 9: // TAB
$(item).popover('toggle').focus();
return false;
case 38: // ARROW-UP
case 63232:
mode = 'previous';
case 40: // ARROW-DOWN
case 63233:
entry = e.target.parentNode;
while (entry = entry[mode + 'Sibling']) {
if (node = $(entry).children('.active')[0]) {
node.focus();
break;
}
}
return false; // prevents from scrolling the whole page
}
});
popover.find('a.active').first().focus();
}
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = false;
}
// add overlay element for phone layout
if (mobile && !$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('.popover-body', popover).addClass('webkit-scroller');
})
.on('hide.bs.popover', function() {
if (level == 1) {
$('.popover-overlay').remove();
}
if (popup_id && menus[popup_id] && popup.is(':visible')) {
menus[popup_id].transitioning = true;
}
})
.on('hidden.bs.popover', function() {
if (/-clone$/.test(popup.attr('id'))) {
popup.remove();
}
else {
popup.attr('aria-hidden', true)
// Some menus aren't being hidden, force that
.addClass('hidden')
// Bootstrap will detach the popup element from
// the DOM (https://github.com/twbs/bootstrap/issues/20219)
// making our menus to not update buttons state.
// Work around this by attaching it back to the DOM tree.
.appendTo(popup.data('popup-parent') || document.body);
}
// close orphaned popovers, for some reason there are sometimes such dummy elements left
$('.popover-body:empty').each(function() { $(this).parent().remove(); });
if (popup_id && menus[popup_id]) {
delete menus[popup_id];
}
})
// Because Bootstrap does not provide originalEvent in show/shown events
// we have to handle that by our own using click and keydown handlers
.on('click', function() {
$(this).data('event', 'mouse');
})
.on('keydown', function(e) {
if (e.originalEvent) {
switch (e.originalEvent.which) {
case 13:
case 32:
// Open the popup on ENTER or SPACE
e.preventDefault();
$(this).data('event', 'key').popover('toggle');
break;
case 27:
// Close the popup on ESC key
$(this).popover('hide');
break;
}
}
});
// re-add title attribute removed by bootstrap popover
if (title) {
$(item).attr('title', title);
}
popup.attr('aria-hidden', 'true').data('button', item);
// stop propagation to e.g. do not hide the popup when
// clicking inside on form elements
if (popup.data('editable')) {
popup.on('click mousedown', function(e) { e.stopPropagation(); });
}
};
/**
* Closes all popups (for use as event handler)
*/
function popups_close(e)
{
// Ignore some of propagated click events (see pretty_select())
if (popups_close_lock && popups_close_lock > (new Date().getTime() - 250)) {
return;
}
$('.popover.show').each(function() {
var popup = $('.popover-body', this),
button = popup.children().first().data('button');
if (button && e.target != button && !$(button).find(e.target).length && typeof button !== 'string') {
$(button).popover('hide');
}
if (!button) {
$(this).remove();
}
});
};
/**
* Handler for menu-open and menu-close events
*/
function menu_toggle(p)
{
if (!p || !p.name || (p.props && p.props.skinable === false)) {
return;
}
if (is_framed && is_mobile()) {
if (!p.win) {
p.win = window;
}
return parent.UI.menu_toggle(p);
}
if (p.name == 'messagelistmenu') {
menu_messagelist(p);
}
else if (p.event == 'menu-open') {
var fn, pos,
content = $('ul', p.obj).first(),
target = p.props && p.props.link ? p.props.link : p.originalEvent.target;
// Sanity check, make sure we have some content to show
if (!content.length) {
return;
}
if ($(target).is('span')) {
target = $(target).parents('a,li')[0];
}
if (p.name.match(/^drag/)) {
// create a fake element to position drag menu on the cursor position
pos = rcube_event.get_mouse_pos(p.originalEvent);
target = $('<a>').css({
position: 'absolute',
left: pos.x,
top: pos.y,
height: '1px',
width: '1px',
visibility: 'hidden'
})
.appendTo(document.body).get(0);
}
pos = $(target).data('popup-pos') || 'right';
if (p.name == 'folder-selector') {
content.addClass('listing folderlist');
}
else if (p.name == 'addressbook-selector' || p.name == 'contactgroup-selector') {
content.addClass('listing contactlist');
}
else if (content.hasClass('menu')) {
content.addClass('listing');
}
if (p.name == 'pagejump-selector') {
content.addClass('simplelist');
p.obj.addClass('simplelist');
pos = 'top';
}
// There can be only one menu of the same type
if (menus[p.name]) {
menu_hide(p.name, p.originalEvent);
}
// Popover menus use animation. Sometimes the same menu is
// immediately hidden and shown (e.g. folder-selector for copy and move action)
// we have to wait until the previous menu hides before we can open it again
fn = function() {
if (menus[p.name] && menus[p.name].transitioning) {
return setTimeout(fn, 50);
}
if (!$(target).data('popup')) {
$(target).data({
event: rcube_event.is_keyboard(p.originalEvent) ? 'key' : 'mouse',
popup: p.name,
'popup-pos': pos,
'popup-trigger': 'manual'
});
popup_init(target, p.win);
}
menus[p.name] = {target: target};
$(target).popover('show');
}
fn();
}
else {
menu_hide(p.name, p.originalEvent);
}
// Stop propagation so multi-level menus work properly
p.originalEvent.stopPropagation();
};
/**
* Close menu by name
*/
function menu_hide(name, event)
{
var target = menu_target(name);
if (name.match(/^drag/)) {
$(target).popover('dispose').remove();
}
else {
$(target).popover('hide');
// In phone mode close all menus when forwardmenu is requested to be closed
// FIXME: This is a hack, we need some generic solution.
if (name == 'forwardmenu') {
popups_close(event);
}
}
};
/**
* Destroys menu by name
*
* This is required when you replace the menu content element
*/
function menu_destroy(name)
{
$('[aria-owns=' + name + ']').popover('dispose').data('popup', null);
};
/**
* Get menu target by name
*/
function menu_target(name)
{
var target;
if (menus[name]) {
target = menus[name].target;
}
else {
target = $('#' + name).data('button');
if (!target) {
// catch cases as 'forwardmenu' where menu suffix has no hyphen
// or try with -menu suffix if it's not in the menu name already
if (name.match(/(?!-)menu$/)) {
name = name.substr(0, name.length - 4);
}
target = $('#' + name + '-menu').data('button');
}
}
return target;
};
/**
* Messages list options dialog
*/
function menu_messagelist(p)
{
var content = $('#listoptions-menu'),
width = content.width() + 25,
dialog = content.clone(true);
// set form values
$('select[name="sort_col"]', dialog).val(rcmail.env.sort_col || '');
$('select[name="sort_ord"]', dialog).val(rcmail.env.sort_order || 'ASC');
$('select[name="mode"]', dialog).val(rcmail.env.threading ? 'threads' : 'list');
// Fix id/for attributes
$('select', dialog).each(function() { this.id = this.id + '-clone'; });
$('label', dialog).each(function() { $(this).attr('for', $(this).attr('for') + '-clone'); });
var save_func = function(e) {
if (rcube_event.is_keyboard(e.originalEvent)) {
$('#listmenulink').focus();
}
var col = $('select[name="sort_col"]', dialog).val(),
ord = $('select[name="sort_ord"]', dialog).val(),
mode = $('select[name="mode"]', dialog).val();
rcmail.set_list_options([], col, ord, mode == 'threads' ? 1 : 0);
return true;
};
dialog = rcmail.simple_dialog(dialog, rcmail.gettext('listoptionstitle'), save_func, {
closeOnEscape: true,
minWidth: 400
});
};
/**
* About dialog
*/
function about_dialog(elem)
{
var support_url, support_func, support_button = false,
dialog = $('<iframe>').attr({id: 'aboutframe', src: rcmail.url('settings/about', {_framed: 1})}),
support_link = $('#supportlink');
if (support_link.length && (support_url = support_link.attr('href'))) {
support_button = support_link.text();
support_func = function(e) { support_url.indexOf('mailto:') < 0 ? window.open(support_url) : location.href = support_url; };
}
rcmail.simple_dialog(dialog, $(elem).text(), support_func, {
button: support_button,
button_class: 'help',
cancel_button: 'close',
height: 400
});
};
/**
* Show/hide more mail headers (envelope)
*/
function headers_show(toggle)
{
var key = 'mail.show.envelope',
pref = get_pref(key),
show = toggle ? !pref : pref,
mode = show ? 'summary' : 'details',
headers = $('div.header-content');
$('div.header-links').find('a.headers-details,a.headers-summary')
.removeClass().addClass('headers-' + mode).text(rcmail.gettext(mode));
headers[show ? 'addClass' : 'removeClass']('details-view');
if (toggle) {
// save new pref
save_pref(key, show);
}
};
/**
* Mail headers dialog
*/
function headers_dialog()
{
var props = {_uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _framed: 1},
dialog = $('<iframe>').attr({id: 'headersframe', src: rcmail.url('headers', props)});
rcmail.simple_dialog(dialog, rcmail.gettext('arialabelmessageheaders'), null, {
cancel_button: 'close',
height: 400
});
};
/**
* Mail import dialog
*/
function import_dialog()
{
if (!rcmail.commands['import-messages']) {
return;
}
var content = $('#uploadform'),
dialog = content.clone(true);
var save_func = function(e) {
return rcmail.command('import-messages', $(dialog.find('form')[0]));
};
rcmail.simple_dialog(dialog, rcmail.gettext('importmessages'), save_func, {
button: 'import',
closeOnEscape: true,
minWidth: 400
});
};
/**
* Search options menu popup
*/
function searchmenu(obj)
{
var n, all,
list = $('input[name="s_mods[]"]', obj),
scope_select = $('#s_scope', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (!$(obj).data('initialized')) {
$(obj).data('initialized', true);
if (list.length) {
list.on('change', function() { set_searchmod(obj, this); });
rcmail.addEventListener('beforesearch', function() { set_searchmod(obj); });
}
}
if (rcmail.env.search_mods) {
if (rcmail.env.task == 'mail') {
if (scope == 'all') {
mbox = '*';
}
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
scope_select.val(scope);
}
else {
all = '*';
}
if (mods[all]) {
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
}
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods) {
list.filter('[value="' + n + '"]').prop('checked', true);
}
}
}
};
function set_searchmod(menu, elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('#s_scope', menu).val(),
interval = $('#s_interval', menu).val();
if (scope == 'all') {
mbox = '*';
}
if (!mods) {
mods = {};
}
if (task == 'mail') {
if (!mods[mbox]) {
mods[mbox] = rcube_clone_object(mods['*']);
}
m = mods[mbox];
all = 'text';
rcmail.env.search_scope = scope;
rcmail.env.search_interval = interval;
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem) {
return;
}
if (!elem.checked) {
delete(m[elem.value]);
}
else {
m[elem.value] = 1;
}
// mark all fields
if (elem.value == all) {
$('input[name="s_mods[]"]', menu).map(function() {
if (this == elem) {
return;
}
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
};
/**
* Spellcheck languages list
*/
function spellmenu(obj)
{
var i, link, li, list = [],
lang = rcmail.spellcheck_lang(),
ul = $('ul', obj);
if (!ul.length) {
ul = $('<ul class="selectable listing iconized" role="menu">');
for (i in rcmail.env.spell_langs) {
li = $('<li role="menuitem">');
link = $('<a href="#'+ i +'" tabindex="0"></a>')
.text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
rcmail.spellcheck_lang_set($(this).data('lang'));
rcmail.hide_menu('spell-menu', e);
return false;
}
});
link.appendTo(li);
list.push(li);
}
ul.append(list).appendTo(obj);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang) {
el.addClass('selected').attr('aria-selected', 'true');
}
else if (el.hasClass('selected')) {
el.removeClass('selected').removeAttr('aria-selected');
}
});
};
/**
* Add/remove item to/from compose options status bar
*/
function compose_status(id, status)
{
var bar = $('#composestatusbar'), ico = bar.find('a.button.icon.' + id);
if (!status) {
ico.remove();
}
else if (!ico.length) {
$('<a>').attr('class', 'button icon ' + id)
.on('click', function() { show_sidebar(); })
.appendTo(bar);
}
};
/**
* Attachment menu
*/
function attachmentmenu(obj, button, event)
{
var id = $(button).parent().attr('id').replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action, obj).off('click').attr('onclick', '').click(function(e) {
return rcmail.command(action + '-attachment', id, this, e.originalEvent);
});
});
// call menu-open so core can set state of menu commands
return rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, obj, event);
};
/**
* Appends drop-icon to attachments list item (to invoke attachment menu)
*/
function attachmentmenu_append(item)
{
item = $(item);
if (!item.is('.no-menu') && !item.children('.dropdown').length) {
var label = rcmail.gettext('options'),
fname = item.find('a.filename');
var button = $('<a>').attr({
href: '#',
tabindex: fname.attr('tabindex') || 0,
title: label,
'class': 'button icon dropdown skip-content'
})
.on('click', function(e) {
return attachmentmenu($('#attachmentmenu'), button, e);
})
.append($('<span>').attr('class', 'inner').text(label));
if (fname.length) {
button.insertAfter(fname);
}
else {
button.appendTo(item);
}
}
};
/**
* Mailto menu
*/
function mailtomenu(obj, button, event, onclick)
{
var mailto = $(button).attr('href').replace(/^mailto:/, '');
if (mailto.indexOf('@') < 0) {
return true; // let the browser handle this
}
// disable all menu actions
obj.find('a').off('click').removeClass('active');
if (rcmail.env.has_writeable_addressbook) {
$('.addressbook', obj).addClass('active')
.on('click', function(e) {
var i, contact = mailto,
txt = $(button).filter('.rcmContactAddress').text();
contact = contact.split('?')[0].split(',')[0].replace(/(^<|>$)/g, '');
if (txt) {
txt = txt.replace('<' + contact + '>', '');
contact = '"' + txt.trim() + '" <' + contact + '>';
}
return rcmail.command('add-contact', contact, this, e.originalEvent);
});
}
$('.compose', obj).addClass('active').on('click', function(e) {
// Execute the original onclick handler to support mailto URL arguments (#6751)
if (onclick) {
button.onclick = onclick;
// use the second argument to tell our handler to not display the menu again
$(button).trigger('click', [true]);
button.onclick = null;
}
else {
rcmail.command('compose', mailto, this, e.originalEvent);
}
return false; // for Chrome
});
return rcmail.command('menu-open', {menu: 'mailto-menu', link: button}, button, event.originalEvent);
};
/**
* Appends popup menu to mailto links
*/
function mailtomenu_append(item)
{
// Remember the original onclick handler and display the menu instead
var onclick = item.onclick;
item.onclick = null;
$(item).on('click', function(e, menu) {
return menu || mailtomenu($('#mailto-menu'), item, e, onclick);
});
};
/**
* Headers menu in mail compose
*/
function headersmenu(obj, button, event)
{
$('li > a', obj).each(function() {
var link = $(this), target = '#compose_' + link.data('target');
link[$(target).is(':visible') ? 'removeClass' : 'addClass']('active')
.off().on('click', function() {
$(target).removeClass('hidden').find('.recipient-input input').focus();
link.removeClass('active');
rcmail.set_menu_buttons();
});
});
};
/**
* Reset/hide compose message recipient input
*/
function header_reset(id)
{
$('#' + id).val('').change()
// jump to the next input
.closest('.form-group').nextAll(':not(.hidden)').first().find('input').focus();
$('a[data-target=' + id.replace(/^_/, '') + ']').addClass('active');
rcmail.set_menu_buttons();
};
/**
* Recipient (contact) selector
*/
function recipient_selector(field, opts)
{
if (!opts) opts = {};
var title = rcmail.gettext(opts.title || 'insertcontact'),
dialog = $('#recipient-dialog'),
parent = dialog.parent(),
close_func = function() {
if (dialog.is(':visible')) {
rcmail.env.recipient_dialog.dialog('close');
}
},
insert_func = function() {
if (opts.action) {
opts.action();
close_func();
return;
}
rcmail.command('add-recipient');
};
if (!rcmail.env.recipient_selector_initialized) {
rcmail.addEventListener('add-recipient', close_func);
rcmail.env.recipient_selector_initialized = true;
}
if (field) {
rcmail.env.focused_field = '#_' + field;
}
rcmail.contact_list.clear_selection();
rcmail.contact_list.multiselect = 'multiselect' in opts ? opts.multiselect : true;
rcmail.env.recipient_dialog = rcmail.simple_dialog(dialog, title, insert_func, {
button: rcmail.gettext(opts.button || 'insert'),
button_class: opts.button_class || 'insert recipient',
height: 600,
classes: {
'ui-dialog-content': 'p-0' // remove padding on dialog content
},
open: function() {
// Don't want focus in the search field, we focus first contacts source record instead
$('#directorylist a').first().focus();
},
close: function() {
dialog.appendTo(parent);
$(this).remove();
$(opts.focus || rcmail.env.focused_field).focus();
}
});
};
/**
* Create/Update quota widget (setquota event handler)
*/
function update_quota(p)
{
var element = $('#quotadisplay'),
bar = element.find('.bar'),
value = p.total ? p.percent : 0;
if (!bar.length) {
bar = $('<span class="bar"><span class="value"></span></span>').appendTo(element);
}
if (value > 0 && value < 10) {
value = 10; // smaller values look not so nice
}
bar.find('.value').css('width', value + '%')[value >= 90 ? 'addClass' : 'removeClass']('warning');
// set title and reset tooltip's data (needed in case of empty title)
element.attr({'data-original-title': '', title: element.find('.count').attr('title')});
if (p.table) {
element.css('cursor', 'pointer').data('popup-pos', 'top')
.off('click').on('click', function(e) {
rcmail.simple_dialog(p.table, 'quota', null, {cancel_button: 'close'});
});
}
else {
element.tooltip('dispose').tooltip({trigger: is_mobile() ? 'click' : 'hover'});
}
};
/**
* Replaces recipient input with content-editable element that uses "recipient boxes"
*/
function recipient_input(obj)
{
var list, input, selection = '',
apply_func = function() {
// update the original input
$(obj).val(list.text() + input.val());
},
insert_recipient = function(name, email, replace) {
var recipient = $('<li class="recipient">'),
name_element = $('<span class="name">').html(recipient_input_name(name || email))
.on('dblclick', function(e) { recipient_input_edit_dialog(e, insert_recipient); }),
email_element = $('<span class="email">'),
// TODO: should the 'close' link have tabindex?
link = $('<a>').attr({'class': 'button icon remove'})
.click(function() {
recipient.remove();
apply_func();
input.focus();
return false;
});
if (name) {
email = ' <' + email + '>';
}
email_element.text((name ? email : '') + ',');
recipient.attr('title', name ? (name + email) : null)
.append([name_element, email_element, link])
if (replace)
replace.replaceWith(recipient);
else
recipient.insertBefore(input.parent());
apply_func();
},
update_func = function(text) {
var result;
text = (text || input.val()).replace(/[,;\s]+$/, '');
result = recipient_input_parser(text);
$.each(result.recipients, function() {
insert_recipient(this.name, this.email);
});
input.val(result.text);
apply_func();
return result.recipients.length > 0;
},
parse_func = function(e, ac) {
var last, paste, value = this.value;
// On paste the text is not yet in the input we have to use clipboard.
// Also because on paste new-line characters are replaced by spaces (#6460)
if (e.type == 'paste') {
// pasted text
paste = (e.originalEvent.clipboardData || window.clipboardData).getData('text') || '';
// insert pasted text in place of the selection (or just cursor position)
value = value.substring(0, this.selectionStart) + paste + value.substring(this.selectionEnd);
e.preventDefault();
}
// #7231: When clicking on autocompletion list a change event
// is fired twice. We have to remove last recipient box if it is
// the same recpient (with incomplete email address).
// FIXME: Anyone with a better solution?
else if (ac) {
last = list.find('li.recipient').last();
if (last.length && this.value.indexOf(last.text().replace(/[ ,]+$/, '')) > -1) {
last.remove();
}
}
update_func(value);
},
keydown_func = function(e) {
// On Backspace remove the last recipient
if (e.keyCode == 8 && !input.val().length) {
list.children('li.recipient').last().remove();
apply_func();
return false;
}
// Here we add a recipient box when the separator (,;\s) or Enter was pressed,
else if (e.key == ' ' || e.key == ',' || e.key == ';' || (e.key == 'Enter' && !rcmail.ksearch_visible())) {
if (update_func()) {
return false;
}
}
};
// Create the input element and "editable" area
input = $('<input>').attr({type: 'text', tabindex: $(obj).attr('tabindex')})
.on('paste change', parse_func)
.on('keydown', keydown_func)
.on('blur', function() { list.removeClass('focus'); })
.on('focus mousedown', function() { list.addClass('focus'); });
list = $('<ul>').addClass('form-control recipient-input ac-input rounded-left')
.append($('<li class="input">').append(input))
// "selection" hack to allow text selection in the recipient box or multiple boxes (#7129)
.on('mouseup', function () { selection = window.getSelection().toString(); })
.on('click', function() { if (!selection.length) input.focus(); })
.sortable({
appendTo: document.body,
items: "> .recipient",
connectWith: '.recipient-input',
receive: function(event, ui) {
var recipient = list.text();
list.find('.recipient').remove();
update_func(recipient);
if (ui.sender) {
ui.sender.find('input').change();
}
}
});
// Hide the original input/textarea
// Note: we do not remove the original element, and we do not use
// display: none, because we want to handle onfocus event
// Note: tabindex:-1 to make Shift+TAB working on these widgets
$(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'})
.attr('tabindex', -1)
.after(list)
// some core code sometimes focuses or changes the original node
// in such cases we wan't to parse it's value and apply changes
// to the widget element
.on('focus', function(e) { input.focus(); e.preventDefault(); })
.on('change', function() {
$('li.recipient', list).remove();
input.val(this.value).change();
})
// copy and parse the value already set
.change();
// Init autocompletion
rcmail.init_address_input_events(input);
};
/**
* Parses recipient address input and extracts recipients from it
*/
function recipient_input_parser(text)
{
// support new-line as a separator, for paste action (#6460)
text = text.replace(/[,;\s]*[\r\n]+/g, ',').trim();
var recipients = [],
address_rx_part = '(\\S+|("[^"]+"))@\\S+',
recipient_rx1 = new RegExp('(<' + address_rx_part + '>)'),
recipient_rx2 = new RegExp('(' + address_rx_part + ')'),
global_rx = /(?=\S)[^",;]*(?:"[^\\"]*(?:\\[,;\S][^\\"]*)*"[^",;]*)*/g,
matches = text.match(global_rx);
$.each(matches || [], function() {
if (this.length && (recipient_rx1.test(this) || recipient_rx2.test(this))) {
var email, str = this;
text = text.replace(str, '');
// Support space-separated email addresses
while (str.length && str.indexOf(RegExp.$1) === 0) {
email = RegExp.$1;
recipients.push({
name: '',
email: email.replace(/(^<|>$)/g, '')
});
str = str.replace(email, '').trim();
if (!recipient_rx1.test(str) && !recipient_rx2.test(str)) {
break;
}
}
if (email != RegExp.$1) {
email = RegExp.$1;
recipients.push({
name: str.replace(email, '').trim(),
email: email.replace(/(^<|>$)/g, '')
});
}
}
});
text = text.replace(/[,;]+/, ',').replace(/^[,;\s]+/, '');
return {recipients: recipients, text: text};
};
/**
* Generates HTML for a text adding <span class="hidden">
* for quote/backslash characters, so they are hidden from the user,
* but still in place to make copying simpler
*
* Note: Selection works in Chrome, but not in Firefox?
*/
function recipient_input_name(text)
{
var i, char, result = '', len = text.length;
if (text.charAt(0) != '"' && text.indexOf('"') > -1) {
text = '"' + text.replace('\\', '\\\\').replace('"', '\\"') + '"';
}
for (i=0; i<len; i++) {
char = text.charAt(i);
switch (char) {
case '"':
if (i > 0 && i < len - 1) {
result += '"';
break;
}
result += '<span class="quotes">' + char + '</span>';
break;
case '\\':
result += '<span class="quotes">' + char + '</span>';
if (text.charAt(i+1) == '\\') {
result += char;
i++;
}
break;
case '<':
result += '&lt;';
break;
case '>':
result += '&gt;';
break;
default:
result += char;
}
}
return result;
};
/**
* Displays dialog to edit a recipient entry
*/
function recipient_input_edit_dialog(e, callback)
{
var element = $(e.target).parents('.recipient'),
recipient = element.text().replace(/,+$/, ''),
input = $('<input>').attr({type: 'text', size: 50}).val(recipient),
content = $('<label>').text(rcmail.gettext('recipient')).append(input);
rcmail.simple_dialog(content, 'recipientedit', function() {
var result, value = input.val();
if (value) {
if (value != recipient) {
result = recipient_input_parser(value);
if (result.recipients.length != 1) {
return false;
}
callback(result.recipients[0].name, result.recipients[0].email, element);
}
return true;
}
});
};
/**
* Adds logic to the contact photo widget
*/
function image_upload_input(obj)
{
var reset_button = $('<a>')
.attr({'class': 'icon button delete', href: '#', })
.click(function(e) { rcmail.command('delete-photo', '', this, e); return false; }),
img = $(obj).find('img')[0],
img_onload = function() {
var state = (img.currentSrc || img.src).indexOf(rcmail.env.photo_placeholder) != -1;
$(obj)[state ? 'removeClass' : 'addClass']('changed');
};
$(obj).append(reset_button).click(function() { rcmail.upload_input('upload-form'); });
// Note: Looks like only Firefox does not need this separate call
img_onload();
$(img).on('load', img_onload);
};
/**
* Displays loading... overlay for iframes
*/
function iframe_loader(frame)
{
frame = $(frame);
if (frame.length) {
var loader = $('<div class="iframe-loader">')
.append($('<div class="spinner spinner-border" role="status">')
.append($('<span class="sr-only">').text(rcmail.gettext('loading'))));
// custom 'loaded' event is expected to be triggered by plugins
// when using the loader not on an iframe
frame.on('load error loaded', function() {
// wait some time to make sure the iframe stopped loading
setTimeout(function() { loader.remove(); }, 500);
})
.parent().append(loader);
// fix scrolling in iOS
if (ios) {
frame.parent().addClass('ios-scroll');
}
}
};
/**
* Convert checkbox input into Bootstrap's custom switch
*/
function pretty_checkbox(checkbox)
{
var label, parent, id, checkbox = $(checkbox);
if (checkbox.is('.custom-control-input')) {
return;
}
if (!(id = checkbox.attr('id'))) {
id = 'icochk' + (++env.checkboxes);
checkbox.attr('id', id);
}
if (checkbox.parent().is('label')) {
label = checkbox.parent();
checkbox = checkbox.detach();
label.before(checkbox);
}
else {
label = $('<label>');
}
label.attr({'for': id, 'class': 'custom-control-label', title: checkbox.attr('title') || ''})
.on('click', function(e) { e.stopPropagation(); });
checkbox.addClass('form-check-input custom-control-input')
.wrap('<div class="custom-control custom-switch">')
.parent().append(label);
};
/**
* Fix pretty checkbox input in a cloned element
*/
function pretty_checkbox_fix(params)
{
var id, input = $(params.row).find('input[id^=icochk]');
if (input.length) {
id = 'icochk' + (++env.checkboxes);
input.attr('id', id).next('label').attr('for', id);
}
};
/**
* Make select dropdowns pretty
* TODO: searching, optgroup, [multiple], iPhone/iPad
*/
function pretty_select(select)
{
// iPhone is not supported yet (problem with browser dropdown on focus)
if (bw.iphone || bw.ipad) {
return;
}
select = $(select);
if (select.is('.pretty-select')) {
return;
}
var select_ident = 'select' + select.attr('id') + select.attr('name');
var is_menu_open = function() {
// Use proper window in cases when the select element intialized
// inside an iframe is then used in a dialog inside a parent's window
// For some reason we can't access data-button property in cross-window
// case, we use data-ident attribute instead
var win = select[0].ownerDocument.defaultView;
if (win.$('.select-menu .listing').data('ident') == select_ident) {
return true;
}
};
var close_func = function() {
var open = is_menu_open();
select.popover('dispose').focus();
return !open;
};
var open_func = function(e) {
var last_char, last_index = -1, items = [], index = [],
dialog = select.closest('.ui-dialog')[0],
max_height = (document.documentElement.clientHeight || $(document.body).height()) - 75,
max_width = $(document.body).width() - 20,
min_width = Math.min(select.outerWidth(), max_width),
value = select.val();
if (!is_mobile()) {
max_height *= 0.5;
}
// close other popups
popups_close(e);
$('option', select).each(function() {
var label = $(this).text(),
link = $('<a href="#">')
.data('value', this.value)
.addClass(this.disabled ? 'disabled' : 'active' + (this.value == value ? ' selected' : ''));
if (label.length) {
link.text(label);
index.push(this.disabled ? '' : label.charAt(0).toLowerCase());
}
else {
link.html('&nbsp;'); // link can't be empty
index.push('');
}
items.push($('<li>').append(link));
});
var list = $('<ul class="listing selectable iconized">')
.attr('data-ident', select_ident)
.data('button', select[0])
.append(items)
.on('click', 'a.active', function() {
// first close the list, then update the select, the order is important
// for cases when the select might be removed in change event (datepicker)
var val = $(this).data('value'), ret = close_func();
select.val(val).change();
return ret;
})
.on('keydown', 'a.active', function(e) {
var item, char, last, node, mode = 'next';
switch (e.which) {
case 27: // ESC
case 9: // TAB
return close_func();
case 13: // ENTER
case 32: // SPACE
$(this).click();
return false; // for IE
case 38: // ARROW-UP
case 63232:
mode = 'previous';
// no-break
case 40: // ARROW-DOWN
case 63233:
item = e.target.parentNode;
while (item = item[mode + 'Sibling']) {
if (node = $(item).children('.active')[0]) {
node.focus();
break;
}
}
return false; // prevents from scrolling the whole page
default:
// A letter key has been pressed, search mode
char = e.originalEvent.key;
if (char && char.length == 1) {
char = char.toLowerCase();
if (last_char != char) {
last_index = -1;
}
last = index.indexOf(char, last_index + 1);
if (last > -1 || (last = index.indexOf(char)) > -1) {
list.find('a').eq(last).focus();
}
last_char = char;
last_index = last;
}
}
});
select.popover('dispose')
.popover({
// because of focus issues we can't always use body,
// if select is in a dialog, popover has to be a child of this dialog
container: dialog || document.body,
content: list[0],
placement: 'bottom',
trigger: 'manual',
boundary: 'viewport',
html: true,
offset: '0,2',
sanitize: false,
template: '<div class="popover select-menu" style="min-width: ' + min_width + 'px; max-width: ' + max_width + 'px">'
+ '<div class="popover-header"></div>'
+ '<div class="popover-body" style="max-height: ' + max_height + 'px"></div></div>'
})
.on('shown.bs.popover', function() {
select.focus(); // for Chrome
// Set popup Close title
list.parent().prev()
.empty()
.append($('<a class="button icon cancel">').text(rcmail.gettext('close'))
.on('click', function(e) {
e.stopPropagation();
return close_func();
})
);
// Find the selected item, focus it
var selected = list.find('a.selected').first();
if (selected.focus().length) {
var list_parent = list.parent();
// try to scroll the list so focused element is in center
last_index = list.find('a').index(selected[0]);
last_char = index[last_index];
if (last_index > 5) {
list_parent.scrollTop(list_parent.scrollTop() + list_parent.height()/2);
}
}
// focus first active element on the list
else if (rcube_event.is_keyboard(e)) {
list.find('a.active').first().focus();
}
// don't propagate mousedown event
list.on('mousedown', function(e) { e.stopPropagation(); });
})
.popover('show');
};
select.addClass('pretty-select custom-select form-control')
.on('mousedown keydown', function(e) {
select = $(e.target); // so it works after clone
// Do nothing on disabled select or on TAB key
if (select.prop('disabled')) {
return;
}
if (e.which == 9) {
close_func();
return true;
}
// Close popup on ESC key or on click if already open
if (e.which == 27 || (e.type == 'mousedown' && is_menu_open())) {
return close_func();
}
select.focus();
// prevent displaying browser-default select dropdown
select.prop('disabled', true);
setTimeout(function() { select.prop('disabled', false); }, 0);
e.stopPropagation();
// display options in our way (on SPACE, ENTER, ARROW-DOWN or mousedown)
if (e.type == 'mousedown' || e.which == 13 || e.which == 32 || e.which == 40 || e.which == 63233) {
open_func(e);
// Prevent from closing the menu by general popover closing handler (popups_close())
// We used to just stop propagation in onclick handler, but it didn't work
// in Chrome where onclick handler wasn't invoked on mobile (#6705)
popups_close_lock = new Date().getTime();
return false;
}
})
};
/**
* HTML editor textarea wrapper with nice looking tabs-like switch
*/
function html_editor_init(obj)
{
// Here we support two structures
// 1. <div><textarea></textarea><select class="hidden"></div>
// 2. <tr><td><td><td><textarea></textarea></td></tr>
// <tr><td><td><td><input type="checkbox"></td></tr>
var sw, is_table = false,
editor = $(obj),
parent = editor.parent(),
plain_btn = $('<a class="mce-i-html" href="#" tabindex="-1"></a>')
.attr('title', rcmail.gettext('htmltoggle'))
.on('click', function(e) {
if (rcmail.command('toggle-editor', {id: editor.attr('id'), html: true}, '', e.originalEvent)) {
parent.addClass('ishtml');
}
})
.on('keydown', function(e) {
if (e.which == 9) { // TAB
editor.focus();
return false;
}
}),
toolbar = $('<div class="editor-toolbar">').append(plain_btn);
if (parent.is('td')) {
sw = $('input[type="checkbox"]', parent.parent().next());
is_table = true;
}
else {
sw = editor.next('select.hidden');
}
// make the textarea autoresizeable
textarea_autoresize_init(obj);
// sanity check
if (sw.length != 1) {
return;
}
parent.addClass('html-editor');
editor.after(toolbar).data('control', sw)
.on('keydown', function(e) {
// ALT + F10 is the way to access toolbar in TinyMCE, let's do the same for plain editor
if (e.altKey && e.which == 121) {
plain_btn.focus();
}
});
if (is_table) {
// Hide unwanted table cells
sw.parents('tr').first().hide();
parent.prev().hide();
// Modify the textarea cell to use 100% width
parent.addClass('col-sm-12');
}
};
/**
* Make the textarea autoresizeable depending on it's content length.
* The way there's no vertical scrollbar.
*/
function textarea_autoresize_init(textarea)
{
var padding = parseInt($(textarea).css('padding-top')) + parseInt($(textarea).css('padding-bottom')) + 2,
// FIXME: Is there a better way to get initial height of the textarea?
// At this moment clientHeight/offsetHeight is 0.
min_height = ($(textarea)[0].rows || 5) * 21,
resize = function() {
// Wait until the textarea is visible
if (!textarea.scrollHeight) {
return setTimeout(resize, 250);
}
if (textarea.scrollHeight - padding <= min_height) {
return;
}
// To fix scroll-jump issue in Edge we'll find the scrolling parent
// and re-apply scrollTop value after we reset textarea height
var scroll_element, scroll_pos = 0;
$(textarea).parents().each(function() {
if (textarea.scrollTop > 0) {
scroll_element = textarea;
scroll_pos = textarea.scrollTop;
return false;
}
});
var oldHeight = $(textarea).outerHeight();
$(textarea).outerHeight(0);
var newHeight = Math.max(min_height, textarea.scrollHeight);
$(textarea).outerHeight(oldHeight);
if (newHeight !== oldHeight) {
$(textarea).height(newHeight);
}
if (scroll_pos) {
scroll_element.scrollTop = scroll_pos;
}
};
$(textarea).on('input', resize).trigger('input');
};
// Inititalizes smart list input
function smart_field_init(field)
{
var tip, id = field.id + '_list',
area = $('<div class="multi-input"><div class="content"></div><div class="invalid-feedback"></div></div>'),
list = field.value ? field.value.split("\n") : [''];
if ($('#' + id).length) {
return;
}
// add input rows
$.each(list, function(i, v) {
smart_field_row_add($('.content', area), v, i, field);
});
area.attr('id', id);
field = $(field);
if (field.attr('disabled')) {
area.hide();
}
// disable the original field anyway, we don't want it in POST
else {
field.prop('disabled', true);
}
if (field.data('hidden')) {
area.hide();
}
field.after(area);
if (field.hasClass('is-invalid')) {
area.addClass('is-invalid');
$('.invalid-feedback', area).text(field.data('error-msg'));
}
};
function smart_field_row_add(area, value, idx, field, after)
{
// build row element content
var input,
elem = $('<div class="input-group">'
+ '<input type="text" class="form-control">'
+ '<span class="input-group-append"><a class="icon reset input-group-text" href="#"></a></span>'
+ '</div>');
input = elem.find('input').attr({
value: value,
name: field.name + '[]',
size: $(field).data('size'),
title: field.title,
placeholder: field.placeholder
})
.keydown(function(e) {
// element creation event (on Enter)
if (e.which == 13) {
var elem = smart_field_row_add(area, '', (new Date()).getTime(), field, input.parent());
$('input', elem).focus();
}
// backspace or delete: remove input, focus previous one
else if ((e.which == 8 || e.which == 46) && input.val() == '') {
var parent = input.parent(),
siblings = area.children();
if (siblings.length > 1) {
if (parent.prev().length) {
parent.prev().children('input').focus();
}
else {
parent.next().children('input').focus();
}
parent.remove();
return false;
}
}
});
// element deletion event
elem.find('a.reset').click(function() {
var record = $(this.parentNode.parentNode);
if (area.children().length > 1) {
$('input', record.next().length ? record.next() : record.prev()).focus();
record.remove();
}
else {
$('input', record).val('').focus();
}
});
elem.find('input,a')
.on('focus', function() { area.addClass('focused'); })
.on('blur', function() { area.removeClass('focused'); });
if (after) {
after.after(elem);
}
else {
elem.appendTo(area);
}
return elem;
};
// Reset and fill the smart list input with new data
function smart_field_reset(field, data)
{
var id = field.id + '_list',
list = data.length ? data : [''],
area = $('#' + id).children('.content');
area.empty();
// add input rows
$.each(list, function(i, v) {
smart_field_row_add(area, v, i, field);
});
};
/**
* Register form errors, mark fields as invalid, dsplay the error below the input
*/
function form_errors(tips)
{
$.each(tips, function() {
var input = $('#' + this[0]).addClass('is-invalid');
if (input.data('type') == 'list') {
input.data('error-msg', this[2]);
$('#' + this[0] + '_list > .invalid-feedback').text(this[2]);
return;
}
input.after($('<span class="invalid-feedback">').text(this[2]));
});
};
/**
* Show/hide the navigation list
*/
function switch_nav_list(obj)
{
var records, height, speed = 250,
button = $('a', obj),
navlist = $(obj).next();
if (!navlist.height()) {
records = $('tr,li', navlist).filter(function() { return this.style.display != 'none'; });
height = $(records[0]).height() || 50;
navlist.animate({height: (Math.min(5, records.length) * height + 1) + 'px'}, speed);
button.addClass('collapse').removeClass('expand');
$(obj).addClass('expanded');
}
else {
navlist.animate({height: '0'}, speed);
button.addClass('expand').removeClass('collapse');
$(obj).removeClass('expanded');
}
};
/**
* Create a splitter (resizing) element on a layout column
*/
function splitter_init(node)
{
// Use id of the list element, if exists, as a part of the key, instead of action.column-id
// This way e.g. the sidebar in Settings is always the same width for all Settings' pages
var list_id = node.find('.scroller .listing').first().attr('id'),
key = rcmail.env.task + '.' + (list_id || (rcmail.env.action + '.' + node.attr('id'))),
pos = get_pref(key),
inverted = node.is('.sidebar-right'),
set_width = function(width) {
node.css({
width: Math.max(100, width),
// reset default properties
// 'min-width': 100,
flex: 'none'
});
};
if (!node[inverted ? 'prev' : 'next']().length) {
return;
}
$('<div class="column-resizer">')
.addClass(inverted ? 'inverted' : null)
.appendTo(node)
.on('mousedown', function(e) {
var ts, splitter = $(this), offset = node.position().left;
// Makes col-resize cursor follow the mouse pointer on dragging
// and fixes issues related to iframes
splitter.addClass('active');
// Disable selection on document while dragging
// It can happen when you move mouse out of window, on top
document.body.style.userSelect = 'none';
// Start listening to mousemove events
$(document)
.on('mousemove.resizer', function(e) {
// Use of timeouts makes the move more smooth in Chrome
clearTimeout(ts);
ts = setTimeout(function() {
// For left-side-splitter we need the current offset
if (inverted) {
offset = node.position().left;
}
var cursor_position = rcube_event.get_mouse_pos(e).x,
width = inverted ? node.width() + (offset - cursor_position) : cursor_position - offset;
set_width(width);
}, 5);
})
.on('mouseup.resizer', function() {
// Remove registered events
$(document).off('.resizer');
$('iframe').off('.resizer');
document.body.style.userSelect = 'auto';
// Set back the splitter width to normal
splitter.removeClass('active');
// Save the current position (width)
save_pref(key, node.width());
});
});
if (pos) {
set_width(pos);
}
};
/**
* Wrapper for rcmail.open_window to intercept window opening
* and display a dialog with an iframe instead of a real window.
*/
function window_open(url)
{
// Use 4th argument to bypass the dialog-mode e.g. for external windows
if (!is_mobile() || arguments[3] === true) {
return env.open_window.apply(rcmail, arguments);
}
// _extwin=1, _framed=1 are required to display attachment preview
// layout properly and make mobile menus working
url = rcmail.add_url(url, '_framed', 1);
url = rcmail.add_url(url, '_extwin', 1);
var label, title = '',
props = {cancel_button: 'close', width: 768, height: 768},
frame = $('<iframe>').attr({id: 'windowframe', src: url});
if (/_action=([a-z_]+)/.test(url) && (label = rcmail.labels[RegExp.$1])) {
title = label;
}
if (/_frame=1/.test(url)) {
props.dialogClass = 'no-titlebar';
}
rcmail.simple_dialog(frame, title, null, props);
return true;
};
/**
* Get layout modes. In frame mode returns the parent layout modes.
*/
function layout_metadata()
{
if (is_framed) {
var doc = $(parent.document.documentElement);
return {
mode: doc[0].className.match(/layout-([a-z]+)/) ? RegExp.$1 : mode,
touch: doc.is('.touch'),
};
}
return {mode: mode, touch: touch};
};
/**
* Returns true if the layout is in 'small' or 'phone' mode
*/
function is_mobile()
{
var meta = layout_metadata();
return meta.mode == 'phone' || meta.mode == 'small';
};
/**
* Returns true if the layout is in 'touch' mode
*/
function is_touch()
{
var meta = layout_metadata();
return meta.touch;
};
/**
* Get preference stored in browser
*/
function get_pref(key)
{
if (!prefs) {
prefs = rcmail.local_storage_get_item('prefs.elastic', {});
}
// fall-back to cookies
if (prefs[key] == null) {
var cookie = rcmail.get_cookie(key);
if (cookie != null) {
prefs[key] = cookie;
// copy value to local storage and remove cookie (if localStorage is supported)
if (rcmail.local_storage_set_item('prefs.elastic', prefs)) {
rcmail.set_cookie(key, cookie, new Date()); // expire cookie
}
}
}
return prefs[key];
};
/**
* Saves preference value to browser storage
*/
function save_pref(key, val)
{
prefs[key] = val;
// write prefs to local storage (if supported)
if (!rcmail.local_storage_set_item('prefs.elastic', prefs)) {
// store value in cookie
var exp = new Date();
exp.setYear(exp.getFullYear() + 1);
rcmail.set_cookie(key, val, exp);
}
};
}
if (window.rcmail) {
/**
* Elastic version of show_menu as we don't need e.g. menu positioning from core
* TODO: keyboard navigation in menus
*/
rcmail.show_menu = function(prop, show, event)
{
var name = typeof prop == 'object' ? prop.menu : prop,
obj = $('#' + name);
if (typeof prop == 'string') {
prop = {menu: name};
}
// just delegate the action to rcube_elastic_ui
return rcmail.triggerEvent(show === false ? 'menu-close' : 'menu-open', {name: name, obj: obj, props: prop, originalEvent: event});
}
/**
* Elastic version of hide_menu as we don't need e.g. menus stack handling
*/
rcmail.hide_menu = function(name, event)
{
// delegate to rcube_elastic_ui
return rcmail.triggerEvent('menu-close', {name: name, props: {menu: name}, originalEvent: event});
}
}
else {
// rcmail does not exists e.g. on the error template inside a frame
// we fake the engine a little
var rcmail = parent.rcmail,
rcube_webmail = parent.rcube_webmail,
bw = {};
}
var UI = new rcube_elastic_ui();
// Improve non-inline datepickers
if ($ && $.datepicker) {
var __newInst = $.datepicker._newInst;
$.extend($.datepicker, {
_newInst: function(target, inline) {
var inst = __newInst.call(this, target, inline);
if (!inst.inline) {
UI.datepicker_init(inst.dpDiv);
}
return inst;
}
});
}
diff --git a/skins/larry/templates/contactprint.html b/skins/larry/templates/contactprint.html
index b83dcc464..2f0083c36 100644
--- a/skins/larry/templates/contactprint.html
+++ b/skins/larry/templates/contactprint.html
@@ -1,20 +1,20 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<roundcube:object name="meta" />
<roundcube:object name="links" />
<link rel="stylesheet" type="text/css" href="/print.css" />
</head>
<body>
-<div id="header"><roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" type="print" /></div>
+<div id="header"><roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" logo-type="print" logo-match="all" /></div>
<div id="contact-details" class="boxcontent">
<div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
<roundcube:object name="contacthead" id="contacthead" />
<br style="clear:both" />
<roundcube:object name="contactdetails" />
</div>
</body>
</html>
diff --git a/skins/larry/templates/messageprint.html b/skins/larry/templates/messageprint.html
index 53916656a..815926c55 100644
--- a/skins/larry/templates/messageprint.html
+++ b/skins/larry/templates/messageprint.html
@@ -1,19 +1,19 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<roundcube:object name="meta" />
<roundcube:object name="links" />
<link rel="stylesheet" type="text/css" href="/print.css" />
</head>
<body>
-<div id="header"><roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" type="print" /></div>
+<div id="header"><roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" logo-type="print" logo-match="all" /></div>
<div id="printmessageframe">
<roundcube:object name="messageHeaders" class="headers-table" max="10" />
<roundcube:object name="messageAttachments" id="attachment-list" />
<roundcube:object name="messageBody" id="messagebody" showImages="false" />
</div>
</body>
</html>
diff --git a/tests/Rcmail/OutputHtml.php b/tests/Rcmail/OutputHtml.php
index 2943acbd3..ee1562c01 100644
--- a/tests/Rcmail/OutputHtml.php
+++ b/tests/Rcmail/OutputHtml.php
@@ -1,180 +1,276 @@
<?php
/**
* Test class to test rcmail_output_html class
*
* @package Tests
*/
class Rcmail_RcmailOutputHtml extends PHPUnit\Framework\TestCase
{
/**
* Test get_template_logo()
*/
function test_logo()
{
$rcmail = rcube::get_instance();
$output = new rcmail_output_html();
$reflection = new ReflectionClass('rcmail_output_html');
$set_skin = $reflection->getProperty('skin_name');
$set_template = $reflection->getProperty('template_name');
$get_template_logo = $reflection->getMethod('get_template_logo');
$set_skin->setAccessible(true);
$set_template->setAccessible(true);
$get_template_logo->setAccessible(true);
$set_skin->setValue($output, 'elastic');
$rcmail->config->set('skin_logo', 'img00');
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img00', $result);
$set_template->setValue($output, 'mail');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img00', $result);
$rcmail->config->set('skin_logo', array(
"elastic:login[small]" => "img01",
"elastic:login" => "img02",
"elastic:*[small]" => "img03",
"larry:*" => "img04",
"*:login[small]" => "img05",
"*:login" => "img06",
"*[print]" => "img07",
"*" => "img08",
));
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon'));
+ $this->assertSame(null, $result);
+
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon', 'template'));
+ $this->assertSame('img02', $result);
+
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon', 'all'));
+ $this->assertSame('img02', $result);
+
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img01', $result);
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img02', $result);
$set_template->setValue($output, 'mail');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img03', $result);
$set_template->setValue($output, 'mail');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img08', $result);
- $set_template->setValue($output, 'mail');
- $result = $get_template_logo->invokeArgs($output, array('small'));
- $this->assertSame('img03', $result);
-
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img08', $result);
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('print'));
+ $this->assertSame('img07', $result);
+
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('print', 'template'));
+ $this->assertSame('img07', $result);
+
$set_skin->setValue($output, 'larry');
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon'));
+ $this->assertSame(null, $result);
+
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon', 'template'));
+ $this->assertSame('img06', $result);
+
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon', 'all'));
+ $this->assertSame('img04', $result);
+
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img05', $result);
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img04', $result);
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img04', $result);
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('print', 'template'));
+ $this->assertSame('img07', $result);
+
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('print'));
+ $this->assertSame('img07', $result);
+
$set_skin->setValue($output, '_test_');
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('favicon'));
+ $this->assertSame(null, $result);
+
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('print', 'template'));
+ $this->assertSame('img06', $result);
+
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img05', $result);
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img06', $result);
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array('print'));
$this->assertSame('img07', $result);
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('_test_'));
+ $this->assertSame(null, $result);
+
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img08', $result);
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('print', 'template'));
+ $this->assertSame('img07', $result);
+
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('print'));
+ $this->assertSame('img07', $result);
+
$rcmail->config->set('skin_logo', array(
"elastic:login[small]" => "img09",
"elastic:login" => "img10",
"larry:*" => "img11",
"elastic[small]" => "img12",
"login[small]" => "img13",
"login" => "img14",
"[print]" => "img15",
"*" => "img16",
));
$set_skin->setValue($output, 'elastic');
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img09', $result);
$set_template->setValue($output, 'mail');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame(null, $result);
$set_skin->setValue($output, '_test_');
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array('small'));
$this->assertSame('img13', $result);
$set_template->setValue($output, 'login');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img14', $result);
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array('print'));
$this->assertSame('img15', $result);
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('_test_', 'all'));
+ $this->assertSame('img16', $result);
+
+ $set_template->setValue($output, '_test_');
+ $result = $get_template_logo->invokeArgs($output, array('_test_', 'template'));
+ $this->assertSame(null, $result);
+
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array('_test_'));
$this->assertSame(null, $result);
$set_template->setValue($output, '_test_');
$result = $get_template_logo->invokeArgs($output, array());
$this->assertSame('img16', $result);
+
+ $rcmail->config->set('skin_logo', array(
+ "elastic:[print]" => "img17",
+ "elastic:messageprint" => "img18",
+ "elastic:*" => "img19",
+ ));
+
+ $set_skin->setValue($output, 'elastic');
+
+ $set_template->setValue($output, 'login');
+ $result = $get_template_logo->invokeArgs($output, array('print'));
+ $this->assertSame('img17', $result);
+
+ $set_template->setValue($output, 'messageprint');
+ $result = $get_template_logo->invokeArgs($output, array('_test_', 'template'));
+ $this->assertSame('img18', $result);
+
+ $set_template->setValue($output, 'contactprint');
+ $result = $get_template_logo->invokeArgs($output, array('print', 'template'));
+ $this->assertSame('img17', $result);
+
+ $set_template->setValue($output, 'contactprint');
+ $result = $get_template_logo->invokeArgs($output, array('_test_', 'template'));
+ $this->assertSame(null, $result);
+
+ $set_template->setValue($output, 'contactprint');
+ $result = $get_template_logo->invokeArgs($output, array('_test_', 'all'));
+ $this->assertSame('img19', $result);
+
+ $set_template->setValue($output, 'contactprint');
+ $result = $get_template_logo->invokeArgs($output, array());
+ $this->assertSame('img19', $result);
}
/**
* Data for test_conditions()
*/
function data_conditions()
{
return array(
array("_start_<roundcube:if condition='1' />A<roundcube:endif />_end_", "_start_A_end_"),
array("_start_<roundcube:if condition='0' />A<roundcube:else />B<roundcube:endif />_end_", "_start_B_end_"),
array("_start_<roundcube:if condition='0'/>A<roundcube:else/>B<roundcube:endif/>_end_", "_start_B_end_"),
array("_start_<roundcube:if condition='0'>A<roundcube:else>B<roundcube:endif>_end_", "_start_B_end_"),
array("_start_<roundcube:if condition='0' />A<roundcube:elseif condition='1' />B<roundcube:else />C<roundcube:endif />_end_", "_start_B_end_"),
array("_start_<roundcube:if condition='1' /><roundcube:if condition='0' />A<roundcube:else />B<roundcube:endif />C<roundcube:else />D<roundcube:endif />_end_", "_start_BC_end_"),
array("_start_<roundcube:if condition='1' /><roundcube:if condition='1' />A<roundcube:else />B<roundcube:endif />C<roundcube:else />D<roundcube:endif />_end_", "_start_AC_end_"),
array("_start_<roundcube:if condition='1' /><roundcube:if condition='0' />A<roundcube:elseif condition='1' />B<roundcube:else />C<roundcube:endif />D<roundcube:else />E<roundcube:endif />_end_", "_start_BD_end_"),
array("_start_<roundcube:if condition='0' />A<roundcube:elseif condition='1' /><roundcube:if condition='0' />B<roundcube:else /><roundcube:if condition='1' />C<roundcube:endif />D<roundcube:endif /><roundcube:else />E<roundcube:endif />_end_", "_start_CD_end_"),
array("_start_<roundcube:if condition='0'>A<roundcube:elseif condition='1'><roundcube:if condition='0'>B<roundcube:else><roundcube:if condition='1'>C<roundcube:endif>D<roundcube:endif><roundcube:else>E<roundcube:endif>_end_", "_start_CD_end_")
);
}
/**
* Test text to html conversion
*
* @dataProvider data_conditions
*/
function test_conditions($input, $output)
{
$object = new rcmail_output_html;
$result = $object->just_parse($input);
$this->assertEquals($output, $result);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 21, 11:57 PM (13 h, 9 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80062
Default Alt Text
(330 KB)

Event Timeline