mirror of
https://github.com/roundcube/roundcubemail.git
synced 2026-03-04 07:14:02 +01:00
aka Persisted Login plugin functionality in core code. Allows admins to set `$config['session_lifetime_extension_days']`, which allows users to switch on an extended session lifetime in the login form. In effect, these user sessions are valid for the configured number of days after the last activity, even across network outages, closed browsers (as long as they keep their cookies), etc.
2752 lines
99 KiB
PHP
2752 lines
99 KiB
PHP
<?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
|
|
*/
|
|
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_extends = [];
|
|
protected $skin_name = '';
|
|
protected $scripts_path = '';
|
|
protected $script_files = [];
|
|
protected $css_files = [];
|
|
protected $scripts = [];
|
|
protected $task;
|
|
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->task = $task;
|
|
$this->init($framed);
|
|
}
|
|
|
|
/**
|
|
* Initialization
|
|
*/
|
|
protected function init($framed = false)
|
|
{
|
|
$this->set_env('task', $this->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', intval($version[0]) * 10000 + intval($version[1]) * 100 + ($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'));
|
|
|
|
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
|
|
*/
|
|
#[\Override]
|
|
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)
|
|
*/
|
|
public function set_assets_path($path)
|
|
{
|
|
// 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));
|
|
}
|
|
|
|
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 (preg_match('/^[a-f0-9]{' . $length . '}$/', $_base[$last])) {
|
|
$path = '../' . $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
$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;
|
|
} elseif (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;
|
|
} elseif ($prod_name = $this->config->get('product_name')) {
|
|
$title = $prod_name . ' :: ' . $title;
|
|
}
|
|
}
|
|
|
|
return $title;
|
|
}
|
|
|
|
/**
|
|
* Getter for the current skin path property
|
|
*/
|
|
#[\Override]
|
|
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);
|
|
|
|
$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)
|
|
// @phpstan-ignore-next-line
|
|
if (!is_string($skin) || str_contains($skin, '/') || str_contains($skin, '\\')) {
|
|
rcube::raise_error('Invalid skin name', true);
|
|
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_name)
|
|
{
|
|
$skin_path = 'skins/' . $skin_name;
|
|
$this->skin_paths[] = $skin_path;
|
|
|
|
// read meta file and check for dependencies
|
|
$meta = $this->get_skin_info($skin_name);
|
|
|
|
$meta['path'] = $skin_path;
|
|
$skin_id = array_last(explode('/', $skin_path));
|
|
|
|
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($meta['extends']);
|
|
$this->skin_extends[] = $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']);
|
|
}
|
|
|
|
if (!empty($this->skin_extends)) {
|
|
$this->set_env('skin_extends', $this->skin_extends);
|
|
}
|
|
|
|
$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 $cmd Method to call
|
|
* @param mixed ...$args Method arguments
|
|
*/
|
|
#[\Override]
|
|
public function command($cmd, ...$args)
|
|
{
|
|
if (str_contains($cmd, 'plugin.')) {
|
|
$this->js_commands[] = ['triggerEvent', $cmd, $args[0]];
|
|
} else {
|
|
array_unshift($args, $cmd);
|
|
|
|
$this->js_commands[] = $args;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a localized label to the client environment
|
|
*
|
|
* @param mixed ...$args Labels (an array of strings, or many string arguments)
|
|
*/
|
|
#[\Override]
|
|
public function add_label(...$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()
|
|
*/
|
|
#[\Override]
|
|
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)
|
|
*/
|
|
#[\Override]
|
|
public function reset($all = false)
|
|
{
|
|
$framed = $this->framed;
|
|
$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 (str_starts_with($file, 'plugins/jqueryui')) {
|
|
$css_files[] = $file;
|
|
}
|
|
}
|
|
|
|
foreach ($this->script_files as $position => $scripts_list) {
|
|
foreach ($scripts_list as $script_attribs) {
|
|
if (str_starts_with($script_attribs['src'], 'plugins/jqueryui')) {
|
|
$script_files[$position][] = $script_attribs;
|
|
}
|
|
}
|
|
}
|
|
|
|
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->init();
|
|
}
|
|
|
|
// 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())
|
|
*/
|
|
#[\Override]
|
|
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)
|
|
*/
|
|
#[\Override]
|
|
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,
|
|
'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
|
|
*
|
|
* @see https://php.net/manual/en/function.exit.php
|
|
*/
|
|
public 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 && !str_contains($skin_path, $this->app->plugins->url)) {
|
|
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,
|
|
'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;
|
|
}
|
|
|
|
$path = false;
|
|
}
|
|
|
|
// read template file
|
|
if (!$path || ($templ = @file_get_contents($path)) === false) {
|
|
rcube::raise_error([
|
|
'code' => 404,
|
|
'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];
|
|
} elseif ($unlock) {
|
|
$top_commands[] = ['hide_message', $unlock];
|
|
}
|
|
|
|
$commands = array_merge($top_commands, $this->js_commands);
|
|
|
|
foreach ($commands as $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
|
|
*/
|
|
#[\Override]
|
|
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 $this->resource_location($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 = [
|
|
'%(?P<name>src|background|data-src-[a-z]+)=(?P<opener>["\']?)(?P<file>[a-z0-9/_.?=-]+)(?P<closer>["\'\s>])%i',
|
|
// fix href attributes in <link>'s only (#9941)
|
|
'%(?P<prefix><link[^>]*)(?P<name>href)=(?P<opener>["\']?)(?P<file>[a-z0-9/_.?=-]+)(?P<closer>["\'\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['file'];
|
|
$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);
|
|
}
|
|
|
|
$file = $this->resource_location($file);
|
|
|
|
return ($matches['prefix'] ?? '') . $matches['name'] . '=' . $matches['opener'] . $file . $matches['closer'];
|
|
}
|
|
|
|
/**
|
|
* 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]);
|
|
$file = $this->resource_location($file);
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Modify resource file location to be passed via the static.php end-point.
|
|
*/
|
|
protected function resource_location($location)
|
|
{
|
|
if (!str_contains($location, '://')) {
|
|
$location = ltrim($location, '/');
|
|
$prefix = '';
|
|
|
|
// FIXME: Would REQUEST_URI be a better option than PATH_INFO?
|
|
if (!empty($_SERVER['PATH_INFO'])) {
|
|
$path = explode('/', trim($_SERVER['PATH_INFO'], '/'));
|
|
$prefix = str_repeat('../', count($path) + 1);
|
|
}
|
|
|
|
if (!str_starts_with($location, $prefix . 'static.php')
|
|
&& !str_starts_with($location, 'data:')
|
|
&& !str_ends_with($location, '.php')
|
|
) {
|
|
$location = $prefix . 'static.php/' . $location;
|
|
}
|
|
}
|
|
|
|
return $location;
|
|
}
|
|
|
|
/**
|
|
* Public wrapper to dip into template parsing.
|
|
*
|
|
* @param string $input Template content
|
|
*
|
|
* @return string
|
|
*/
|
|
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',
|
|
],
|
|
[
|
|
"(\$_SESSION['\\1'] ?? null)",
|
|
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
|
|
"(\$this->env['\\1'] ?? null)",
|
|
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
|
|
"(\$_COOKIE['\\1'] ?? null)",
|
|
"(\$this->browser->{'\\1'} ?? null)",
|
|
"'{$this->template_name}'",
|
|
],
|
|
$expression
|
|
);
|
|
|
|
// Note: We used create_function() before but it's deprecated in PHP 7.2
|
|
// and really it was just a wrapper on eval().
|
|
return eval("return ({$expression});");
|
|
}
|
|
|
|
/**
|
|
* Parse variable strings
|
|
*
|
|
* @param string $type Variable type (env, config etc)
|
|
* @param string $name Variable name
|
|
*
|
|
* @return mixed Variable value
|
|
*/
|
|
protected function parse_variable($type, $name)
|
|
{
|
|
$value = '';
|
|
|
|
switch ($type) {
|
|
case 'env':
|
|
$value = $this->env[$name] ?? 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 = $_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)
|
|
{
|
|
$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 (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
case 'frame':
|
|
return $this->frame($attrib);
|
|
// show a label (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
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']);
|
|
} elseif (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 (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
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 (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
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 (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
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);
|
|
} elseif ($object == 'doctype') {
|
|
$content = html::doctype($attrib['value']);
|
|
} elseif ($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;
|
|
}
|
|
|
|
if (($link = $this->get_template_logo('link')) !== null) {
|
|
$attrib['onclick'] = "location.href='{$link}';";
|
|
$attrib['style'] = 'cursor:pointer;';
|
|
}
|
|
|
|
$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->asset_url($template_logo, true);
|
|
} elseif (!empty($attrib['data-src-' . $type])) {
|
|
$additional_logos[$type] = $this->asset_url($attrib['data-src-' . $type], true);
|
|
}
|
|
}
|
|
|
|
if (!empty($additional_logos)) {
|
|
$this->set_env('additional_logos', $additional_logos);
|
|
}
|
|
|
|
if (!empty($attrib['src'])) {
|
|
$content = html::img($attrib);
|
|
}
|
|
} elseif ($object == 'productname') {
|
|
$name = $this->config->get('product_name', 'Roundcube Webmail');
|
|
$content = html::quote($name);
|
|
} elseif ($object == 'version') {
|
|
$ver = (string) RCMAIL_VERSION;
|
|
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
|
|
if (function_exists('shell_exec')) {
|
|
if (preg_match('/Revision:\s(\d+)/', (string) @shell_exec('svn info'), $regs)) {
|
|
$ver .= ' [SVN r' . $regs[1] . ']';
|
|
}
|
|
} else {
|
|
$ver .= ' [SVN]';
|
|
}
|
|
} elseif (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
|
|
if (function_exists('shell_exec')) {
|
|
if (preg_match('/Date:\s+([^\n]+)/', (string) @shell_exec('git log -1'), $regs)) {
|
|
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
|
|
$ver .= ' [GIT ' . $date . ']';
|
|
}
|
|
}
|
|
} else {
|
|
$ver .= ' [GIT]';
|
|
}
|
|
}
|
|
$content = html::quote($ver);
|
|
} elseif ($object == 'steptitle') {
|
|
$content = html::quote($this->get_pagetitle(false));
|
|
} elseif ($object == 'pagetitle') {
|
|
// Deprecated, <title> will be added automatically
|
|
$content = html::quote($this->get_pagetitle());
|
|
} elseif ($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);
|
|
} elseif ($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
|
|
// - ['key' => 'val']
|
|
// - [string, string]
|
|
// - [[], string]
|
|
// - [['key' => 'val'], ['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;
|
|
} elseif ($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' => (string) $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 (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
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 (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
case 'exp':
|
|
return html::quote($this->eval_expression($attrib['expression']));
|
|
// return variable (<< reindent once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7179 is fixed)
|
|
case 'var':
|
|
$var = explode(':', $attrib['name']);
|
|
$value = $this->parse_variable($var[0], $var[1]);
|
|
|
|
if (is_array($value)) {
|
|
$value = implode(', ', $value);
|
|
}
|
|
|
|
return html::quote((string) $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 (str_starts_with($key, 'data-label-')) {
|
|
// 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, (string) $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;
|
|
}
|
|
} elseif (!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'];
|
|
} elseif (!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';
|
|
}
|
|
|
|
// 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']);
|
|
} elseif (!empty($attrib['task']) && in_array($attrib['task'], rcmail::$main_tasks)) {
|
|
$attrib['href'] = $this->app->url(['action' => $attrib['command'], 'task' => $attrib['task']]);
|
|
} elseif (in_array($attrib['command'], $a_static_commands)) {
|
|
$attrib['href'] = $this->app->url(['action' => $attrib['command']]);
|
|
} elseif (($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'];
|
|
}
|
|
} elseif ($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'];
|
|
} elseif ($attrib['type'] == 'link') {
|
|
$btn_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);
|
|
}
|
|
} elseif ($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 = $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]
|
|
* @param bool $add_path Whether to prepend `scripts_path` to the file path, and to append an mtime-based parameter to it, too
|
|
* @param array $tag_attributes Additional attributes for the script tag
|
|
*/
|
|
public function include_script($file, $position = 'head', $add_path = true, $tag_attributes = [])
|
|
{
|
|
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] = [];
|
|
}
|
|
|
|
$tag_attributes = ['src' => $file] + $tag_attributes;
|
|
// Sort the array so differently ordered keys don't cause a duplicatedly loaded script.
|
|
ksort($tag_attributes);
|
|
|
|
if (!in_array($tag_attributes, $this->script_files[$position])) {
|
|
$this->script_files[$position][] = $tag_attributes;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = static function ($output, $attributes) {
|
|
return $output . html::script($attributes);
|
|
};
|
|
|
|
$merge_scripts = static 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 = (string) 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 = $this->scripts['head_top'] ?? '';
|
|
$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'))) {
|
|
// @phpstan-ignore-next-line
|
|
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) : 'javascript:false;';
|
|
|
|
// 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();
|
|
} elseif ($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
|
|
elseif ($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('imap_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_string('_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;
|
|
|
|
$form_content = [
|
|
'hidden' => [
|
|
'task' => $input_task->show(),
|
|
'action' => $input_action->show(),
|
|
'tzone' => $input_tzone->show(),
|
|
'url' => $input_url->show(),
|
|
],
|
|
'inputs' => [
|
|
'user' => [
|
|
'title' => html::label('rcmloginuser', html::quote($this->app->gettext('username'))),
|
|
'content' => $input_user->show(rcube_utils::get_input_string('_user', rcube_utils::INPUT_GPC)),
|
|
],
|
|
'password' => [
|
|
'title' => html::label('rcmloginpwd', html::quote($this->app->gettext('password'))),
|
|
'content' => $input_pass->show(),
|
|
],
|
|
],
|
|
'buttons' => [],
|
|
];
|
|
|
|
if ($this->config->session_lifetime_extension_days() > 0) {
|
|
$session_lifetime_extension_hidden_field = new html_hiddenfield(['name' => '_session_lifetime_extension', 'value' => '0']);
|
|
$form_content['hidden']['session_lifetime_extension'] = $session_lifetime_extension_hidden_field->show();
|
|
|
|
// Make sure the value is in the range 1..365.
|
|
$session_lifetime_extension_text = str_replace('#', $this->config->session_lifetime_extension_days(), $this->app->gettext('session_lifetime_extension_switch_text'));
|
|
$session_lifetime_extension_checkbox = new html_checkbox(['name' => '_session_lifetime_extension', 'id' => '_session_lifetime_extension', 'title' => $session_lifetime_extension_text]);
|
|
$form_content['inputs']['session_lifetime_extension'] = [
|
|
'content' => html::label(['for' => '_session_lifetime_extension'], [$session_lifetime_extension_checkbox->show(), $session_lifetime_extension_text]),
|
|
];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
} elseif (is_array($default_host) && ($host = key($default_host)) !== null) {
|
|
$val = is_numeric($host) ? $default_host[$host] : $host;
|
|
$input_host = new html_hiddenfield(['name' => '_host', 'id' => 'rcmloginhost', 'value' => $val] + $attrib);
|
|
|
|
$form_content['hidden']['host'] = $input_host->show();
|
|
$input_host = null;
|
|
} elseif (empty($default_host)) {
|
|
$input_host = new html_inputfield(['name' => '_host', 'id' => 'rcmloginhost', 'class' => 'form-control']
|
|
+ $attrib + $host_attrib);
|
|
}
|
|
|
|
// add host selection row
|
|
if (is_object($input_host)) {
|
|
$form_content['inputs']['host'] = [
|
|
'title' => html::label('rcmloginhost', html::quote($this->app->gettext('server'))),
|
|
'content' => $input_host->show(rcube_utils::get_input_string('_host', rcube_utils::INPUT_GPC)),
|
|
];
|
|
}
|
|
|
|
if (rcube_utils::get_boolean($attrib['submit'])) {
|
|
$button_attr = ['type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit'];
|
|
$button = html::tag('button', $button_attr, $this->app->gettext('login'));
|
|
|
|
$form_content['buttons']['submit'] = ['outterclass' => 'formbuttons', 'content' => $button];
|
|
}
|
|
|
|
$data = $this->app->plugins->exec_hook('loginform_content', $form_content);
|
|
|
|
$this->add_gui_object('loginform', $form_name);
|
|
|
|
// output login form contents
|
|
$out = implode('', $data['hidden']);
|
|
|
|
if (count($data['inputs']) > 0) {
|
|
// create HTML table with two cols
|
|
$table = new html_table(['cols' => 2]);
|
|
|
|
foreach ($data['inputs'] as $input) {
|
|
if (isset($input['title'])) {
|
|
$table->add('title', $input['title']);
|
|
$table->add('input', $input['content']);
|
|
} else {
|
|
$table->add(['colspan' => 2, 'class' => 'input'], $input['content']);
|
|
}
|
|
}
|
|
|
|
$out .= $table->show();
|
|
}
|
|
|
|
foreach ($data['buttons'] as $button) {
|
|
$out .= html::p($button['outterclass'], $button['content']);
|
|
}
|
|
|
|
// surround html output with a form tag
|
|
if (empty($attrib['form'])) {
|
|
$out = $this->form_tag(['name' => $form_name, 'method' => 'post'], $out);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* GUI object 'preloader'
|
|
* Loads javascript code for images preloading
|
|
*
|
|
* @param array $attrib Named parameters
|
|
*/
|
|
protected function preloader($attrib): void
|
|
{
|
|
$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) || (isset($_REQUEST['_task']) && $_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 $attrib 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_string('_charset', rcube_utils::INPUT_POST)) {
|
|
$set = $post;
|
|
} elseif (!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|null 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;
|
|
}
|
|
}
|
|
} elseif ($type != 'link') {
|
|
$template_logo = $logo;
|
|
}
|
|
}
|
|
|
|
return $template_logo;
|
|
}
|
|
}
|