Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F86312
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
99 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index 6494879a5..00ceca85f 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2735 +1,2735 @@
<?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 = [];
protected $js_env = [];
protected $js_labels = [];
protected $js_commands = [];
protected $skin_paths = [];
protected $skin_name = '';
protected $scripts_path = '';
protected $script_files = [];
protected $css_files = [];
protected $scripts = [];
protected $meta_tags = [];
protected $link_tags = ['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><meta name='generator' content='Roundcube'></head>\n<body></body>\n</html>";
// deprecated names of templates used before 0.5
protected $deprecated_templates = [
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
];
// deprecated names of template objects used before 1.4
protected $deprecated_template_objects = [
'addressframe' => 'contentframe',
'messagecontentframe' => 'contentframe',
'prefsframe' => 'contentframe',
'folderframe' => 'contentframe',
'identityframe' => 'contentframe',
'responseframe' => 'contentframe',
'keyframe' => 'contentframe',
'filterframe' => 'contentframe',
];
/**
* Constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = $this->config->get('devel_mode');
$this->set_env('task', $task);
$this->set_env('standard_windows', (bool) $this->config->get('standard_windows'));
$this->set_env('locale', !empty($_SESSION['language']) ? $_SESSION['language'] : 'en_US');
$this->set_env('devel_mode', $this->devel_mode);
// Version number e.g. 1.4.2 will be 10402
$version = explode('.', preg_replace('/[^0-9.].*/', '', RCMAIL_VERSION));
$this->set_env('rcversion', $version[0] * 10000 + $version[1] * 100 + (isset($version[2]) ? $version[2] : 0));
// add cookie info
$this->set_env('cookie_domain', ini_get('session.cookie_domain'));
$this->set_env('cookie_path', ini_get('session.cookie_path'));
$this->set_env('cookie_secure', filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN));
// Easy way to change skin via GET argument, for developers
if ($this->devel_mode && !empty($_GET['skin']) && preg_match('/^[a-z0-9-_]+$/i', $_GET['skin'])) {
if ($this->check_skin($_GET['skin'])) {
$this->set_skin($_GET['skin']);
$this->app->user->save_prefs(['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([
'loginform' => [$this, 'login_form'],
'preloader' => [$this, 'preloader'],
'username' => [$this, 'current_username'],
'message' => [$this, 'message_container'],
'charsetselector' => [$this, 'charset_selector'],
'aboutcontent' => [$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 bool $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)
{
// set absolute path for assets if /index.php/foo/bar url is used
if (empty($path) && !empty($_SERVER['PATH_INFO'])) {
- $path = preg_replace('/\/?\?_task=[a-z]+/', '', $this->app->url([], true));
+ $path = preg_replace('/\?_task=[a-z]+/', '', $this->app->url([], true));
}
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 (isset($this->env['task'])) {
if ($this->env['task'] == 'login') {
$title = $this->app->gettext([
'name' => 'welcome',
'vars' => ['product' => $this->config->get('product_name')]
]);
}
else {
$title = ucfirst($this->env['task']);
}
}
else {
$title = '';
}
if ($full && $title) {
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 = [];
$this->skins = [];
$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([
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Invalid skin name'
], true, false);
return false;
}
$skins_allowed = $this->config->get('skins_allowed');
if (!empty($skins_allowed) && !in_array($skin, (array) $skins_allowed)) {
return false;
}
$path = RCUBE_INSTALL_PATH . 'skins/';
return !empty($skin) && is_dir($path . $skin) && is_readable($path . $skin);
}
/**
* Helper method to recursively read skin meta files and register search paths
*/
private function load_skin($skin_path)
{
$this->skin_paths[] = $skin_path;
// read meta file and check for dependencies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
$meta['path'] = $skin_path;
$path_elements = explode('/', $skin_path);
$skin_id = end($path_elements);
if (empty($meta['name'])) {
$meta['name'] = $skin_id;
}
$this->skins[$skin_id] = $meta;
// Keep skin config for ajax requests (#6613)
$_SESSION['skin_config'] = [];
if (!empty($meta['extends'])) {
$path = RCUBE_INSTALL_PATH . 'skins/';
if (is_dir($path . $meta['extends']) && is_readable($path . $meta['extends'])) {
$_SESSION['skin_config'] = $this->load_skin('skins/' . $meta['extends']);
}
}
if (!empty($meta['config'])) {
foreach ($meta['config'] as $key => $value) {
$this->config->set($key, $value, true);
$_SESSION['skin_config'][$key] = $value;
}
$value = array_merge((array) $this->config->get('dont_override'), array_keys($meta['config']));
$this->config->set('dont_override', $value, true);
}
if (!empty($meta['localization'])) {
$locdir = $meta['localization'] === true ? 'localization' : $meta['localization'];
if ($texts = $this->app->read_localization(RCUBE_INSTALL_PATH . $skin_path . '/' . $locdir)) {
$this->app->load_language($_SESSION['language'], $texts);
}
}
// Use array_merge() here to allow for global default and extended skins
if (!empty($meta['meta'])) {
$this->meta_tags = array_merge($this->meta_tags, (array) $meta['meta']);
}
if (!empty($meta['links'])) {
$this->link_tags = array_merge($this->link_tags, (array) $meta['links']);
}
$this->set_env('dark_mode_support', (bool) $this->config->get('dark_mode_support'));
return $_SESSION['skin_config'];
}
/**
* Check if a specific template exists
*
* @param string $name Template name
*
* @return bool True if template exists, False otherwise
*/
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))
|| (!empty($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 string|false 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 ($file[0] != '/') {
$file = '/' . $file;
}
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[] = ['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 bool $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(['rcube','Q'], $vars);
}
$msgtext = $this->app->gettext(['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 = isset($this->env['task']) ? $this->env['task'] : '';
$env = $all ? null : array_intersect_key($this->env, ['extwin' => 1, 'framed' => 1]);
// keep jQuery-UI files
$css_files = $script_files = [];
foreach ($this->css_files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$css_files[] = $file;
}
}
foreach ($this->script_files as $position => $files) {
foreach ($files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$script_files[$position][] = $file;
}
}
}
parent::reset();
// let some env variables survive
$this->env = $this->js_env = $env;
$this->framed = $framed || !empty($this->env['framed']);
$this->js_labels = [];
$this->js_commands = [];
$this->scripts = [];
$this->header = '';
$this->footer = '';
$this->body = '';
$this->css_files = [];
$this->script_files = [];
// 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 = [], $delay = 1, $secure = false)
{
if (!empty($this->env['extwin']) && !(is_string($p) && preg_match('#^https?://#', $p))) {
if (!is_array($p)) {
$p = ['_action' => $p];
}
$p['_extwin'] = 1;
}
$location = $this->app->url($p, false, false, $secure);
$this->header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin template.
*
* @param string $templ Template name
* @param bool $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([
'code' => 505,
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Recursion alert: ignoring output->send()'
], true, false
);
return;
}
$this->parse($templ, false);
}
else {
$this->framed = true;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
if (!empty($this->script_files)) {
$this->set_env('request_token', $this->app->get_request_token());
}
// Fix assets path on blankpage
if (!empty($this->js_env['blankpage'])) {
$this->js_env['blankpage'] = $this->asset_url($this->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 = [];
$this->script_files = [];
$this->header = '';
$this->footer = '';
}
// write all javascript commands
if (!empty($commands)) {
$this->add_script($commands, 'head_top');
}
$this->page_headers();
// call super method
$this->_write($template);
}
/**
* Send common page headers
* For now it only (re)sets X-Frame-Options when needed
*/
public function page_headers()
{
if (headers_sent()) {
return;
}
// allow (legal) iframe content to be loaded
$framed = $this->framed || !empty($this->env['framed']);
if ($framed && ($xopt = $this->app->config->get('x_frame_options', 'sameorigin'))) {
if (strtolower($xopt) === 'deny') {
$this->header('X-Frame-Options: sameorigin', true);
}
}
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string $name Template name
* @param bool $exit Exit script
* @param bool $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 = [];
$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) {
// skin folder in plugin dir
$plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path;
// plugin folder in skin dir
$plugin_skin_paths[] = $skin_path . '/plugins/' . $plugin;
}
// 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) === false) {
continue;
}
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$name.html";
// fallback to deprecated template names
if (!is_readable($path) && !empty($this->deprecated_templates[$realname])) {
$dname = $this->deprecated_templates[$realname];
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$dname.html";
if (is_readable($path)) {
rcube::raise_error([
'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([
'code' => 404,
'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(
['/\bplugins\//', '/(["\']?)\/this\//'],
[$this->app->plugins->url, '\\1' . $this->app->plugins->url . $skin_dir . '/'],
$templ
);
}
// parse for special tags
$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", [
'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 = [];
// these should be always on top,
// e.g. hide_message() below depends on env.framed
if (!$this->framed && !empty($this->js_env)) {
$top_commands[] = ['set_env', $this->js_env];
}
if (!empty($this->js_labels)) {
$top_commands[] = ['add_label', $this->js_labels];
}
// unlock interface after iframe load
$unlock = isset($_REQUEST['_unlock']) ? preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']) : 0;
if ($this->framed) {
$top_commands[] = ['iframe_loaded', $unlock];
}
else if ($unlock) {
$top_commands[] = ['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 (isset($str[0]) && $str[0] == '/') {
if ($search_path && ($file_url = $this->get_skin_file($str))) {
return $file_url;
}
return $this->base_path . $str;
}
return $str;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
$args = [
'code' => $code,
'message' => $message,
];
$page = new rcmail_action_utils_error;
$page->run($args);
}
/**
* Modify path by adding URL prefix if configured
*
* @param string $path Asset path
* @param bool $abs_url Pass to self::abs_url() first
*
* @return string Asset path
*/
public function asset_url($path, $abs_url = false)
{
// iframe content can't be in a different domain
// @TODO: check if assets are on a different domain
if ($abs_url) {
$path = $this->abs_url($path, true);
}
if (!$this->assets_path || in_array($path[0], ['?', '/', '.']) || 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_\-]+)/', [$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)
{
$regexp = '!(src|href|background|data-src-[a-z]+)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i';
return preg_replace_callback($regexp, [$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)
{
$regexp = '!(src|href|background)=(["\']?)([a-z0-9/_.?=-]+)(["\'\s>])!i';
return preg_replace_callback($regexp, [$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 dip 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)
{
$regexp1 = '/<roundcube:if\s+([^>]+)>/is';
$regexp2 = '/<roundcube:(if|elseif|else|endif)\s*([^>]*)>/is';
$pos = 0;
// Find IF tags and process them
while ($pos < strlen($input) && preg_match($regexp1, $input, $conditions, PREG_OFFSET_CAPTURE, $pos)) {
$pos = $start = $conditions[0][1];
// Process the 'condition' attribute
$attrib = html::parse_attrib_string($conditions[1][0]);
$condmet = isset($attrib['condition']) && $this->check_condition($attrib['condition']);
// Define start/end position of the content to pass into the output
$content_start = $condmet ? $pos + strlen($conditions[0][0]) : null;
$content_end = null;
$level = 0;
$endif = null;
$n = $pos + 1;
// Process the code until the closing tag (for the processed IF tag)
while (preg_match($regexp2, $input, $matches, PREG_OFFSET_CAPTURE, $n)) {
$tag_start = $matches[0][1];
$tag_end = $tag_start + strlen($matches[0][0]);
$tag_name = strtolower($matches[1][0]);
switch ($tag_name) {
case 'if':
$level++;
break;
case 'endif':
if (!$level--) {
$endif = $tag_end;
if ($content_end === null) {
$content_end = $tag_start;
}
break 2;
}
break;
case 'elseif':
if (!$level) {
if ($condmet) {
if ($content_end === null) {
$content_end = $tag_start;
}
}
else {
// Process the 'condition' attribute
$attrib = html::parse_attrib_string($matches[2][0]);
$condmet = isset($attrib['condition']) && $this->check_condition($attrib['condition']);
if ($condmet) {
$content_start = $tag_end;
}
}
}
break;
case 'else':
if (!$level) {
if ($condmet) {
if ($content_end === null) {
$content_end = $tag_start;
}
}
else {
$content_start = $tag_end;
}
}
break;
}
$n = $tag_end;
}
// No ending tag found
if ($endif === null) {
$pos = strlen($input);
if ($content_end === null) {
$content_end = $pos;
}
}
if ($content_start === null) {
$content = '';
}
else {
$content = substr($input, $content_start, $content_end - $content_start);
}
// Replace the whole IF statement with the output content
$input = substr_replace($input, $content, $start, max($endif, $content_end, $pos) - $start);
$pos = $start;
}
return $input;
}
/**
* Determines if a given condition is met
*
* @param string $condition Condition statement
*
* @return bool 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 (!empty($attrib['method']) && strtolower($attrib['method']) == 'post') {
$hidden = new html_hiddenfield(['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(
[
'/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',
],
[
"(isset(\$_SESSION['\\1']) ? \$_SESSION['\\1'] : null)",
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"(isset(\$this->env['\\1']) ? \$this->env['\\1'] : null)",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"(isset(\$_COOKIE['\\1']) ? \$_COOKIE['\\1'] : null)",
"(isset(\$this->browser->{'\\1'}) ? \$this->browser->{'\\1'} : null)",
"'{$this->template_name}'",
],
$expression
);
// Note: We used create_function() before but it's deprecated in PHP 7.2
// and really it was just a wrapper on eval().
return eval("return ($expression);");
}
/**
* Parse variable strings
*
* @param string $type Variable type (env, config etc)
* @param string $name Variable name
*
* @return mixed Variable value
*/
protected function parse_variable($type, $name)
{
$value = '';
switch ($type) {
case 'env':
$value = isset($this->env[$name]) ? $this->env[$name] : null;
break;
case 'config':
$value = $this->config->get($name);
if (is_array($value) && !empty($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 = isset($_SESSION[$name]) ? $_SESSION[$name] : '';
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name], ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
break;
case 'browser':
$value = isset($this->browser->{$name}) ? $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)
{
$regexp = '/<roundcube:([-_a-z]+)\s+((?:[^>]|\\\\>)+)(?<!\\\\)>/Ui';
return preg_replace_callback($regexp, [$this, 'xml_command'], $input);
}
/**
* Callback function for parsing an xml command tag
* and turn it into real html content
*
* @param array $matches Matches array of preg_replace_callback
*
* @return string Tag/Object content
*/
protected function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = html::parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// localize title and summary attributes
if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
$attrib['title'] = $this->app->gettext($attrib['title']);
}
if ($command != 'button' && !empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) {
$attrib['summary'] = $this->app->gettext($attrib['summary']);
}
// execute command
switch ($command) {
// return a button
case 'button':
if (!empty($attrib['name']) || !empty($attrib['command'])) {
return $this->button($attrib);
}
break;
// frame
case 'frame':
return $this->frame($attrib);
break;
// show a label
case 'label':
if (!empty($attrib['expression'])) {
$attrib['name'] = $this->eval_expression($attrib['expression']);
}
if (!empty($attrib['name']) || !empty($attrib['command'])) {
$vars = $attrib + ['product' => $this->config->get('product_name')];
unset($vars['name'], $vars['command']);
$label = $this->app->gettext($attrib + ['vars' => $vars]);
$quoting = null;
if (!empty($attrib['quoting'])) {
$quoting = strtolower($attrib['quoting']);
}
else if (isset($attrib['html'])) {
$quoting = rcube_utils::get_boolean((string) $attrib['html']) ? 'no' : '';
}
// 'noshow' can be used in skins to define new labels
if (!empty($attrib['noshow'])) {
return '';
}
switch ($quoting) {
case 'no':
case 'raw':
break;
case 'javascript':
case 'js':
$label = rcube::JQ($label);
break;
default:
$label = html::quote($label);
break;
}
return $label;
}
break;
case 'add_label':
$this->add_label($attrib['name']);
break;
// include a file
case 'include':
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
break;
}
if ($attrib['file'][0] != '/') {
$attrib['file'] = '/templates/' . $attrib['file'];
}
$old_base_path = $this->base_path;
$include = '';
$attr_skin_path = !empty($attrib['skinpath']) ? $attrib['skinpath'] : null;
if (!empty($attrib['skin_path'])) {
$attr_skin_path = $attrib['skin_path'];
}
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attr_skin_path)) {
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$path = realpath(RCUBE_INSTALL_PATH . $path);
}
if (is_readable($path)) {
$allow_php = $this->config->get('skin_include_php');
$include = $allow_php ? $this->include_php($path) : file_get_contents($path);
$include = $this->parse_conditions($include);
$include = $this->parse_xml($include);
$include = $this->fix_paths($include);
}
$this->base_path = $old_base_path;
return $include;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib + ['content' => '']);
return $hook['content'];
// define a container block
case 'container':
if (!empty($attrib['name']) && !empty($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 + ['content' => '']);
return $hook['content'];
}
break;
// return code for a specific application object
case 'object':
$object = strtolower($attrib['name']);
$content = '';
$handler = null;
// correct deprecated object names
if (!empty($this->deprecated_template_objects[$object])) {
$object = $this->deprecated_template_objects[$object];
}
if (!empty($this->object_handlers[$object])) {
$handler = $this->object_handlers[$object];
}
// execute object handler function
if (is_callable($handler)) {
$this->prepare_object_attribs($attrib);
// We assume that objects with src attribute are internal (in most
// cases this is a watermark frame). We need this to make sure assets_path
// is added to the internal assets paths
$external = empty($attrib['src']);
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += ['alt' => $this->xml_command(['', 'object', 'name="productname"'])];
// 'type' attribute added in 1.4 was renamed 'logo-type' in 1.5
// check both for backwards compatibility
$logo_type = !empty($attrib['logo-type']) ? $attrib['logo-type'] : null;
$logo_match = !empty($attrib['logo-match']) ? $attrib['logo-match'] : null;
if (!empty($attrib['type']) && empty($logo_type)) {
$logo_type = $attrib['type'];
}
if (($template_logo = $this->get_template_logo($logo_type, $logo_match)) !== null) {
$attrib['src'] = $template_logo;
}
$additional_logos = [];
$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);
}
else if (!empty($attrib['data-src-' . $type])) {
$additional_logos[$type] = $this->abs_url($attrib['data-src-' . $type]);
}
}
if (!empty($additional_logos)) {
$this->set_env('additional_logos', $additional_logos);
}
if (!empty($attrib['src'])) {
$content = html::img($attrib);
}
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string) RCMAIL_VERSION;
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle(false));
}
else if ($object == 'pagetitle') {
// Deprecated, <title> will be added automatically
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'contentframe') {
if (empty($attrib['id'])) {
$attrib['id'] = 'rcm' . $this->env['task'] . 'frame';
}
// parse variables
if (preg_match('/^(config|env):([a-z0-9_]+)$/i', $attrib['src'], $matches)) {
$attrib['src'] = $this->parse_variable($matches[1], $matches[2]);
}
$content = $this->frame($attrib, true);
}
else if ($object == 'meta' || $object == 'links') {
if ($object == 'meta') {
$source = 'meta_tags';
$tag = 'meta';
$key = 'name';
$param = 'content';
}
else {
$source = 'link_tags';
$tag = 'link';
$key = 'rel';
$param = 'href';
}
foreach ($this->$source as $name => $vars) {
// $vars can be in many forms:
// - string
// - array('key' => 'val')
// - array(string, string)
// - array(array(), string)
// - array(array('key' => 'val'), array('key' => 'val'))
// normalise this for processing by checking for string array keys
$vars = is_array($vars) ? (count(array_filter(array_keys($vars), 'is_string')) > 0 ? [$vars] : $vars) : [$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 : [$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, [$key => $name, 'nl' => true] + $args);
}
}
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + ['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', [$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;
// these commands can be called directly via url
$a_static_commands = ['compose', 'list', 'preferences', 'folders', 'identities'];
if (empty($attrib['command']) && empty($attrib['name']) && empty($attrib['href'])) {
return '';
}
$command = !empty($attrib['command']) ? $attrib['command'] : null;
$action = $command ?: (!empty($attrib['name']) ? $attrib['name'] : null);
if (!empty($attrib['task'])) {
$command = $attrib['task'] . '.' . $command;
$element = $attrib['task'] . '.' . $action;
}
else {
$element = (!empty($this->env['task']) ? $this->env['task'] . '.' : '') . $action;
}
$disabled_actions = (array) $this->config->get('disabled_actions');
// remove buttons for disabled actions
if (in_array($element, $disabled_actions) || in_array($action, $disabled_actions)) {
return '';
}
// try to find out the button type
if (!empty($attrib['type'])) {
$attrib['type'] = strtolower($attrib['type']);
if (strpos($attrib['type'], '-menuitem')) {
$attrib['type'] = substr($attrib['type'], 0, -9);
$menuitem = true;
}
}
else if (!empty($attrib['image']) || !empty($attrib['imagepas']) || !empty($attrib['imageact'])) {
$attrib['type'] = 'image';
}
else {
$attrib['type'] = 'button';
}
if (empty($attrib['image'])) {
if (!empty($attrib['imagepas'])) {
$attrib['image'] = $attrib['imagepas'];
}
else if (!empty($attrib['imageact'])) {
$attrib['image'] = $attrib['imageact'];
}
}
if (empty($attrib['id'])) {
// ensure auto generated IDs are unique between main window and content frame
// Elastic skin duplicates buttons between the two on smaller screens (#7618)
$prefix = ($this->framed || !empty($this->env['framed'])) ? 'frm' : '';
$attrib['id'] = sprintf('rcmbtn%s%d', $prefix, $s_button_count++);
}
// get localized text for labels and titles
$domain = !empty($attrib['domain']) ? $attrib['domain'] : null;
if (!empty($attrib['title'])) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $domain));
}
if (!empty($attrib['label'])) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $domain));
}
if (!empty($attrib['alt'])) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $domain));
}
// set accessibility attributes
if (empty($attrib['role'])) {
$attrib['role'] = 'button';
}
if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
if (array_key_exists('tabindex', $attrib)) {
$attrib['data-tabindex'] = $attrib['tabindex'];
}
$attrib['tabindex'] = '-1'; // disable button by default
$attrib['aria-disabled'] = 'true';
}
// set title to alt attribute for IE browsers
if ($this->browser->ie && empty($attrib['title']) && !empty($attrib['alt'])) {
$attrib['title'] = $attrib['alt'];
}
// add empty alt attribute for XHTML compatibility
if (!isset($attrib['alt'])) {
$attrib['alt'] = '';
}
// register button in the system
if (!empty($attrib['command'])) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
self::JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
!empty($attrib['imageact']) ? $this->abs_url($attrib['imageact']) : (!empty($attrib['classact']) ? $attrib['classact'] : ''),
!empty($attrib['imagesel']) ? $this->abs_url($attrib['imagesel']) : (!empty($attrib['classsel']) ? $attrib['classsel'] : ''),
!empty($attrib['imageover']) ? $this->abs_url($attrib['imageover']) : ''
));
// make valid href to specific buttons
if (in_array($attrib['command'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(['task' => $attrib['command']]);
$attrib['onclick'] = sprintf("return %s.command('switch-task','%s',this,event)", self::JS_OBJECT_NAME, $attrib['command']);
}
else if (!empty($attrib['task']) && in_array($attrib['task'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(['action' => $attrib['command'], 'task' => $attrib['task']]);
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = $this->app->url(['action' => $attrib['command']]);
}
else if (($attrib['command'] == 'permaurl' || $attrib['command'] == 'extwin') && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (empty($attrib['href'])) {
$attrib['href'] = '#';
}
if (!empty($attrib['task'])) {
if (!empty($attrib['classact'])) {
$attrib['class'] = $attrib['classact'];
}
}
else if ($command && empty($attrib['onclick'])) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
self::JS_OBJECT_NAME,
$command,
!empty($attrib['prop']) ? $attrib['prop'] : ''
);
}
$out = '';
$btn_content = null;
$link_attrib = [];
// generate image tag
if ($attrib['type'] == 'image') {
$attrib_str = html::attrib_string(
$attrib,
[
'style', 'class', 'id', 'width', 'height', 'border', 'hspace',
'vspace', 'align', 'alt', 'tabindex', 'title'
]
);
$btn_content = sprintf('<img src="%s"%s />', $this->abs_url($attrib['image']), $attrib_str);
if (!empty($attrib['label'])) {
$btn_content .= ' '.$attrib['label'];
}
$link_attrib = ['href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'target'];
}
else if ($attrib['type'] == 'link') {
$btn_content = isset($attrib['content']) ? $attrib['content'] : (!empty($attrib['label']) ? $attrib['label'] : $attrib['command']);
$link_attrib = array_merge(html::$common_attrib, ['href', 'onclick', 'tabindex', 'target', 'rel']);
if (!empty($attrib['innerclass'])) {
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if (!empty($attrib['label'])) {
$attrib['value'] = $attrib['label'];
}
if (!empty($attrib['command'])) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, ['type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled']);
}
else {
if (!empty($attrib['label'])) {
$attrib['value'] = $attrib['label'];
}
if (!empty($attrib['command'])) {
$attrib['disabled'] = 'disabled';
}
$content = isset($attrib['content']) ? $attrib['content'] : $attrib['label'];
$out = html::tag('button', $attrib, $content, ['type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled']);
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, $link_attrib);
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
if (!empty($attrib['wrapper'])) {
$out = html::tag($attrib['wrapper'], null, $out);
}
if (!empty($menuitem)) {
$class = !empty($attrib['menuitem-class']) ? ' class="' . $attrib['menuitem-class'] . '"' : '';
$out = '<li role="menuitem"' . $class . '>' . $out . '</li>';
}
return $out;
}
/**
* Link an external script file
*
* @param string $file File URL
* @param string $position Target position [head|head_bottom|foot]
*/
public function include_script($file, $position = 'head', $add_path = true)
{
if ($add_path && !preg_match('|^https?://|i', $file) && $file[0] != '/') {
$file = $this->file_mod($this->scripts_path . $file);
}
if (!isset($this->script_files[$position]) || !is_array($this->script_files[$position])) {
$this->script_files[$position] = [];
}
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 before </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([], $script);
};
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script("\$(function() {\n" . $this->scripts['docready'] . "\n});", 'foot');
}
$page_header = '';
$page_footer = '';
$meta = '';
// declare page language
if (!empty($_SESSION['language'])) {
$lang = substr($_SESSION['language'], 0, 2);
$output = preg_replace('/<html/', '<html lang="' . html::quote($lang) . '"', $output, 1);
if (!headers_sent()) {
$this->header('Content-Language: ' . $lang);
}
}
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
$this->header('Content-Type: text/html; charset=' . $this->charset);
}
$meta .= html::tag('meta', [
'http-equiv' => 'content-type',
'content' => "text/html; charset={$this->charset}",
'nl' => true
]);
}
// include page title (after charset specification)
$meta .= '<title>' . html::quote($this->get_pagetitle()) . "</title>\n";
$output = preg_replace('/(<head[^>]*>)\n*/i', "\\1\n{$meta}", $output, 1, $count);
if (!$count) {
$page_header .= $meta;
}
// include scripts into header/footer
if (!empty($this->script_files['head'])) {
$page_header .= array_reduce((array) $this->script_files['head'], $merge_script_files);
}
$head = isset($this->scripts['head_top']) ? $this->scripts['head_top'] : '';
$head .= isset($this->scripts['head']) ? $this->scripts['head'] : '';
$page_header .= array_reduce((array) $head, $merge_scripts);
$page_header .= $this->header . "\n";
if (!empty($this->script_files['head_bottom'])) {
$page_header .= array_reduce((array) $this->script_files['head_bottom'], $merge_script_files);
}
if (!empty($this->script_files['foot'])) {
$page_footer .= array_reduce((array) $this->script_files['foot'], $merge_script_files);
}
$page_footer .= $this->footer . "\n";
if (!empty($this->scripts['foot'])) {
$page_footer .= array_reduce((array) $this->scripts['foot'], $merge_scripts);
}
// find page header
if ($hpos = stripos($output, '</head>')) {
$page_header .= "\n";
}
else {
if (!is_numeric($hpos)) {
$hpos = stripos($output, '<body');
}
if (!is_numeric($hpos) && ($hpos = stripos($output, '<html'))) {
while ($output[$hpos] != '>') {
$hpos++;
}
$hpos++;
}
$page_header = "<head>\n$page_header\n</head>\n";
}
// add page header
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', [
'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", ['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 bool $is_contentframe Register this iframe as the 'contentframe' gui object
*
* @return string IFRAME element
*/
public function frame($attrib, $is_contentframe = false)
{
static $idcount = 0;
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmframe' . ++$idcount;
}
$attrib['name'] = $attrib['id'];
$attrib['src'] = !empty($attrib['src']) ? $this->abs_url($attrib['src'], true) : 'about:blank';
// register as 'contentframe' object
if ($is_contentframe || !empty($attrib['contentframe'])) {
$this->set_env('contentframe', !empty($attrib['contentframe']) ? $attrib['contentframe'] : $attrib['name']);
}
return html::iframe($attrib);
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array $attrib Named tag parameters
* @param string $content HTML content of the form
*
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
$hidden = '';
if (!empty($this->env['extwin'])) {
$hiddenfield = new html_hiddenfield(['name' => '_extwin', 'value' => '1']);
$hidden = $hiddenfield->show();
}
else if ($this->framed || !empty($this->env['framed'])) {
$hiddenfield = new html_hiddenfield(['name' => '_framed', 'value' => '1']);
$hidden = $hiddenfield->show();
}
if (!$content) {
$attrib['noclose'] = true;
}
return html::tag('form',
$attrib + ['action' => $this->app->comm_path, 'method' => 'get'],
$hidden . $content,
['id', 'class', 'style', 'name', 'method', 'action', 'enctype', 'onsubmit']
);
}
/**
* Build a form tag with a unique request token
*
* @param array $attrib Named tag parameters including 'action' and 'task' values
* which will be put into hidden fields
* @param string $content Form content
*
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if (!empty($attrib['task'])) {
$hidden->add(['name' => '_task', 'value' => $attrib['task']]);
}
if (!empty($attrib['action'])) {
$hidden->add(['name' => '_action', 'value' => $attrib['action']]);
}
// we already have a <form> tag
if (!empty($attrib['form'])) {
if ($this->framed || !empty($this->env['framed'])) {
$hidden->add(['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;
// already fetched
if (!empty($username)) {
return $username;
}
// Current username is an e-mail address
if (isset($_SESSION['username']) && 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)
&& !empty($_SERVER['QUERY_STRING'])
&& !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 autocomplete attribute
$user_attrib = $autocomplete > 0 ? [] : ['autocomplete' => 'off'];
$host_attrib = $autocomplete > 0 ? [] : ['autocomplete' => 'off'];
$pass_attrib = $autocomplete > 1 ? [] : ['autocomplete' => 'off'];
if ($username_filter && strtolower($username_filter) == 'email') {
$user_attrib['type'] = 'email';
}
$input_task = new html_hiddenfield(['name' => '_task', 'value' => 'login']);
$input_action = new html_hiddenfield(['name' => '_action', 'value' => 'login']);
$input_tzone = new html_hiddenfield(['name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_']);
$input_url = new html_hiddenfield(['name' => '_url', 'id' => 'rcmloginurl', 'value' => $url]);
$input_user = new html_inputfield(['name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required']
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(['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(['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([
'name' => '_host', 'id' => 'rcmloginhost', 'value' => is_numeric($host) ? $default_host[$host] : $host] + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(['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(['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 = ['type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit'];
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// add oauth login button
if ($this->config->get('oauth_auth_uri') && $this->config->get('oauth_provider')) {
// hide login form fields when `oauth_login_redirect` is configured
if ($this->config->get('oauth_login_redirect')) {
$out = '';
}
$link_attr = ['href' => $this->app->url(['action' => 'oauth']), 'id' => 'rcmloginoauth', 'class' => 'button oauth ' . $this->config->get('oauth_provider')];
$out .= html::p('oauthlogin', html::a($link_attr, $this->app->gettext(['name' => 'oauthlogin', 'vars' => ['provider' => $this->config->get('oauth_provider_name', 'OAuth')]])));
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(['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([$this, 'abs_url'], $images);
$images = array_map([$this, 'asset_url'], $images);
if (empty($images) || $_REQUEST['_task'] == 'logout') {
return;
}
$this->add_script('var images = ' . self::json_serialize($images, $this->devel_mode) .';
for (var i=0; i<images.length; i++) {
img = new Image();
img.src = images[i];
}', 'docready');
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
$attrib['class'] = trim((!empty($attrib['class']) ? $attrib['class'] : '') . ' no-bs');
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if (isset($attrib['type']) && $attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
if (empty($attrib['placeholder'])) {
$attrib['placeholder'] = $this->app->gettext('searchplaceholder');
}
$label = html::label(['for' => $attrib['id'], 'class' => 'voice'], rcube::Q($this->app->gettext('arialabelsearchterms')));
$input_q = new html_inputfield($attrib);
$out = $label . $input_q->show();
$name = 'qsearchbox';
// Support for multiple searchforms on the same page
if (isset($attrib['gui-object']) && $attrib['gui-object'] !== false && $attrib['gui-object'] !== 'false') {
$name = $attrib['gui-object'];
}
$this->add_gui_object($name, $attrib['id']);
// add form tag around text field
if (empty($attrib['form']) && empty($attrib['no-form'])) {
$out = $this->form_tag([
'name' => !empty($attrib['form-name']) ? $attrib['form-name'] : 'rcmqsearchform',
'onsubmit' => sprintf(
"%s.command('%s'); return false",
self::JS_OBJECT_NAME,
!empty($attrib['command']) ? $attrib['command'] : 'search'
),
// 'style' => "display:inline"
], $out);
}
if (!empty($attrib['wrapper'])) {
$options_button = '';
$ariatag = !empty($attrib['ariatag']) ? $attrib['ariatag'] : 'h2';
$domain = !empty($attrib['label-domain']) ? $attrib['label-domain'] : null;
$options = !empty($attrib['options']) ? $attrib['options'] : null;
$header_label = $this->app->gettext('arialabel' . $attrib['label'], $domain);
$header_attrs = [
'id' => 'aria-label-' . $attrib['label'],
'class' => 'voice'
];
$header = html::tag($ariatag, $header_attrs, rcube::Q($header_label));
if (!empty($attrib['options'])) {
$options_button = $this->button([
'type' => 'link',
'href' => '#search-filter',
'class' => 'button options',
'label' => 'options',
'title' => 'options',
'tabindex' => '0',
'innerclass' => 'inner',
'data-target' => $options
]);
}
$search_button = $this->button([
'type' => 'link',
'href' => '#search',
'class' => 'button search',
'label' => $attrib['buttontitle'],
'title' => $attrib['buttontitle'],
'tabindex' => '0',
'innerclass' => 'inner',
]);
$reset_button = $this->button([
'type' => 'link',
'command' => !empty($attrib['reset-command']) ? $attrib['reset-command'] : 'reset-search',
'class' => 'button reset',
'label' => 'resetsearch',
'title' => 'resetsearch',
'tabindex' => '0',
'innerclass' => 'inner',
]);
$out = html::div([
'role' => 'search',
'aria-labelledby' => !empty($attrib['label']) ? 'aria-label-' . $attrib['label'] : null,
'class' => $attrib['wrapper'],
],
"$header$out\n$reset_button\n$options_button\n$search_button"
);
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
protected function message_container($attrib)
{
if (isset($attrib['id']) === false) {
$attrib['id'] = 'rcmMessageContainer';
}
$this->add_gui_object('message', $attrib['id']);
return html::div($attrib, '');
}
/**
* GUI object 'charsetselector'
*
* @param array $attrib Named parameters for the select tag
*
* @return string HTML code for the gui object
*/
public function charset_selector($attrib)
{
// pass the following attributes to the form class
$field_attrib = ['name' => '_charset'];
foreach ($attrib as $attr => $value) {
if (in_array($attr, ['id', 'name', 'class', 'style', 'size', 'tabindex'])) {
$field_attrib[$attr] = $value;
}
}
$charsets = [
'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 = [
'about.' . $_SESSION['language'] . '.html',
'about.' . substr($_SESSION['language'], 0, 2) . '.html',
'about.html',
];
foreach ($filenames as $file) {
$fn = RCUBE_CONFIG_DIR . $file;
if (is_readable($fn)) {
$content = file_get_contents($fn);
$content = $this->parse_conditions($content);
$content = $this->parse_xml($content);
break;
}
}
return $content;
}
/**
* Get logo URL for current template based on skin_logo config option
*
* @param string $type Type of the logo to check for (e.g. 'print' or 'small')
* default is null (no special type)
* @param string $match (optional) 'all' = type, template or wildcard, 'template' = type or template
* Note: when type is specified matches are limited to type only unless $match is defined
*
* @return string image URL
*/
protected function get_template_logo($type = null, $match = null)
{
$template_logo = null;
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
$template_names = [
$this->skin_name . ':' . $this->template_name . '[' . $type . ']',
$this->skin_name . ':' . $this->template_name,
$this->skin_name . ':*[' . $type . ']',
$this->skin_name . ':[' . $type . ']',
$this->skin_name . ':*',
'*:' . $this->template_name . '[' . $type . ']',
'*:' . $this->template_name,
'*:*[' . $type . ']',
'*:[' . $type . ']',
$this->template_name . '[' . $type . ']',
$this->template_name,
'*[' . $type . ']',
'[' . $type . ']',
'*',
];
if (empty($type)) {
// If no type provided then remove those options from the list
$template_names = preg_grep("/\]$/", $template_names, PREG_GREP_INVERT);
}
elseif ($match === null) {
// Type specified with no special matching requirements so remove all none type specific options from the list
$template_names = preg_grep("/\]$/", $template_names);
}
if ($match == 'template') {
// Match only specific type or template name
$template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
}
foreach ($template_names as $key) {
if (isset($logo[$key])) {
$template_logo = $logo[$key];
break;
}
}
}
else {
$template_logo = $logo;
}
}
return $template_logo;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 10:43 AM (36 m, 55 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80157
Default Alt Text
(99 KB)
Attached To
Mode
R3 roundcubemail
Attached
Detach File
Event Timeline
Log In to Comment