Page MenuHomePhorge

No OneTemporary

Size
488 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/program/actions/settings/about.php b/program/actions/settings/about.php
index e156f8704..3296e6139 100644
--- a/program/actions/settings/about.php
+++ b/program/actions/settings/about.php
@@ -1,124 +1,126 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display license information about program and enabled plugins |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_about extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('about'));
$rcmail->output->add_handlers([
'supportlink' => [$this, 'supportlink'],
'pluginlist' => [$this, 'plugins_list'],
'copyright' => function() {
return 'Copyright &copy; 2005-2020, The Roundcube Dev Team';
},
'license' => function() {
return 'This program is free software; you can redistribute it and/or modify it under the terms '
. 'of the <a href="http://www.gnu.org/licenses/gpl.html" target="_blank">GNU General Public License</a> '
. 'as published by the Free Software Foundation, either version 3 of the License, '
. 'or (at your option) any later version.<br/>'
. 'Some <a href="https://roundcube.net/license" target="_blank">exceptions</a> '
. 'for skins &amp; plugins apply.';
},
]);
$rcmail->output->send('about');
}
public static function supportlink($attrib)
{
$rcmail = rcmail::get_instance();
if ($url = $rcmail->config->get('support_url')) {
$label = !empty($attrib['label']) ? $attrib['label'] : 'support';
$attrib['href'] = $url;
return html::a($attrib, $rcmail->gettext($label));
}
}
public static function plugins_list($attrib)
{
$rcmail = rcmail::get_instance();
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmpluginlist';
}
$plugins = array_filter($rcmail->plugins->active_plugins);
$plugin_info = [];
foreach ($plugins as $name) {
if ($info = $rcmail->plugins->get_info($name)) {
$plugin_info[$name] = $info;
}
}
// load info from required plugins, too
foreach ($plugin_info as $name => $info) {
if (!empty($info['require']) && is_array($info['require'])) {
foreach ($info['require'] as $req_name) {
if (!isset($plugin_info[$req_name]) && ($req_info = $rcmail->plugins->get_info($req_name))) {
$plugin_info[$req_name] = $req_info;
}
}
}
}
if (empty($plugin_info)) {
return '';
}
ksort($plugin_info, SORT_LOCALE_STRING);
$table = new html_table($attrib);
// add table header
$table->add_header('name', $rcmail->gettext('plugin'));
$table->add_header('version', $rcmail->gettext('version'));
$table->add_header('license', $rcmail->gettext('license'));
$table->add_header('source', $rcmail->gettext('source'));
foreach ($plugin_info as $name => $data) {
- $uri = !empty($data['src_uri']) ? $data['src_uri'] : $data['uri'];
+ $uri = !empty($data['src_uri']) ? $data['src_uri'] : (isset($data['uri']) ? $data['uri'] : '');
if ($uri && stripos($uri, 'http') !== 0) {
$uri = 'http://' . $uri;
}
$table->add_row();
$table->add('name', rcube::Q(!empty($data['name']) ? $data['name'] : $name));
$table->add('version', !empty($data['version']) ? rcube::Q($data['version']) : '');
$table->add('license', !empty($data['license_uri']) ? html::a(['target' => '_blank', 'href' => rcube::Q($data['license_uri'])],
rcube::Q($data['license'])) : $data['license']);
$table->add('source', $uri ? html::a(['target' => '_blank', 'href' => rcube::Q($uri)],
rcube::Q($rcmail->gettext('download'))) : '');
}
return $table->show();
}
}
diff --git a/program/actions/settings/folder_delete.php b/program/actions/settings/folder_delete.php
index 53d95ae13..27fd36a05 100644
--- a/program/actions/settings/folder_delete.php
+++ b/program/actions/settings/folder_delete.php
@@ -1,66 +1,66 @@
<?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: |
| Provide functionality to delete a folder |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_delete extends rcmail_action
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$plugin = $rcmail->plugins->exec_hook('folder_delete', ['name' => $mbox]);
if (empty($plugin['abort'])) {
$deleted = $storage->delete_folder($plugin['name']);
}
else {
$deleted = $plugin['result'];
}
// #1488692: update session
if ($deleted && isset($_SESSION['mbox']) && $_SESSION['mbox'] === $mbox) {
$rcmail->session->remove('mbox');
}
}
if (!empty($deleted)) {
// Remove folder and subfolders rows
$rcmail->output->command('remove_folder_row', $mbox);
$rcmail->output->show_message('folderdeleted', 'confirmation');
// Clear content frame
$rcmail->output->command('subscription_select');
$rcmail->output->command('set_quota', self::quota_content());
}
else {
self::display_server_error('errorsaving');
}
$rcmail->output->send();
}
}
diff --git a/program/actions/settings/folder_edit.php b/program/actions/settings/folder_edit.php
index e0ab32814..8d01d0045 100644
--- a/program/actions/settings/folder_edit.php
+++ b/program/actions/settings/folder_edit.php
@@ -1,344 +1,345 @@
<?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: |
| Provide functionality to edit a folder |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_edit extends rcmail_action_settings_folders
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->add_handlers([
'folderdetails' => [$this, 'folder_form'],
]);
$rcmail->output->add_label('nonamewarning');
$rcmail->output->send('folderedit');
}
-
public static function folder_form($attrib)
{
// WARNING: folder names in UI are encoded with RCUBE_CHARSET
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
// edited folder name (empty in create-folder mode)
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true);
// predefined path for new folder
$parent = rcube_utils::get_input_value('_path', rcube_utils::INPUT_GPC, true);
$threading_supported = $storage->get_capability('THREAD');
$dual_use_supported = $storage->get_capability(rcube_storage::DUAL_USE_FOLDERS);
$delimiter = $storage->get_hierarchy_delimiter();
// Get mailbox parameters
if (strlen($mbox)) {
$options = self::folder_options($mbox);
$namespace = $storage->get_namespace();
$path = explode($delimiter, $mbox);
$folder = array_pop($path);
$path = implode($delimiter, $path);
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$hidden_fields = ['name' => '_mbox', 'value' => $mbox];
}
else {
$options = [];
$path = $parent;
$folder = '';
$hidden_fields = [];
// allow creating subfolders of INBOX folder
if ($path == 'INBOX') {
$path = $storage->mod_folder($path, 'in');
}
}
// remove personal namespace prefix
$path_id = null;
if (strlen($path)) {
$path_id = $path;
$path = $storage->mod_folder($path . $delimiter);
if ($path[strlen($path)-1] == $delimiter) {
$path = substr($path, 0, -1);
}
}
$form = [];
// General tab
$form['props'] = [
'name' => $rcmail->gettext('properties'),
];
// Location (name)
- if ($options['protected']) {
+ if (!empty($options['protected'])) {
$foldername = str_replace($delimiter, ' &raquo; ', rcube::Q(self::localize_foldername($mbox, false, true)));
}
- else if ($options['norename']) {
+ else if (!empty($options['norename'])) {
$foldername = rcube::Q($folder);
}
else {
if (isset($_POST['_name'])) {
$folder = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true));
}
$foldername = new html_inputfield(['name' => '_name', 'id' => '_name', 'size' => 30, 'class' => 'form-control']);
$foldername = '<span class="input-group">' . $foldername->show($folder);
- if ($options['special'] && ($sname = self::localize_foldername($mbox, false, true)) != $folder) {
+ if (!empty($options['special']) && ($sname = self::localize_foldername($mbox, false, true)) != $folder) {
$foldername .= ' <span class="input-group-append"><span class="input-group-text">(' . rcube::Q($sname) .')</span></span>';
}
$foldername .= '</span>';
}
$form['props']['fieldsets']['location'] = [
'name' => $rcmail->gettext('location'),
'content' => [
'name' => [
'label' => $rcmail->gettext('foldername'),
'value' => $foldername,
],
],
];
- if (!empty($options) && ($options['norename'] || $options['protected'])) {
+ if (!empty($options) && (!empty($options['norename']) || !empty($options['protected']))) {
// prevent user from moving folder
$hidden_path = new html_hiddenfield(['name' => '_parent', 'value' => $path]);
$form['props']['fieldsets']['location']['content']['name']['value'] .= $hidden_path->show();
}
else {
$selected = isset($_POST['_parent']) ? $_POST['_parent'] : $path_id;
$exceptions = [$mbox];
// Exclude 'prefix' namespace from parent folders list (#1488349)
// If INBOX. namespace exists, folders created as INBOX subfolders
// will be listed at the same level - selecting INBOX as a parent does nothing
if ($prefix = $storage->get_namespace('prefix')) {
$exceptions[] = substr($prefix, 0, -1);
}
$select = self::folder_selector([
'id' => '_parent',
'name' => '_parent',
'noselection' => '---',
'maxlength' => 150,
'unsubscribed' => true,
'skip_noinferiors' => true,
'exceptions' => $exceptions,
'additional' => strlen($selected) ? [$selected] : null,
]);
$form['props']['fieldsets']['location']['content']['parent'] = [
'label' => $rcmail->gettext('parentfolder'),
'value' => $select->show($selected),
];
}
// Settings
$form['props']['fieldsets']['settings'] = [
'name' => $rcmail->gettext('settings'),
];
// For servers that do not support both sub-folders and messages in a folder
if (!$dual_use_supported) {
if (!strlen($mbox)) {
$select = new html_select(['name' => '_type', 'id' => '_type']);
$select->add($rcmail->gettext('dualusemail'), 'mail');
$select->add($rcmail->gettext('dualusefolder'), 'folder');
$value = rcube_utils::get_input_value('_type', rcube_utils::INPUT_POST);
$value = $select->show($value ?: 'mail');
}
else {
$value = $options['noselect'] ? 'folder' : 'mail';
$value = $rcmail->gettext('dualuse' . $value);
}
$form['props']['fieldsets']['settings']['content']['type'] = [
'label' => $rcmail->gettext('dualuselabel'),
'value' => $value,
];
}
// Settings: threading
- if ($threading_supported && ($mbox == 'INBOX' || (!$options['noselect'] && !$options['is_root']))) {
+ if ($threading_supported && ($mbox == 'INBOX' || (empty($options['noselect']) && empty($options['is_root'])))) {
$value = 0;
$select = new html_select(['name' => '_viewmode', 'id' => '_viewmode']);
$select->add($rcmail->gettext('list'), 0);
$select->add($rcmail->gettext('threads'), 1);
if (isset($_POST['_viewmode'])) {
$value = (int) $_POST['_viewmode'];
}
else if (strlen($mbox)) {
$a_threaded = $rcmail->config->get('message_threading', []);
$default_mode = $rcmail->config->get('default_list_mode', 'list');
$value = (int) (isset($a_threaded[$mbox]) ? $a_threaded[$mbox] : $default_mode == 'threads');
}
$form['props']['fieldsets']['settings']['content']['viewmode'] = [
'label' => $rcmail->gettext('listmode'),
'value' => $select->show($value),
];
}
// Information (count, size) - Edit mode
if (strlen($mbox)) {
// Number of messages
$form['props']['fieldsets']['info'] = [
'name' => $rcmail->gettext('info'),
'content' => []
];
if ((!$options['noselect'] && !$options['is_root']) || $mbox == 'INBOX') {
$msgcount = $storage->count($mbox, 'ALL', true, false);
if ($msgcount) {
// Get the size on servers with supposed-to-be-fast method for that
if ($storage->get_capability('STATUS=SIZE')) {
$size = $storage->folder_size($mbox);
if ($size !== false) {
$size = self::show_bytes($size);
}
}
// create link with folder-size command
if (!isset($size) || $size === false) {
$onclick = sprintf("return %s.command('folder-size', '%s', this)",
rcmail_output::JS_OBJECT_NAME, rcube::JQ($mbox));
$attr = ['href' => '#', 'onclick' => $onclick, 'id' => 'folder-size'];
$size = html::a($attr, $rcmail->gettext('getfoldersize'));
}
}
else {
// no messages -> zero size
$size = 0;
}
$form['props']['fieldsets']['info']['content']['count'] = [
'label' => $rcmail->gettext('messagecount'),
'value' => (int) $msgcount
];
$form['props']['fieldsets']['info']['content']['size'] = [
'label' => $rcmail->gettext('size'),
'value' => $size,
];
}
// show folder type only if we have non-private namespaces
if (!empty($namespace['shared']) || !empty($namespace['others'])) {
$form['props']['fieldsets']['info']['content']['foldertype'] = [
'label' => $rcmail->gettext('foldertype'),
'value' => $rcmail->gettext($options['namespace'] . 'folder')
];
}
}
// Allow plugins to modify folder form content
$plugin = $rcmail->plugins->exec_hook('folder_form', [
'form' => $form,
'options' => $options,
'name' => $mbox,
'parent_name' => $parent
]);
$form = $plugin['form'];
// Set form tags and hidden fields
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-folder', null, $hidden_fields);
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
// Create form output
foreach ($form as $idx => $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = self::get_form_part($fieldset, $attrib);
if ($subcontent) {
$subcontent = html::tag('legend', null, rcube::Q($fieldset['name'])) . $subcontent;
$content .= html::tag('fieldset', null, $subcontent) ."\n";
}
}
}
else {
$content = self::get_form_part($tab, $attrib);
}
if ($idx != 'props') {
$out .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) ."\n";
}
else {
$out .= $content ."\n";
}
}
$out .= "\n$form_end";
$rcmail->output->set_env('messagecount', isset($msgcount) ? (int) $msgcount : 0);
$rcmail->output->set_env('folder', $mbox);
if ($mbox !== null && empty($_POST)) {
$rcmail->output->command('parent.set_quota', self::quota_content(null, $mbox));
}
return $out;
}
public static function get_form_part($form, $attrib = [])
{
$rcmail = rcmail::get_instance();
$content = '';
if (!empty($form['content']) && is_array($form['content'])) {
$table = new html_table(['cols' => 2]);
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_' . $col;
$label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext($col);
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show($attrib);
}
else {
$content = $form['content'];
}
return $content;
}
}
diff --git a/program/actions/settings/folder_purge.php b/program/actions/settings/folder_purge.php
index d7902e4c4..8a1994ff1 100644
--- a/program/actions/settings/folder_purge.php
+++ b/program/actions/settings/folder_purge.php
@@ -1,70 +1,70 @@
<?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: |
| Provide functionality of folder purge |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_purge extends rcmail_action
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
$delimiter = $storage->get_hierarchy_delimiter();
$trash_mbox = $rcmail->config->get('trash_mbox');
$trash_regexp = '/^' . preg_quote($trash_mbox . $delimiter, '/') . '/';
// we should only be purging trash (or their subfolders)
if (!strlen($trash_mbox) || $mbox === $trash_mbox || preg_match($trash_regexp, $mbox)) {
$success = $storage->delete_message('*', $mbox);
- $delete = true;
+ $delete = true;
}
// move to Trash
else {
$success = $storage->move_message('1:*', $trash_mbox, $mbox);
- $delete = false;
+ $delete = false;
}
- if ($success) {
+ if (!empty($success)) {
$rcmail->output->set_env('messagecount', 0);
if ($delete) {
$rcmail->output->show_message('folderpurged', 'confirmation');
$rcmail->output->command('set_quota', self::quota_content(null, $mbox));
}
else {
$rcmail->output->show_message('messagemoved', 'confirmation');
}
$_SESSION['unseen_count'][$mbox] = 0;
$rcmail->output->command('show_folder', $mbox, null, true);
}
else {
self::display_server_error('errorsaving');
}
$rcmail->output->send();
}
}
diff --git a/program/actions/settings/folder_rename.php b/program/actions/settings/folder_rename.php
index 100c8390b..bad99270d 100644
--- a/program/actions/settings/folder_rename.php
+++ b/program/actions/settings/folder_rename.php
@@ -1,94 +1,94 @@
<?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: |
| Provide functionality of folder rename |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_rename extends rcmail_action_settings_folders
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$name = trim(rcube_utils::get_input_value('_folder_newname', rcube_utils::INPUT_POST, true));
$oldname = rcube_utils::get_input_value('_folder_oldname', rcube_utils::INPUT_POST, true);
if (strlen($name) && strlen($oldname)) {
$rename = self::rename_folder($oldname, $name);
}
if (!empty($rename)) {
self::update_folder_row($name, $oldname);
}
else {
self::display_server_error('errorsaving');
}
$rcmail->output->send();
}
public static function rename_folder($oldname, $newname)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$plugin = $rcmail->plugins->exec_hook('folder_rename', [
'oldname' => $oldname, 'newname' => $newname]);
if (empty($plugin['abort'])) {
$renamed = $storage->rename_folder($oldname, $newname);
}
else {
$renamed = $plugin['result'];
}
// update per-folder options for modified folder and its subfolders
if ($renamed) {
$delimiter = $storage->get_hierarchy_delimiter();
$a_threaded = (array) $rcmail->config->get('message_threading', []);
$oldprefix = '/^' . preg_quote($oldname . $delimiter, '/') . '/';
foreach ($a_threaded as $key => $val) {
if ($key == $oldname) {
unset($a_threaded[$key]);
$a_threaded[$newname] = $val;
}
else if (preg_match($oldprefix, $key)) {
unset($a_threaded[$key]);
$a_threaded[preg_replace($oldprefix, $newname . $delimiter, $key)] = $val;
}
}
$rcmail->user->save_prefs(['message_threading' => $a_threaded]);
// #1488692: update session
if (isset($_SESSION['mbox']) && $_SESSION['mbox'] === $oldname) {
$_SESSION['mbox'] = $newname;
}
return true;
}
return false;
}
}
diff --git a/program/actions/settings/folder_save.php b/program/actions/settings/folder_save.php
index fe101e506..06ad5adaa 100644
--- a/program/actions/settings/folder_save.php
+++ b/program/actions/settings/folder_save.php
@@ -1,228 +1,233 @@
<?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: |
| Handler for saving the create/edit folder form |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_save extends rcmail_action_settings_folder_edit
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
// WARNING: folder names in UI are encoded with RCUBE_CHARSET
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true));
$path = rcube_utils::get_input_value('_parent', rcube_utils::INPUT_POST, true);
$old_imap = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
$type = rcube_utils::get_input_value('_type', rcube_utils::INPUT_POST);
$name_imap = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP');
// $path is in UTF7-IMAP already
// init IMAP connection
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$options = strlen($old_imap) ? self::folder_options($old_imap) : [];
// Folder name checks
- if ($options['protected'] || $options['norename']) {
+ if (!empty($options['protected']) || !empty($options['norename'])) {
// do nothing
}
else if (!strlen($name)) {
$error = $rcmail->gettext('namecannotbeempty');
}
else if (mb_strlen($name) > 128) {
$error = $rcmail->gettext('nametoolong');
}
else if ($name[0] == '.' && $rcmail->config->get('imap_skip_hidden_folders')) {
$error = $rcmail->gettext('namedotforbidden');
}
- else if (!$storage->folder_validate($name, $char)) {
+ else if (!$storage->folder_validate($name, $char = null)) {
$error = $rcmail->gettext('forbiddencharacter') . " ($char)";
}
if (!empty($error)) {
$rcmail->output->command('display_message', $error, 'error');
}
else {
- if ($options['protected'] || $options['norename']) {
+ if (!empty($options['protected']) || !empty($options['norename'])) {
$name_imap = $old_imap;
}
else if (strlen($path)) {
$name_imap = $path . $delimiter . $name_imap;
}
else {
$name_imap = $storage->mod_folder($name_imap, 'in');
}
}
$dual_use_supported = $storage->get_capability(rcube_storage::DUAL_USE_FOLDERS);
$acl_supported = $storage->get_capability('ACL');
// Check access rights to the parent folder
if (empty($error) && $acl_supported && strlen($path) && (!strlen($old_imap) || $old_imap != $name_imap)) {
$parent_opts = $storage->folder_info($path);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
$error = $rcmail->gettext('parentnotwritable');
}
}
if (!empty($error)) {
$rcmail->output->command('display_message', $error, 'error');
$folder = null;
}
else {
- $folder['name'] = $name_imap;
- $folder['oldname'] = $old_imap;
- $folder['class'] = '';
- $folder['options'] = $options;
- $folder['settings'] = [
- // List view mode: 0-list, 1-threads
- 'view_mode' => (int) rcube_utils::get_input_value('_viewmode', rcube_utils::INPUT_POST),
- 'sort_column' => rcube_utils::get_input_value('_sortcol', rcube_utils::INPUT_POST),
- 'sort_order' => rcube_utils::get_input_value('_sortord', rcube_utils::INPUT_POST),
+ $folder = [
+ 'name' => $name_imap,
+ 'oldname' => $old_imap,
+ 'class' => '',
+ 'options' => $options,
+ 'settings' => [
+ // List view mode: 0-list, 1-threads
+ 'view_mode' => (int) rcube_utils::get_input_value('_viewmode', rcube_utils::INPUT_POST),
+ 'sort_column' => rcube_utils::get_input_value('_sortcol', rcube_utils::INPUT_POST),
+ 'sort_order' => rcube_utils::get_input_value('_sortord', rcube_utils::INPUT_POST),
+ ]
];
}
// create a new mailbox
if (empty($error) && !strlen($old_imap)) {
$folder['subscribe'] = true;
+ $folder['noselect'] = false;
// Server does not support both sub-folders and messages in a folder
// For folders that are supposed to contain other folders we will:
// - disable subscribtion
// - add a separator at the end to make them \NoSelect
if (!$dual_use_supported && $type == 'folder') {
$folder['subscribe'] = false;
$folder['noselect'] = true;
}
$plugin = $rcmail->plugins->exec_hook('folder_create', ['record' => $folder]);
$folder = $plugin['record'];
if (!$plugin['abort']) {
$created = $storage->create_folder($folder['name'], $folder['subscribe'], null, $folder['noselect']);
}
else {
$created = $plugin['result'];
}
if ($created) {
// Save folder settings
if (isset($_POST['_viewmode'])) {
$a_threaded = (array) $rcmail->config->get('message_threading', []);
$a_threaded[$folder['name']] = (bool) $_POST['_viewmode'];
$rcmail->user->save_prefs(['message_threading' => $a_threaded]);
}
self::update_folder_row($folder['name'], null, $folder['subscribe'], $folder['class']);
$rcmail->output->show_message('foldercreated', 'confirmation');
// reset folder preview frame
$rcmail->output->command('subscription_select');
$rcmail->output->send('iframe');
}
else {
// show error message
if (!empty($plugin['message'])) {
$rcmail->output->show_message($plugin['message'], 'error', null, false);
}
else {
self::display_server_error('errorsaving');
}
}
}
// update a mailbox
else if (empty($error)) {
$plugin = $rcmail->plugins->exec_hook('folder_update', ['record' => $folder]);
$folder = $plugin['record'];
- $rename = ($folder['oldname'] != $folder['name']);
+ $rename = $folder['oldname'] != $folder['name'];
if (!$plugin['abort']) {
if ($rename) {
$updated = $storage->rename_folder($folder['oldname'], $folder['name']);
}
else {
$updated = true;
}
}
else {
$updated = $plugin['result'];
}
if ($updated) {
// Update folder settings,
if (isset($_POST['_viewmode'])) {
$a_threaded = (array) $rcmail->config->get('message_threading', []);
- // In case of name change update names of childrens in settings
+ // In case of name change update names of children in settings
if ($rename) {
$oldprefix = '/^' . preg_quote($folder['oldname'] . $delimiter, '/') . '/';
foreach ($a_threaded as $key => $val) {
if ($key == $folder['oldname']) {
unset($a_threaded[$key]);
}
else if (preg_match($oldprefix, $key)) {
unset($a_threaded[$key]);
$a_threaded[preg_replace($oldprefix, $folder['name'].$delimiter, $key)] = $val;
}
}
}
$a_threaded[$folder['name']] = (bool) $_POST['_viewmode'];
$rcmail->user->save_prefs(['message_threading' => $a_threaded]);
}
$rcmail->output->show_message('folderupdated', 'confirmation');
$rcmail->output->set_env('folder', $folder['name']);
if ($rename) {
// #1488692: update session
- if ($_SESSION['mbox'] === $folder['oldname']) {
+ if (isset($_SESSION['mbox']) && $_SESSION['mbox'] === $folder['oldname']) {
$_SESSION['mbox'] = $folder['name'];
}
self::update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']);
$rcmail->output->send('iframe');
}
else if (!empty($folder['class'])) {
self::update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']);
}
}
else {
// show error message
if (!empty($plugin['message'])) {
$rcmail->output->show_message($plugin['message'], 'error', null, false);
}
else {
self::display_server_error('errorsaving');
}
}
}
$rcmail->overwrite_action('edit-folder');
}
}
diff --git a/program/actions/settings/folder_size.php b/program/actions/settings/folder_size.php
index 1eaa5be1b..cfb589d3f 100644
--- a/program/actions/settings/folder_size.php
+++ b/program/actions/settings/folder_size.php
@@ -1,49 +1,49 @@
<?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: |
| Provide functionality of getting folder size |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_size extends rcmail_action
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$name = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
$size = $storage->folder_size($name);
// @TODO: check quota and show percentage usage of specified mailbox?
if ($size !== false) {
$rcmail->output->command('folder_size_update', self::show_bytes($size));
}
else {
self::display_server_error();
}
$rcmail->output->send();
}
}
diff --git a/program/actions/settings/folder_subscribe.php b/program/actions/settings/folder_subscribe.php
index b95e28e0f..86084ec07 100644
--- a/program/actions/settings/folder_subscribe.php
+++ b/program/actions/settings/folder_subscribe.php
@@ -1,67 +1,67 @@
<?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: |
| Handler for folder subscribe action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_subscribe extends rcmail_action
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$result = $storage->subscribe([$mbox]);
// Handle virtual (non-existing) folders
if (
!$result
&& $storage->get_error_code() == -1
&& $storage->get_response_code() == rcube_storage::TRYCREATE
) {
$result = $storage->create_folder($mbox, true);
if ($result) {
// @TODO: remove 'virtual' class of folder's row
}
}
+ }
- if ($result) {
- // Handle subscription of protected folder (#1487656)
- if ($rcmail->config->get('protect_default_folders') && $storage->is_special_folder($mbox)) {
- $rcmail->output->command('disable_subscription', $mbox);
- }
-
- $rcmail->output->show_message('foldersubscribed', 'confirmation');
- }
- else {
- self::display_server_error('errorsaving');
- $rcmail->output->command('reset_subscription', $mbox, false);
+ if (!empty($result)) {
+ // Handle subscription of protected folder (#1487656)
+ if ($rcmail->config->get('protect_default_folders') && $storage->is_special_folder($mbox)) {
+ $rcmail->output->command('disable_subscription', $mbox);
}
+
+ $rcmail->output->show_message('foldersubscribed', 'confirmation');
+ }
+ else {
+ self::display_server_error('errorsaving');
+ $rcmail->output->command('reset_subscription', $mbox, false);
}
$rcmail->output->send();
}
}
diff --git a/program/actions/settings/folder_unsubscribe.php b/program/actions/settings/folder_unsubscribe.php
index ae63d2331..4e927d7c3 100644
--- a/program/actions/settings/folder_unsubscribe.php
+++ b/program/actions/settings/folder_unsubscribe.php
@@ -1,50 +1,50 @@
<?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: |
| Handler for folder unsubscribe action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_unsubscribe extends rcmail_action
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$result = $storage->unsubscribe([$mbox]);
+ }
- if ($result) {
- $rcmail->output->show_message('folderunsubscribed', 'confirmation');
- }
- else {
- self::display_server_error('errorsaving');
- $rcmail->output->command('reset_subscription', $mbox, true);
- }
+ if (!empty($result)) {
+ $rcmail->output->show_message('folderunsubscribed', 'confirmation');
+ }
+ else {
+ self::display_server_error('errorsaving');
+ $rcmail->output->command('reset_subscription', $mbox, true);
}
$rcmail->output->send();
}
}
diff --git a/program/actions/settings/folders.php b/program/actions/settings/folders.php
index 068de75bc..32a6a77a4 100644
--- a/program/actions/settings/folders.php
+++ b/program/actions/settings/folders.php
@@ -1,366 +1,378 @@
<?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: |
| Provide functionality of folders listing |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folders extends rcmail_action_settings_index
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$rcmail->output->set_pagetitle($rcmail->gettext('folders'));
$rcmail->output->set_env('prefix_ns', $storage->get_namespace('prefix'));
$rcmail->output->set_env('quota', (bool) $storage->get_capability('QUOTA'));
$rcmail->output->include_script('treelist.js');
// add some labels to client
$rcmail->output->add_label('deletefolderconfirm', 'purgefolderconfirm', 'movefolderconfirm',
'folderdeleting', 'foldermoving', 'foldersubscribing', 'folderunsubscribing',
'move', 'quota');
// register UI objects
$rcmail->output->add_handlers([
'foldersubscription' => [$this, 'folder_subscriptions'],
'folderfilter' => [$this, 'folder_filter'],
'quotadisplay' => [$rcmail, 'quota_display'],
'searchform' => [$rcmail->output, 'search_form'],
]);
$rcmail->output->send('folders');
}
// build table with all folders listed by server
public static function folder_subscriptions($attrib)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmSubscriptionlist';
}
// get folders from server
$storage->clear_cache('mailboxes', true);
$a_unsubscribed = $storage->list_folders();
$a_subscribed = $storage->list_folders_subscribed('', '*', null, null, true); // unsorted
$delimiter = $storage->get_hierarchy_delimiter();
$namespace = $storage->get_namespace();
$special_folders = array_flip(array_merge(['inbox' => 'INBOX'], $storage->get_special_folders()));
$protect_default = $rcmail->config->get('protect_default_folders');
$seen = [];
$list_folders = [];
// pre-process folders list
foreach ($a_unsubscribed as $i => $folder) {
$folder_id = $folder;
$folder = $storage->mod_folder($folder);
$foldersplit = explode($delimiter, $folder);
$name = rcube_charset::convert(array_pop($foldersplit), 'UTF7-IMAP');
$is_special = isset($special_folders[$folder_id]);
$parent_folder = $is_special ? '' : join($delimiter, $foldersplit);
$level = $is_special ? 0 : count($foldersplit);
// add any necessary "virtual" parent folders
if ($parent_folder && !isset($seen[$parent_folder])) {
for ($i = 1; $i <= $level; $i++) {
$ancestor_folder = join($delimiter, array_slice($foldersplit, 0, $i));
if ($ancestor_folder && !$seen[$ancestor_folder]++) {
$ancestor_name = rcube_charset::convert($foldersplit[$i-1], 'UTF7-IMAP');
$list_folders[] = [
'id' => $ancestor_folder,
'name' => $ancestor_name,
'level' => $i-1,
'virtual' => true,
];
}
}
}
// Handle properly INBOX.INBOX situation
if (isset($seen[$folder])) {
continue;
}
- $seen[$folder]++;
+ if (isset($seen[$folder])) {
+ $seen[$folder]++;
+ }
+ else {
+ $seen[$folder] = 1;
+ }
$list_folders[] = [
'id' => $folder_id,
'name' => $name,
'level' => $level,
];
}
unset($seen);
$checkbox_subscribe = new html_checkbox([
'name' => '_subscribed[]',
'title' => $rcmail->gettext('changesubscription'),
'onclick' => rcmail_output::JS_OBJECT_NAME.".command(this.checked?'subscribe':'unsubscribe',this.value)",
]);
$js_folders = [];
$folders = [];
$collapsed = (string) $rcmail->config->get('collapsed_folders');
// create list of available folders
foreach ($list_folders as $i => $folder) {
$sub_key = array_search($folder['id'], $a_subscribed);
$is_subscribed = $sub_key !== false;
$is_special = isset($special_folders[$folder['id']]);
$is_protected = $folder['id'] == 'INBOX' || ($protect_default && $is_special);
$noselect = false;
$classes = [];
$folder_utf8 = rcube_charset::convert($folder['id'], 'UTF7-IMAP');
$display_folder = rcube::Q($is_special ? self::localize_foldername($folder['id'], false, true) : $folder['name']);
- if ($folder['virtual']) {
+ if (!empty($folder['virtual'])) {
$classes[] = 'virtual';
}
// Check \Noselect flag (of existing folder)
if (!$is_protected && in_array($folder['id'], $a_unsubscribed)) {
$attrs = $storage->folder_attributes($folder['id']);
$noselect = in_array_nocase('\\Noselect', $attrs);
}
$is_disabled = (($is_protected && $is_subscribed) || $noselect);
// Below we will disable subscription option for "virtual" folders
// according to namespaces, but only if they aren't already subscribed.
// User should be able to unsubscribe from the folder
// even if it doesn't exists or is not accessible (OTRS:1000059)
if (!$is_subscribed && !$is_disabled && !empty($namespace) && $folder['virtual']) {
// check if the folder is a namespace prefix, then disable subscription option on it
if (!$is_disabled && $folder['level'] == 0) {
$fname = $folder['id'] . $delimiter;
foreach ($namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $item) {
if ($item[0] === $fname) {
$is_disabled = true;
break 2;
}
}
}
}
}
// check if the folder is an other users virtual-root folder, then disable subscription option on it
if (!$is_disabled && $folder['level'] == 1 && !empty($namespace['other'])) {
$parts = explode($delimiter, $folder['id']);
$fname = $parts[0] . $delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $fname) {
$is_disabled = true;
break;
}
}
}
// check if the folder is shared, then disable subscription option on it (if not subscribed already)
if (!$is_disabled) {
$tmp_ns = array_merge((array)$namespace['other'], (array)$namespace['shared']);
foreach ($tmp_ns as $item) {
if (strlen($item[0]) && strpos($folder['id'], $item[0]) === 0) {
$is_disabled = true;
break;
}
}
}
}
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$folder_id = rcube_utils::html_identifier($folder['id'], true);
if ($folder_class = self::folder_classname($folder['id'])) {
$classes[] = $folder_class;
}
$folders[$folder['id']] = [
'idx' => $folder_id,
'folder_imap' => $folder['id'],
'folder' => $folder_utf8,
'display' => $display_folder,
- 'protected' => $is_protected || $folder['virtual'],
+ 'protected' => $is_protected || !empty($folder['virtual']),
'class' => join(' ', $classes),
'subscribed' => $is_subscribed,
'level' => $folder['level'],
'collapsed' => $is_collapsed,
'content' => html::a(['href' => '#'], $display_folder)
. $checkbox_subscribe->show(($is_subscribed ? $folder['id'] : ''),
['value' => $folder['id'], 'disabled' => $is_disabled ? 'disabled' : ''])
];
}
$plugin = $rcmail->plugins->exec_hook('folders_list', ['list' => $folders]);
// add drop-target representing 'root'
$root = [
'idx' => rcube_utils::html_identifier('*', true),
'folder_imap' => '*',
'folder' => '',
'display' => '',
'protected' => true,
'class' => 'root',
'content' => '<span>&nbsp;</span>',
];
$folders = [];
$plugin['list'] = array_values($plugin['list']);
array_unshift($plugin['list'], $root);
for ($i = 0, $length = count($plugin['list']); $i<$length; $i++) {
$folders[] = self::folder_tree_element($plugin['list'], $i, $js_folders);
}
$rcmail->output->add_gui_object('subscriptionlist', $attrib['id']);
$rcmail->output->set_env('subscriptionrows', $js_folders);
$rcmail->output->set_env('defaultfolders', array_keys($special_folders));
$rcmail->output->set_env('collapsed_folders', $collapsed);
$rcmail->output->set_env('delimiter', $delimiter);
return html::tag('ul', $attrib, implode('', $folders), html::$common_attrib);
}
public static function folder_tree_element($folders, &$key, &$js_folders)
{
$data = $folders[$key];
$idx = 'rcmli' . $data['idx'];
$js_folders[$data['folder_imap']] = [$data['folder'], $data['display'], $data['protected']];
$content = $data['content'];
$attribs = [
'id' => $idx,
'class' => trim($data['class'] . ' mailbox')
];
$children = [];
- while ($folders[$key+1] && $folders[$key+1]['level'] > $data['level']) {
+ while (
+ isset($folders[$key+1]['level'])
+ && (!isset($data['level']) || $folders[$key+1]['level'] > $data['level'])
+ ) {
$key++;
$children[] = self::folder_tree_element($folders, $key, $js_folders);
}
if (!empty($children)) {
- $content .= html::div('treetoggle ' . ($data['collapsed'] ? 'collapsed' : 'expanded'), '&nbsp;')
- . html::tag('ul', ['style' => $data['collapsed'] ? "display:none" : null],
+ $content .= html::div('treetoggle ' . (!empty($data['collapsed']) ? 'collapsed' : 'expanded'), '&nbsp;')
+ . html::tag('ul', ['style' => !empty($data['collapsed']) ? "display:none" : null],
implode("\n", $children));
}
return html::tag('li', $attribs, $content);
}
public static function folder_filter($attrib)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$namespace = $storage->get_namespace();
if (empty($namespace['personal']) && empty($namespace['shared']) && empty($namespace['other'])) {
return '';
}
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmfolderfilter';
}
if (!rcube_utils::get_boolean($attrib['noevent'])) {
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . '.folder_filter(this.value)';
}
$roots = [];
$select = new html_select($attrib);
$select->add($rcmail->gettext('all'), '---');
foreach (array_keys($namespace) as $type) {
foreach ((array)$namespace[$type] as $ns) {
$root = rtrim($ns[0], $ns[1]);
$label = $rcmail->gettext('namespace.' . $type);
if (count($namespace[$type]) > 1) {
$label .= ' (' . rcube_charset::convert($root, 'UTF7-IMAP', RCUBE_CHARSET) . ')';
}
$select->add($label, $root);
if (strlen($root)) {
$roots[] = $root;
}
}
}
$rcmail->output->add_gui_object('foldersfilter', $attrib['id']);
$rcmail->output->set_env('ns_roots', $roots);
return $select->show();
}
public static function folder_options($mailbox)
{
$rcmail = rcmail::get_instance();
$options = $rcmail->get_storage()->folder_info($mailbox);
- $options['protected'] = $options['is_root']
+ $options['protected'] = !empty($options['is_root'])
|| strtoupper($mailbox) === 'INBOX'
- || ($options['special'] && $rcmail->config->get('protect_default_folders'));
+ || (!empty($options['special']) && $rcmail->config->get('protect_default_folders'));
return $options;
}
/**
* Updates (or creates) folder row in the subscriptions table
*
* @param string $name Folder name
* @param string $oldname Old folder name (for update)
* @param bool $subscribe Checks subscription checkbox
* @param string $class CSS class name for folder row
*/
public static function update_folder_row($name, $oldname = null, $subscribe = false, $class_name = null)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$options = self::folder_options($name);
$name_utf8 = rcube_charset::convert($name, 'UTF7-IMAP');
$foldersplit = explode($delimiter, $storage->mod_folder($name));
$level = count($foldersplit) - 1;
$class_name = trim($class_name . ' mailbox');
if (!empty($options['protected'])) {
$display_name = self::localize_foldername($name);
}
else {
$display_name = rcube_charset::convert($foldersplit[$level], 'UTF7-IMAP');
}
+ $protected = !empty($options['protected']) || !empty($options['noselect']);
+
if ($oldname === null) {
$rcmail->output->command('add_folder_row', $name, $name_utf8, $display_name,
- $options['protected'] || $options['noselect'], $subscribe, $class_name);
+ $protected, $subscribe, $class_name);
}
else {
$rcmail->output->command('replace_folder_row', $oldname, $name, $name_utf8, $display_name,
- $options['protected'] || $options['noselect'], $class_name);
+ $protected, $class_name);
}
}
}
diff --git a/program/actions/settings/identities.php b/program/actions/settings/identities.php
index 1741e9e5b..ce04f027b 100644
--- a/program/actions/settings/identities.php
+++ b/program/actions/settings/identities.php
@@ -1,71 +1,73 @@
<?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: |
| Manage identities of a user account |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identities extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('identities'));
$rcmail->output->include_script('list.js');
$rcmail->output->set_env('identities_level', (int) $rcmail->config->get('identities_level', 0));
$rcmail->output->add_label('deleteidentityconfirm');
$rcmail->output->add_handlers([
'identitieslist' => [$this, 'identities_list'],
]);
$rcmail->output->send('identities');
}
public static function identities_list($attrib)
{
$rcmail = rcmail::get_instance();
// add id to message list table if not specified
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmIdentitiesList';
}
// get identities list and define 'mail' column
$list = $rcmail->user->list_emails();
foreach ($list as $idx => $row) {
$list[$idx]['mail'] = trim($row['name'] . ' <' . rcube_utils::idn_to_utf8($row['email']) . '>');
}
// get all identites from DB and define list of cols to be displayed
$plugin = $rcmail->plugins->exec_hook('identities_list', [
'list' => $list,
'cols' => ['mail']
]);
// @TODO: use <UL> instead of <TABLE> for identities list
$out = self::table_output($attrib, $plugin['list'], $plugin['cols'], 'identity_id');
// set client env
$rcmail->output->add_gui_object('identitieslist', $attrib['id']);
return $out;
}
}
diff --git a/program/actions/settings/identity_delete.php b/program/actions/settings/identity_delete.php
index 01b6fe384..5f3aeb951 100644
--- a/program/actions/settings/identity_delete.php
+++ b/program/actions/settings/identity_delete.php
@@ -1,51 +1,51 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for identity delete action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_delete extends rcmail_action
{
- static $mode = self::MODE_AJAX;
+ protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$iid = rcube_utils::get_input_value('_iid', rcube_utils::INPUT_POST);
if ($iid && preg_match('/^[0-9]+(,[0-9]+)*$/', $iid)) {
$plugin = $rcmail->plugins->exec_hook('identity_delete', ['id' => $iid]);
$deleted = !$plugin['abort'] ? $rcmail->user->delete_identity($iid) : $plugin['result'];
if ($deleted > 0 && $deleted !== false) {
$rcmail->output->show_message('deletedsuccessfully', 'confirmation', null, false);
$rcmail->output->command('remove_identity', $iid);
}
else {
$msg = $plugin['message'] ?: ($deleted < 0 ? 'nodeletelastidentity' : 'errorsaving');
$rcmail->output->show_message($msg, 'error', null, false);
}
}
$rcmail->output->send();
}
}
diff --git a/program/actions/settings/identity_edit.php b/program/actions/settings/identity_edit.php
index ede7ab196..3f9d3ca2d 100644
--- a/program/actions/settings/identity_edit.php
+++ b/program/actions/settings/identity_edit.php
@@ -1,227 +1,228 @@
<?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: |
| Show edit form for an identity record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_edit extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
protected static $record;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$IDENTITIES_LEVEL = intval($rcmail->config->get('identities_level', 0));
// edit-identity
if (($_GET['_iid'] || $_POST['_iid']) && $rcmail->action == 'edit-identity') {
$id = rcube_utils::get_input_value('_iid', rcube_utils::INPUT_GPC);
self::$record = $rcmail->user->get_identity($id);
if (!is_array(self::$record)) {
$rcmail->output->show_message('dberror', 'error');
// go to identities page
$rcmail->overwrite_action('identities');
return;
}
$rcmail->output->set_env('iid', self::$record['identity_id']);
$rcmail->output->set_env('mailvelope_main_keyring', $rcmail->config->get('mailvelope_main_keyring'));
$rcmail->output->set_env('mailvelope_keysize', $rcmail->config->get('mailvelope_keysize'));
}
// add-identity
else {
if ($IDENTITIES_LEVEL > 1) {
$rcmail->output->show_message('opnotpermitted', 'error');
// go to identities page
$rcmail->overwrite_action('identities');
return;
}
if ($IDENTITIES_LEVEL == 1) {
self::$record['email'] = $rcmail->get_user_email();
}
}
$rcmail->output->include_script('list.js');
$rcmail->output->add_handler('identityform', [$this, 'identity_form']);
$rcmail->output->set_env('identities_level', $IDENTITIES_LEVEL);
$rcmail->output->add_label('deleteidentityconfirm', 'generate',
'encryptioncreatekey', 'openmailvelopesettings', 'encryptionprivkeysinmailvelope',
'encryptionnoprivkeysinmailvelope', 'keypaircreatesuccess');
$rcmail->output->set_pagetitle($rcmail->gettext(($rcmail->action == 'add-identity' ? 'addidentity' : 'editidentity')));
if ($rcmail->action == 'add-identity' && $rcmail->output->template_exists('identityadd')) {
$rcmail->output->send('identityadd');
}
$rcmail->output->send('identityedit');
}
public static function identity_form($attrib)
{
$rcmail = rcmail::get_instance();
$IDENTITIES_LEVEL = intval($rcmail->config->get('identities_level', 0));
// Add HTML editor script(s)
self::html_editor('identity');
// add some labels to client
$rcmail->output->add_label('noemailwarning', 'converting', 'editorwarning');
$i_size = $attrib['size'] ?: 40;
$t_rows = $attrib['textarearows'] ?: 6;
$t_cols = $attrib['textareacols'] ?: 40;
// list of available cols
$form = [
'addressing' => [
'name' => $rcmail->gettext('settings'),
'content' => [
'name' => ['type' => 'text', 'size' => $i_size],
'email' => ['type' => 'text', 'size' => $i_size],
'organization' => ['type' => 'text', 'size' => $i_size],
'reply-to' => ['type' => 'text', 'size' => $i_size],
'bcc' => ['type' => 'text', 'size' => $i_size],
'standard' => ['type' => 'checkbox', 'label' => $rcmail->gettext('setdefault')],
]
],
'signature' => [
'name' => $rcmail->gettext('signature'),
'content' => [
'signature' => [
'type' => 'textarea',
'size' => $t_cols,
'rows' => $t_rows,
'spellcheck' => true,
'data-html-editor' => true
],
'html_signature' => [
'type' => 'checkbox',
'label' => $rcmail->gettext('htmlsignature'),
'onclick' => "return rcmail.command('toggle-editor', {id: 'rcmfd_signature', html: this.checked}, '', event)"
],
]
],
'encryption' => [
'name' => $rcmail->gettext('identityencryption'),
'attrs' => ['class' => 'identity-encryption', 'style' => 'display:none'],
'content' => html::div('identity-encryption-block', '')
]
];
// Enable TinyMCE editor
if (self::$record['html_signature']) {
$form['signature']['content']['signature']['class'] = 'mce_editor';
$form['signature']['content']['signature']['is_escaped'] = true;
// Correctly handle HTML entities in HTML editor (#1488483)
self::$record['signature'] = htmlspecialchars(self::$record['signature'], ENT_NOQUOTES, RCUBE_CHARSET);
}
// hide "default" checkbox if only one identity is allowed
if ($IDENTITIES_LEVEL > 1) {
unset($form['addressing']['content']['standard']);
}
// disable some field according to access level
if ($IDENTITIES_LEVEL == 1 || $IDENTITIES_LEVEL == 3) {
$form['addressing']['content']['email']['disabled'] = true;
$form['addressing']['content']['email']['class'] = 'disabled';
}
if ($IDENTITIES_LEVEL == 4) {
foreach ($form['addressing']['content'] as $formfield => $value){
$form['addressing']['content'][$formfield]['disabled'] = true;
$form['addressing']['content'][$formfield]['class'] = 'disabled';
}
}
self::$record['email'] = rcube_utils::idn_to_utf8(self::$record['email']);
// Allow plugins to modify identity form content
$plugin = $rcmail->plugins->exec_hook('identity_form', [
'form' => $form,
'record' => self::$record
]);
$form = $plugin['form'];
self::$record = $plugin['record'];
// Set form tags and hidden fields
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-identity',
intval(self::$record['identity_id']),
['name' => '_iid', 'value' => self::$record['identity_id']]
);
unset($plugin);
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
foreach ($form as $fieldset) {
if (empty($fieldset['content'])) {
continue;
}
$content = '';
if (is_array($fieldset['content'])) {
$table = new html_table(['cols' => 2]);
foreach ($fieldset['content'] as $col => $colprop) {
$colprop['id'] = 'rcmfd_'.$col;
$label = $colprop['label'] ?: $rcmail->gettext(str_replace('-', '', $col));
$value = $colprop['value'] ?: rcube_output::get_edit_field($col, self::$record[$col], $colprop, $colprop['type']);
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $value);
}
$content = $table->show($attrib);
}
else {
$content = $fieldset['content'];
}
$content = html::tag('legend', null, rcube::Q($fieldset['name'])) . $content;
$out .= html::tag('fieldset', $fieldset['attrs'], $content) . "\n";
}
$out .= $form_end;
// add image upload form
$max_size = self::upload_init($rcmail->config->get('identity_image_size', 64) * 1024);
$form_id = 'identityImageUpload';
$out .= '<form id="' . $form_id . '" style="display: none">'
. html::div('hint', $rcmail->gettext(['name' => 'maxuploadsize', 'vars' => ['size' => $max_size]]))
. '</form>';
$rcmail->output->add_gui_object('uploadform', $form_id);
return $out;
}
}
diff --git a/program/actions/settings/identity_save.php b/program/actions/settings/identity_save.php
index 950a5cd74..de69d6cef 100644
--- a/program/actions/settings/identity_save.php
+++ b/program/actions/settings/identity_save.php
@@ -1,268 +1,270 @@
<?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: |
| Save an identity record or to add a new one |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_save extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$IDENTITIES_LEVEL = intval($rcmail->config->get('identities_level', 0));
$a_save_cols = ['name', 'email', 'organization', 'reply-to', 'bcc', 'standard', 'signature', 'html_signature'];
$a_boolean_cols = ['standard', 'html_signature'];
$updated = $default_id = false;
// check input
if (empty($_POST['_email']) && ($IDENTITIES_LEVEL == 0 || $IDENTITIES_LEVEL == 2)) {
$rcmail->output->show_message('noemailwarning', 'warning');
$rcmail->overwrite_action('edit-identity');
return;
}
$save_data = [];
foreach ($a_save_cols as $col) {
$fname = '_'.$col;
if (isset($_POST[$fname])) {
$save_data[$col] = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
}
}
// set "off" values for checkboxes that were not checked, and therefore
// not included in the POST body.
foreach ($a_boolean_cols as $col) {
$fname = '_' . $col;
if (!isset($_POST[$fname])) {
$save_data[$col] = 0;
}
}
// make the identity a "default" if only one identity is allowed
if ($IDENTITIES_LEVEL > 1) {
$save_data['standard'] = 1;
}
// unset email address if user has no rights to change it
if ($IDENTITIES_LEVEL == 1 || $IDENTITIES_LEVEL == 3) {
unset($save_data['email']);
}
// unset all fields except signature
else if ($IDENTITIES_LEVEL == 4) {
foreach ($save_data as $idx => $value) {
if ($idx != 'signature' && $idx != 'html_signature') {
unset($save_data[$idx]);
}
}
}
// Validate e-mail addresses
$email_checks = [rcube_utils::idn_to_ascii($save_data['email'])];
foreach (['reply-to', 'bcc'] as $item) {
foreach (rcube_mime::decode_address_list($save_data[$item], null, false) as $rcpt) {
$email_checks[] = rcube_utils::idn_to_ascii($rcpt['mailto']);
}
}
foreach ($email_checks as $email) {
if ($email && !rcube_utils::check_email($email)) {
// show error message
$rcmail->output->show_message('emailformaterror', 'error', ['email' => rcube_utils::idn_to_utf8($email)], false);
$rcmail->overwrite_action('edit-identity');
return;
}
}
if (!empty($save_data['signature']) && !empty($save_data['html_signature'])) {
// replace uploaded images with data URIs
$save_data['signature'] = self::attach_images($save_data['signature']);
// XSS protection in HTML signature (#1489251)
$save_data['signature'] = self::wash_html($save_data['signature']);
// clear POST data of signature, we want to use safe content
// when the form is displayed again
unset($_POST['_signature']);
}
// update an existing identity
if (!empty($_POST['_iid'])) {
$iid = rcube_utils::get_input_value('_iid', rcube_utils::INPUT_POST);
if (in_array($IDENTITIES_LEVEL, [1, 3, 4])) {
// merge with old identity data, fixes #1488834
$identity = $rcmail->user->get_identity($iid);
$save_data = array_merge($identity, $save_data);
unset($save_data['changed'], $save_data['del'], $save_data['user_id'], $save_data['identity_id']);
}
$plugin = $rcmail->plugins->exec_hook('identity_update', ['id' => $iid, 'record' => $save_data]);
$save_data = $plugin['record'];
if ($save_data['email']) {
$save_data['email'] = rcube_utils::idn_to_ascii($save_data['email']);
}
if (!$plugin['abort']) {
$updated = $rcmail->user->update_identity($iid, $save_data);
}
else {
$updated = $plugin['result'];
}
if ($updated) {
$rcmail->output->show_message('successfullysaved', 'confirmation');
if (!empty($save_data['standard'])) {
$default_id = $iid;
}
// update the changed col in list
$name = $save_data['name'] . ' <' . rcube_utils::idn_to_utf8($save_data['email']) .'>';
$rcmail->output->command('parent.update_identity_row', $iid, rcube::Q(trim($name)));
}
else {
// show error message
$rcmail->output->show_message($plugin['message'] ?: 'errorsaving', 'error', null, false);
$rcmail->overwrite_action('edit-identity');
return;
}
}
// insert a new identity record
else if ($IDENTITIES_LEVEL < 2) {
if ($IDENTITIES_LEVEL == 1) {
$save_data['email'] = $rcmail->get_user_email();
}
$plugin = $rcmail->plugins->exec_hook('identity_create', ['record' => $save_data]);
$save_data = $plugin['record'];
if ($save_data['email']) {
$save_data['email'] = rcube_utils::idn_to_ascii($save_data['email']);
}
if (!$plugin['abort']) {
$insert_id = $save_data['email'] ? $rcmail->user->insert_identity($save_data) : null;
}
else {
$insert_id = $plugin['result'];
}
if ($insert_id) {
$rcmail->plugins->exec_hook('identity_create_after', ['id' => $insert_id, 'record' => $save_data]);
$rcmail->output->show_message('successfullysaved', 'confirmation', null, false);
$_GET['_iid'] = $insert_id;
if (!empty($save_data['standard'])) {
$default_id = $insert_id;
}
// add a new row to the list
$name = $save_data['name'] . ' <' . rcube_utils::idn_to_utf8($save_data['email']) .'>';
$rcmail->output->command('parent.update_identity_row', $insert_id, rcube::Q(trim($name)), true);
}
else {
// show error message
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error', null, false);
$rcmail->overwrite_action('edit-identity');
return;
}
}
else {
$rcmail->output->show_message('opnotpermitted', 'error');
}
// mark all other identities as 'not-default'
if ($default_id) {
$rcmail->user->set_default($default_id);
}
// go to next step
$rcmail->overwrite_action('edit-identity');
}
/**
* Attach uploaded images into signature as data URIs
*/
public static function attach_images($html)
{
$rcmail = rcmail::get_instance();
$offset = 0;
$regexp = '/\s(poster|src)\s*=\s*[\'"]*\S+upload-display\S+file=rcmfile(\w+)[\s\'"]*/';
while (preg_match($regexp, $html, $matches, 0, $offset)) {
$file_id = $matches[2];
$data_uri = ' ';
if ($file_id && ($file = $_SESSION['identity']['files'][$file_id])) {
$file = $rcmail->plugins->exec_hook('attachment_get', $file);
$data_uri .= 'src="data:' . $file['mimetype'] . ';base64,';
$data_uri .= base64_encode($file['data'] ?: file_get_contents($file['path']));
$data_uri .= '" ';
}
$html = str_replace($matches[0], $data_uri, $html);
$offset += strlen($data_uri) - strlen($matches[0]) + 1;
}
return $html;
}
/**
* Sanity checks/cleanups on HTML body of signature
*/
public static function wash_html($html)
{
// Add header with charset spec., washtml cannot work without that
$html = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
. '</head><body>' . $html . '</body></html>';
// clean HTML with washhtml by Frederic Motte
$wash_opts = [
'show_washed' => false,
'allow_remote' => 1,
'charset' => RCUBE_CHARSET,
'html_elements' => ['body', 'link'],
'html_attribs' => ['rel', 'type'],
];
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
// Remove non-UTF8 characters (#1487813)
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
// remove unwanted comments and tags (produced by washtml)
$html = preg_replace(['/<!--[^>]+-->/', '/<\/?body>/'], '', $html);
return $html;
}
}
diff --git a/program/actions/settings/prefs_edit.php b/program/actions/settings/prefs_edit.php
index 5bbe8579a..f016e7e7a 100644
--- a/program/actions/settings/prefs_edit.php
+++ b/program/actions/settings/prefs_edit.php
@@ -1,95 +1,96 @@
<?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: |
| Provide functionality for user's settings & preferences |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_prefs_edit extends rcmail_action_settings_index
{
+ protected static $mode = self::MODE_HTTP;
protected static $section;
protected static $sections;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('preferences'));
self::$section = rcube_utils::get_input_value('_section', rcube_utils::INPUT_GPC);
list(self::$sections,) = self::user_prefs(self::$section);
// register UI objects
$rcmail->output->add_handlers([
'userprefs' => [$this, 'user_prefs_form'],
'sectionname' => [$this, 'prefs_section_name'],
]);
$rcmail->output->send('settingsedit');
}
public static function user_prefs_form($attrib)
{
$rcmail = rcmail::get_instance();
// add some labels to client
$rcmail->output->add_label('nopagesizewarning', 'nosupporterror');
unset($attrib['form']);
$hidden = ['name' => '_section', 'value' => self::$section];
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-prefs', null, $hidden);
$out = $form_start;
if (!empty(self::$sections[self::$section]['header'])) {
$div_attr = ['id' => 'preferences-header', 'class' =>'boxcontent'];
$out .= html::div($div_attr, self::$sections[self::$section]['header']);
}
foreach (self::$sections[self::$section]['blocks'] as $class => $block) {
if (!empty($block['options'])) {
$table = new html_table(['cols' => 2]);
foreach ($block['options'] as $option) {
if (isset($option['title'])) {
$table->add('title', $option['title']);
$table->add(null, $option['content']);
}
else {
$table->add(['colspan' => 2], $option['content']);
}
}
$out .= html::tag('fieldset', $class, html::tag('legend', null, $block['name']) . $table->show($attrib));
}
else if (!empty($block['content'])) {
$out .= html::tag('fieldset', null, html::tag('legend', null, $block['name']) . $block['content']);
}
}
return $out . $form_end;
}
public static function prefs_section_name()
{
return self::$sections[self::$section]['section'];
}
}
diff --git a/program/actions/settings/prefs_save.php b/program/actions/settings/prefs_save.php
index 33764bb5c..85649d56e 100644
--- a/program/actions/settings/prefs_save.php
+++ b/program/actions/settings/prefs_save.php
@@ -1,276 +1,278 @@
<?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: |
| Save user preferences to DB and to the current session |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_prefs_save extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$CURR_SECTION = rcube_utils::get_input_value('_section', rcube_utils::INPUT_POST);
$dont_override = (array) $rcmail->config->get('dont_override');
$a_user_prefs = [];
// set options for specified section
switch ($CURR_SECTION) {
case 'general':
$a_user_prefs = [
'language' => self::prefs_input('language', '/^[a-zA-Z_-]+$/'),
'timezone' => self::prefs_input('timezone', '/^[a-zA-Z_\/-]+$/'),
'date_format' => self::prefs_input('date_format', '/^[a-zA-Z_.\/ -]+$/'),
'time_format' => self::prefs_input('time_format', '/^[a-zA-Z0-9: ]+$/'),
'prettydate' => isset($_POST['_pretty_date']),
'display_next' => isset($_POST['_display_next']),
'refresh_interval' => intval($_POST['_refresh_interval']) * 60,
'standard_windows' => isset($_POST['_standard_windows']),
'skin' => self::prefs_input('skin', '/^[a-zA-Z0-9_.-]+$/'),
];
// compose derived date/time format strings
if (
(isset($_POST['_date_format']) || isset($_POST['_time_format']))
&& $a_user_prefs['date_format']
&& $a_user_prefs['time_format']
) {
$a_user_prefs['date_short'] = 'D ' . $a_user_prefs['time_format'];
$a_user_prefs['date_long'] = $a_user_prefs['date_format'] . ' ' . $a_user_prefs['time_format'];
}
break;
case 'mailbox':
$a_user_prefs = [
'layout' => self::prefs_input('layout', '/^[a-z]+$/'),
'mail_read_time' => intval($_POST['_mail_read_time']),
'autoexpand_threads' => intval($_POST['_autoexpand_threads']),
'check_all_folders' => isset($_POST['_check_all_folders']),
'mail_pagesize' => max(2, intval($_POST['_mail_pagesize'])),
];
break;
case 'mailview':
$a_user_prefs = [
'message_extwin' => intval($_POST['_message_extwin']),
'message_show_email' => isset($_POST['_message_show_email']),
'prefer_html' => isset($_POST['_prefer_html']),
'inline_images' => isset($_POST['_inline_images']),
'show_images' => intval($_POST['_show_images']),
'mdn_requests' => intval($_POST['_mdn_requests']),
'default_charset' => self::prefs_input('default_charset', '/^[a-zA-Z0-9-]+$/'),
];
break;
case 'compose':
$a_user_prefs = [
'compose_extwin' => intval($_POST['_compose_extwin']),
'htmleditor' => intval($_POST['_htmleditor']),
'draft_autosave' => intval($_POST['_draft_autosave']),
'mime_param_folding' => intval($_POST['_mime_param_folding']),
'force_7bit' => isset($_POST['_force_7bit']),
'mdn_default' => isset($_POST['_mdn_default']),
'dsn_default' => isset($_POST['_dsn_default']),
'reply_same_folder' => isset($_POST['_reply_same_folder']),
'spellcheck_before_send' => isset($_POST['_spellcheck_before_send']),
'spellcheck_ignore_syms' => isset($_POST['_spellcheck_ignore_syms']),
'spellcheck_ignore_nums' => isset($_POST['_spellcheck_ignore_nums']),
'spellcheck_ignore_caps' => isset($_POST['_spellcheck_ignore_caps']),
'show_sig' => intval($_POST['_show_sig']),
'reply_mode' => intval($_POST['_reply_mode']),
'sig_below' => isset($_POST['_sig_below']),
'strip_existing_sig' => isset($_POST['_strip_existing_sig']),
'sig_separator' => isset($_POST['_sig_separator']),
'default_font' => self::prefs_input('default_font', '/^[a-zA-Z ]+$/'),
'default_font_size' => self::prefs_input('default_font_size', '/^[0-9]+pt$/'),
'reply_all_mode' => intval($_POST['_reply_all_mode']),
'forward_attachment' => !empty($_POST['_forward_attachment']),
'compose_save_localstorage' => intval($_POST['_compose_save_localstorage']),
];
break;
case 'addressbook':
$a_user_prefs = [
'default_addressbook' => rcube_utils::get_input_value('_default_addressbook', rcube_utils::INPUT_POST, true),
'collected_recipients' => rcube_utils::get_input_value('_collected_recipients', rcube_utils::INPUT_POST, true),
'collected_senders' => rcube_utils::get_input_value('_collected_senders', rcube_utils::INPUT_POST, true),
'autocomplete_single' => isset($_POST['_autocomplete_single']),
'addressbook_sort_col' => self::prefs_input('addressbook_sort_col', '/^[a-z_]+$/'),
'addressbook_name_listing' => intval($_POST['_addressbook_name_listing']),
'addressbook_pagesize' => max(2, intval($_POST['_addressbook_pagesize'])),
'contact_form_mode' => self::prefs_input('contact_form_mode', '/^(private|business)$/'),
];
break;
case 'server':
$a_user_prefs = [
'read_when_deleted' => isset($_POST['_read_when_deleted']),
'skip_deleted' => isset($_POST['_skip_deleted']),
'flag_for_deletion' => isset($_POST['_flag_for_deletion']),
'delete_junk' => isset($_POST['_delete_junk']),
'logout_purge' => isset($_POST['_logout_purge']),
'logout_expunge' => isset($_POST['_logout_expunge']),
];
break;
case 'folders':
$a_user_prefs = [
'show_real_foldernames' => isset($_POST['_show_real_foldernames']),
// stop using SPECIAL-USE (#4782)
'lock_special_folders' => !in_array('lock_special_folders', $dont_override),
];
foreach (rcube_storage::$folder_types as $type) {
$a_user_prefs[$type . '_mbox'] = rcube_utils::get_input_value('_' . $type . '_mbox', rcube_utils::INPUT_POST, true);
};
break;
case 'encryption':
$a_user_prefs = [
'mailvelope_main_keyring' => isset($_POST['_mailvelope_main_keyring']),
];
break;
}
$plugin = rcmail::get_instance()->plugins->exec_hook('preferences_save',
['prefs' => $a_user_prefs, 'section' => $CURR_SECTION]);
$a_user_prefs = $plugin['prefs'];
// don't override these parameters
foreach ($dont_override as $p) {
$a_user_prefs[$p] = $rcmail->config->get($p);
}
// verify some options
switch ($CURR_SECTION) {
case 'general':
// switch UI language
if (isset($_POST['_language']) && $a_user_prefs['language'] != $_SESSION['language']) {
$rcmail->load_language($a_user_prefs['language']);
$rcmail->output->command('reload', 500);
}
// switch skin (if valid, otherwise unset the pref and fall back to default)
if (!$rcmail->output->check_skin($a_user_prefs['skin'])) {
unset($a_user_prefs['skin']);
}
else if ($rcmail->config->get('skin') != $a_user_prefs['skin']) {
$rcmail->output->command('reload', 500);
}
$a_user_prefs['timezone'] = (string) $a_user_prefs['timezone'];
$min_refresh_interval = (int) $rcmail->config->get('min_refresh_interval');
if (!empty($a_user_prefs['refresh_interval']) && $min_refresh_interval) {
if ($a_user_prefs['refresh_interval'] < $min_refresh_interval) {
$a_user_prefs['refresh_interval'] = $min_refresh_interval;
}
}
break;
case 'mailbox':
// force min size
if ($a_user_prefs['mail_pagesize'] < 1) {
$a_user_prefs['mail_pagesize'] = 10;
}
$max_pagesize = (int) $rcmail->config->get('max_pagesize');
if ($max_pagesize && ($a_user_prefs['mail_pagesize'] > $max_pagesize)) {
$a_user_prefs['mail_pagesize'] = $max_pagesize;
}
break;
case 'addressbook':
// force min size
if ($a_user_prefs['addressbook_pagesize'] < 1) {
$a_user_prefs['addressbook_pagesize'] = 10;
}
$max_pagesize = (int) $rcmail->config->get('max_pagesize');
if ($max_pagesize && ($a_user_prefs['addressbook_pagesize'] > $max_pagesize)) {
$a_user_prefs['addressbook_pagesize'] = $max_pagesize;
}
break;
case 'folders':
$storage = $rcmail->get_storage();
$specials = [];
foreach (rcube_storage::$folder_types as $type) {
$specials[$type] = $a_user_prefs[$type . '_mbox'];
}
$storage->set_special_folders($specials);
break;
}
// Save preferences
if (empty($plugin['abort'])) {
$saved = $rcmail->user->save_prefs($a_user_prefs);
}
else {
$saved = $plugin['result'];
}
if ($saved) {
$rcmail->output->show_message('successfullysaved', 'confirmation');
}
else {
$rcmail->output->show_message(!empty($plugin['message']) ? $plugin['message'] : 'errorsaving', 'error');
}
// display the form again
$rcmail->overwrite_action('edit-prefs');
}
/**
* Get option value from POST and validate with a regex
*/
public static function prefs_input($name, $regex)
{
$rcmail = rcmail::get_instance();
$value = rcube_utils::get_input_value('_' . $name, rcube_utils::INPUT_POST);
if (!is_string($value)) {
$value = null;
}
if ($value !== null && strlen($value) && !preg_match($regex, $value)) {
$value = $rcmail->config->get($name);
}
return $value;
}
}
diff --git a/program/actions/settings/response_edit.php b/program/actions/settings/response_edit.php
index 14b031b1e..c87c85444 100644
--- a/program/actions/settings/response_edit.php
+++ b/program/actions/settings/response_edit.php
@@ -1,88 +1,89 @@
<?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: |
| Show edit form for a canned response record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_edit extends rcmail_action_settings_responses
{
+ protected static $mode = self::MODE_HTTP;
protected static $response;
protected static $responses;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$title = $rcmail->gettext($rcmail->action == 'add-response' ? 'addresponse' : 'editresponse');
self::set_response();
$rcmail->output->set_pagetitle($title);
$rcmail->output->set_env('readonly', !empty(self::$response['static']));
$rcmail->output->add_handler('responseform', [$this, 'response_form']);
$rcmail->output->send('responseedit');
}
public static function set_response()
{
$rcmail = rcmail::get_instance();
self::$responses = $rcmail->get_compose_responses();
// edit-response
if (($key = rcube_utils::get_input_value('_key', rcube_utils::INPUT_GPC))) {
foreach (self::$responses as $i => $response) {
if ($response['key'] == $key) {
self::$response = $response;
self::$response['index'] = $i;
break;
}
}
}
return self::$response;
}
public static function response_form($attrib)
{
$rcmail = rcmail::get_instance();
// Set form tags and hidden fields
$disabled = !empty(self::$response['static']);
$key = self::$response['key'];
$hidden = ['name' => '_key', 'value' => $key];
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-response', $key, $hidden);
unset($attrib['form'], $attrib['id']);
$table = new html_table(['cols' => 2]);
$table->add('title', html::label('ffname', rcube::Q($rcmail->gettext('responsename'))));
$table->add(null, rcube_output::get_edit_field('name', self::$response['name'],
['id' => 'ffname', 'size' => $attrib['size'], 'disabled' => $disabled], 'text'));
$table->add('title', html::label('fftext', rcube::Q($rcmail->gettext('responsetext'))));
$table->add(null, rcube_output::get_edit_field('text', self::$response['text'],
['id' => 'fftext', 'size' => $attrib['textareacols'], 'rows' => $attrib['textarearows'], 'disabled' => $disabled], 'textarea'));
// return the complete edit form as table
return "$form_start\n" . $table->show($attrib) . $form_end;
}
}
diff --git a/program/actions/settings/response_save.php b/program/actions/settings/response_save.php
index c793ec4a0..9067ff61c 100644
--- a/program/actions/settings/response_save.php
+++ b/program/actions/settings/response_save.php
@@ -1,86 +1,88 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for saving a canned response record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_save extends rcmail_action_settings_response_edit
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
self::set_response();
if (isset($_POST['_name']) && empty(self::$response['static'])) {
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST));
$text = trim(rcube_utils::get_input_value('_text', rcube_utils::INPUT_POST, true));
if (!empty($name) && !empty($text)) {
$dupes = 0;
foreach (self::$responses as $i => $resp) {
if (!empty(self::$response) && self::$response['index'] === $i) {
continue;
}
if (strcasecmp($name, preg_replace('/\s\(\d+\)$/', '', $resp['name'])) == 0) {
$dupes++;
}
}
if ($dupes) { // require a unique name
$name .= ' (' . ++$dupes . ')';
}
$response = [
'name' => $name,
'text' => $text,
'format' => 'text',
'key' => substr(md5($name), 0, 16)
];
if (!empty(self::$response) && self::$responses[self::$response['index']]) {
self::$responses[self::$response['index']] = $response;
}
else {
self::$responses[] = $response;
}
self::$responses = array_filter(self::$responses, function($item) { return empty($item['static']); });
if ($rcmail->user->save_prefs(['compose_responses' => array_values(self::$responses)])) {
$key = !empty(self::$response) ? self::$response['key'] : null;
$rcmail->output->show_message('successfullysaved', 'confirmation');
$rcmail->output->command('parent.update_response_row', $response, $key);
$rcmail->overwrite_action('edit-response');
self::$response = $response;
}
}
else {
$rcmail->output->show_message('formincomplete', 'error');
}
}
// display the form again
$rcmail->overwrite_action(empty(self::$response) ? 'add-response' : 'edit-response');
}
}
diff --git a/program/actions/settings/responses.php b/program/actions/settings/responses.php
index 5ab1c4c37..e8ce020a0 100644
--- a/program/actions/settings/responses.php
+++ b/program/actions/settings/responses.php
@@ -1,102 +1,104 @@
<?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: |
| Listing of canned responses, and quick insert action handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_responses extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
if (!empty($_POST['_insert'])) {
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST));
$text = trim(rcube_utils::get_input_value('_text', rcube_utils::INPUT_POST, true));
if (!empty($name) && !empty($text)) {
$dupes = 0;
$responses = $rcmail->get_compose_responses(false, true);
foreach ($responses as $resp) {
if (strcasecmp($name, preg_replace('/\s\(\d+\)$/', '', $resp['name'])) == 0) {
$dupes++;
}
}
if ($dupes) { // require a unique name
$name .= ' (' . ++$dupes . ')';
}
$response = ['name' => $name, 'text' => $text, 'format' => 'text', 'key' => substr(md5($name), 0, 16)];
$responses[] = $response;
if ($rcmail->user->save_prefs(['compose_responses' => $responses])) {
$rcmail->output->command('add_response_item', $response);
$rcmail->output->command('display_message', $rcmail->gettext('successfullysaved'), 'confirmation');
}
else {
$rcmail->output->command('display_message', $rcmail->gettext('errorsaving'), 'error');
}
}
$rcmail->output->send();
}
$rcmail->output->set_pagetitle($rcmail->gettext('responses'));
$rcmail->output->include_script('list.js');
$rcmail->output->add_label('deleteresponseconfirm');
$rcmail->output->add_handlers([
'responseslist' => [$this, 'responses_list'],
]);
$rcmail->output->send('responses');
}
/**
* Create template object 'responseslist'
*/
public static function responses_list($attrib)
{
$rcmail = rcmail::get_instance();
$attrib += ['id' => 'rcmresponseslist', 'tagname' => 'table'];
$plugin = $rcmail->plugins->exec_hook('responses_list', [
'list' => $rcmail->get_compose_responses(true),
'cols' => ['name']
]);
$out = self::table_output($attrib, $plugin['list'], $plugin['cols'], 'key');
$readonly_responses = [];
foreach ($plugin['list'] as $item) {
if (!empty($item['static'])) {
$readonly_responses[] = $item['key'];
}
}
// set client env
$rcmail->output->add_gui_object('responseslist', $attrib['id']);
$rcmail->output->set_env('readonly_responses', $readonly_responses);
return $out;
}
}
diff --git a/program/actions/settings/upload.php b/program/actions/settings/upload.php
index 2d59de74d..34dc45a91 100644
--- a/program/actions/settings/upload.php
+++ b/program/actions/settings/upload.php
@@ -1,123 +1,122 @@
<?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: |
| Handles image uploads |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_upload extends rcmail_action
{
- // only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_GET);
$type = preg_replace('/(add|edit)-/', '', $from);
// Plugins in Settings may use this file for some uploads (#5694)
// Make sure it does not contain a dot, which is a special character
// when using rcube_session::append() below
$type = str_replace('.', '-', $type);
// Supported image format types
$IMAGE_TYPES = explode(',', 'jpeg,jpg,jp2,tiff,tif,bmp,eps,gif,png,png8,png24,png32,svg,ico');
// clear all stored output properties (like scripts and env vars)
$rcmail->output->reset();
$max_size = $rcmail->config->get($type . '_image_size', 64) * 1024;
$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GET);
if (is_array($_FILES['_file']['tmp_name'])) {
$multiple = count($_FILES['_file']['tmp_name']) > 1;
foreach ($_FILES['_file']['tmp_name'] as $i => $filepath) {
$err = $_FILES['_file']['error'][$i];
$imageprop = null;
$attachment = null;
// Process uploaded attachment if there is no error
if (!$err) {
if ($max_size < $_FILES['_file']['size'][$i]) {
$err = 'size_error';
}
// check image file type
else {
$image = new rcube_image($filepath);
$imageprop = $image->props();
if (!in_array(strtolower($imageprop['type']), $IMAGE_TYPES)) {
$err = 'type_error';
}
}
}
// save uploaded image in storage backend
if (!empty($imageprop)) {
$attachment = $rcmail->plugins->exec_hook('attachment_upload', [
'path' => $filepath,
'size' => $_FILES['_file']['size'][$i],
'name' => $_FILES['_file']['name'][$i],
'mimetype' => 'image/' . $imageprop['type'],
'group' => $type,
]);
}
if (!$err && !empty($attachment['status']) && empty($attachment['abort'])) {
$id = $attachment['id'];
// store new file in session
unset($attachment['status'], $attachment['abort']);
$rcmail->session->append($type . '.files', $id, $attachment);
$content = rcube::Q($attachment['name']);
$rcmail->output->command('add2attachment_list', "rcmfile$id", [
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true
],
$uploadid
);
}
else {
$error_label = null;
if ($err == 'type_error') {
$error_label = 'invalidimageformat';
}
else if ($err == 'size_error') {
$error_label = ['name' => 'filesizeerror', 'vars' => ['size' => $max_size]];
}
self::upload_error($err, $attachment, $error_label);
}
}
}
else if (self::upload_failure()) {
$rcmail->output->command('remove_from_attachment_list', $uploadid);
}
$rcmail->output->send('iframe');
}
}
diff --git a/program/actions/settings/upload_display.php b/program/actions/settings/upload_display.php
index 98596e4fd..aa5830c90 100644
--- a/program/actions/settings/upload_display.php
+++ b/program/actions/settings/upload_display.php
@@ -1,47 +1,49 @@
<?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: |
| Displaying uploaded images |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_upload_display extends rcmail_action
{
+ protected static $mode = self::MODE_HTTP;
+
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_GET);
$type = preg_replace('/(add|edit)-/', '', $from);
// Plugins in Settings may use this file for some uploads (#5694)
// Make sure it does not contain a dot, which is a special character
// when using rcube_session::append() below
$type = str_replace('.', '-', $type);
$id = 'undefined';
if (preg_match('/^rcmfile(\w+)$/', $_GET['_file'], $regs)) {
$id = $regs[1];
}
self::display_uploaded_file($_SESSION[$type]['files'][$id]);
exit;
}
}
diff --git a/program/include/rcmail_action.php b/program/include/rcmail_action.php
index 3184e5cf9..4149ccc14 100644
--- a/program/include/rcmail_action.php
+++ b/program/include/rcmail_action.php
@@ -1,1456 +1,1459 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| An abstract for HTTP request handlers with some helpers. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* An abstract for HTTP request handlers with some helpers.
*
* @package Webmail
*/
abstract class rcmail_action
{
const MODE_AJAX = 1;
const MODE_HTTP = 2;
/**
* Mode of operation supported by the action. Use MODE_* constants.
* By default all modes are allowed.
*
* @var int
*/
protected static $mode;
/**
* Deprecated action aliases.
*
* @todo Get rid of these (but it will be a big BC break)
* @var array
*/
public static $aliases = [];
/**
* Request handler. The only abstract method.
*
* @param array $args Arguments from the previous step(s)
*/
abstract public function run($args = []);
/**
* Request sanity checks, e.g. supported request mode
*
* @return bool
*/
public function checks()
{
$rcmail = rcmail::get_instance();
if (static::$mode) {
if (!(static::$mode & self::MODE_HTTP) && empty($rcmail->output->ajax_call)) {
return false;
}
if (!(static::$mode & self::MODE_AJAX) && !empty($rcmail->output->ajax_call)) {
return false;
}
}
return true;
}
/**
* Set environment variables for specified config boolean options
*
* @param array $options List of configuration option names
*/
public static function set_env_config($options)
{
$rcmail = rcmail::get_instance();
foreach ((array) $options as $option) {
if ($rcmail->config->get($option)) {
$rcmail->output->set_env($option, true);
}
}
}
/**
* Create a HTML table based on the given data
*
* @param array $attrib Named table attributes
* @param mixed $table_data Table row data. Either a two-dimensional array
* or a valid SQL result set
* @param array $show_cols List of cols to show
* @param string $id_col Name of the identifier col
*
* @return string HTML table code
*/
public static function table_output($attrib, $table_data, $show_cols, $id_col)
{
$rcmail = rcmail::get_instance();
$table = new html_table($attrib);
// add table header
if (!$attrib['noheader']) {
foreach ($show_cols as $col) {
$table->add_header($col, rcube::Q($rcmail->gettext($col)));
}
}
if (!is_array($table_data)) {
$db = $rcmail->get_dbh();
while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
$table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
// format each col
foreach ($show_cols as $col) {
$table->add($col, rcube::Q($sql_arr[$col]));
}
}
}
else {
foreach ($table_data as $row_data) {
$class = !empty($row_data['class']) ? $row_data['class'] : null;
if (!empty($attrib['rowclass'])) {
$class = trim($class . ' ' . $attrib['rowclass']);
}
$rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
$table->add_row(['id' => $rowid, 'class' => $class]);
// format each col
foreach ($show_cols as $col) {
$val = is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col];
$table->add($col, empty($attrib['ishtml']) ? rcube::Q($val) : $val);
}
}
}
return $table->show($attrib);
}
/**
* Return HTML for quota indicator object
*
* @param array $attrib Named parameters
*
* @return string HTML code for the quota indicator object
*/
public static function quota_display($attrib)
{
$rcmail = rcmail::get_instance();
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmquotadisplay';
}
$_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
$quota = self::quota_content($attrib);
$rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
$rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
return html::span($attrib, '&nbsp;');
}
/**
* Return (parsed) quota information
*
* @param array $attrib Named parameters
* @param array $folder Current folder
*
* @return array Quota information
*/
public static function quota_content($attrib = null, $folder = null)
{
$rcmail = rcmail::get_instance();
$quota = $rcmail->storage->get_quota($folder);
$quota = $rcmail->plugins->exec_hook('quota', $quota);
$quota_result = (array) $quota;
$quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
$quota_result['folder'] = $folder !== null && $folder !== '' ? $folder : 'INBOX';
if (!empty($quota['total']) && $quota['total'] > 0) {
if (!isset($quota['percent'])) {
$quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
}
$title = $rcmail->gettext('quota') . ': ' . sprintf('%s / %s (%.0f%%)',
self::show_bytes($quota['used'] * 1024),
self::show_bytes($quota['total'] * 1024),
$quota_result['percent']
);
$quota_result['title'] = $title;
if (!empty($attrib['width'])) {
$quota_result['width'] = $attrib['width'];
}
if (!empty($attrib['height'])) {
$quota_result['height'] = $attrib['height'];
}
// build a table of quota types/roots info
if (($root_cnt = count($quota_result['all'])) > 1 || count($quota_result['all'][key($quota_result['all'])]) > 1) {
$table = new html_table(array('cols' => 3, 'class' => 'quota-info'));
$table->add_header(null, rcube::Q($rcmail->gettext('quotatype')));
$table->add_header(null, rcube::Q($rcmail->gettext('quotatotal')));
$table->add_header(null, rcube::Q($rcmail->gettext('quotaused')));
foreach ($quota_result['all'] as $root => $data) {
if ($root_cnt > 1 && $root) {
$table->add(['colspan' => 3, 'class' => 'root'], rcube::Q($root));
}
if ($storage = $data['storage']) {
$percent = min(100, round(($storage['used']/max(1,$storage['total']))*100));
$table->add('name', rcube::Q($rcmail->gettext('quotastorage')));
$table->add(null, self::show_bytes($storage['total'] * 1024));
$table->add(null, sprintf('%s (%.0f%%)', self::show_bytes($storage['used'] * 1024), $percent));
}
if ($message = $data['message']) {
$percent = min(100, round(($message['used']/max(1,$message['total']))*100));
$table->add('name', rcube::Q($rcmail->gettext('quotamessage')));
$table->add(null, intval($message['total']));
$table->add(null, sprintf('%d (%.0f%%)', $message['used'], $percent));
}
}
$quota_result['table'] = $table->show();
}
}
else {
$unlimited = $rcmail->config->get('quota_zero_as_unlimited');
$quota_result['title'] = $rcmail->gettext($unlimited ? 'unlimited' : 'unknown');
$quota_result['percent'] = 0;
}
// cleanup
unset($quota_result['abort']);
if (empty($quota_result['table'])) {
unset($quota_result['all']);
}
return $quota_result;
}
/**
* Outputs error message according to server error/response codes
*
* @param string $fallback Fallback message label
* @param array $fallback_args Fallback message label arguments
* @param string $suffix Message label suffix
* @param array $params Additional parameters (type, prefix)
*/
public static function display_server_error($fallback = null, $fallback_args = null, $suffix = '', $params = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$err_code = $storage->get_error_code();
$res_code = $storage->get_response_code();
$args = [];
if ($res_code == rcube_storage::NOPERM) {
$error = 'errornoperm';
}
else if ($res_code == rcube_storage::READONLY) {
$error = 'errorreadonly';
}
else if ($res_code == rcube_storage::OVERQUOTA) {
$error = 'erroroverquota';
}
else if ($err_code && ($err_str = $storage->get_error_str())) {
// try to detect access rights problem and display appropriate message
if (stripos($err_str, 'Permission denied') !== false) {
$error = 'errornoperm';
}
// try to detect full mailbox problem and display appropriate message
// there can be e.g. "Quota exceeded" / "quotum would exceed" / "Over quota"
else if (stripos($err_str, 'quot') !== false && preg_match('/exceed|over/i', $err_str)) {
$error = 'erroroverquota';
}
else {
$error = 'servererrormsg';
$args = array('msg' => rcube::Q($err_str));
}
}
else if ($err_code < 0) {
$error = 'storageerror';
}
else if ($fallback) {
$error = $fallback;
$args = $fallback_args;
$params['prefix'] = false;
}
if (!empty($error)) {
if ($suffix && $rcmail->text_exists($error . $suffix)) {
$error .= $suffix;
}
$msg = $rcmail->gettext(array('name' => $error, 'vars' => $args));
if (!empty($params['prefix']) && $fallback) {
$msg = $rcmail->gettext(array('name' => $fallback, 'vars' => $fallback_args)) . ' ' . $msg;
}
$rcmail->output->show_message($msg, !empty($params['type']) ? $params['type'] : 'error');
}
}
/**
* Displays an error message on storage fatal errors
*/
public static function storage_fatal_error()
{
$rcmail = rcmail::get_instance();
$err_code = $rcmail->storage->get_error_code();
switch ($err_code) {
// Not all are really fatal, but these should catch
// connection/authentication errors the best we can
case rcube_imap_generic::ERROR_NO:
case rcube_imap_generic::ERROR_BAD:
case rcube_imap_generic::ERROR_BYE:
self::display_server_error();
}
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*/
public static function html_editor($mode = '')
{
$rcmail = rcmail::get_instance();
$spellcheck = intval($rcmail->config->get('enable_spellcheck'));
$spelldict = intval($rcmail->config->get('spellcheck_dictionary'));
$disabled_plugins = [];
$disabled_buttons = [];
$extra_plugins = [];
$extra_buttons = [];
if (!$spellcheck) {
$disabled_plugins[] = 'spellchecker';
}
$hook = $rcmail->plugins->exec_hook('html_editor', [
'mode' => $mode,
'disabled_plugins' => $disabled_plugins,
'disabled_buttons' => $disabled_buttons,
'extra_plugins' => $extra_plugins,
'extra_buttons' => $extra_buttons,
]);
if (!empty($hook['abort'])) {
return;
}
$lang_codes = [$_SESSION['language']];
$assets_dir = $rcmail->config->get('assets_dir') ?: INSTALL_PATH;
$skin_path = $rcmail->output->get_skin_path();
if ($pos = strpos($_SESSION['language'], '_')) {
$lang_codes[] = substr($_SESSION['language'], 0, $pos);
}
foreach ($lang_codes as $code) {
if (file_exists("$assets_dir/program/js/tinymce/langs/$code.js")) {
$lang = $code;
break;
}
}
if (empty($lang)) {
$lang = 'en';
}
$config = [
'mode' => $mode,
'lang' => $lang,
'skin_path' => $skin_path,
'spellcheck' => $spellcheck, // deprecated
'spelldict' => $spelldict,
'content_css' => 'program/resources/tinymce/content.css',
'disabled_plugins' => $hook['disabled_plugins'],
'disabled_buttons' => $hook['disabled_buttons'],
'extra_plugins' => $hook['extra_plugins'],
'extra_buttons' => $hook['extra_buttons'],
];
if ($path = $rcmail->config->get('editor_css_location')) {
if ($path = $rcmail->find_asset($skin_path . $path)) {
$config['content_css'] = $path;
}
}
$font_family = $rcmail->output->get_env('default_font');
$font_size = $rcmail->output->get_env('default_font_size');
$style = [];
if ($font_family) {
$style[] = "font-family: $font_family;";
}
if ($font_size) {
$style[] = "font-size: $font_size;";
}
if (!empty($style)) {
$config['content_style'] = "body {" . implode(' ', $style) . "}";
}
$rcmail->output->set_env('editor_config', $config);
$rcmail->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia', 'close');
if ($path = $rcmail->config->get('media_browser_css_location', 'program/resources/tinymce/browser.css')) {
if ($path != 'none' && ($path = $rcmail->find_asset($path))) {
$rcmail->output->include_css($path);
}
}
$rcmail->output->include_script('tinymce/tinymce.min.js');
$rcmail->output->include_script('editor.js');
}
/**
* File upload progress handler.
*
* @deprecated We're using HTML5 upload progress
*/
public static function upload_progress()
{
// NOOP
rcmail::get_instance()->output->send();
}
/**
* Initializes file uploading interface.
*
* @param int $max_size Optional maximum file size in bytes
*
* @return string Human-readable file size limit
*/
public static function upload_init($max_size = null)
{
$rcmail = rcmail::get_instance();
// find max filesize value
$max_filesize = rcube_utils::max_upload_size();
if ($max_size && $max_size < $max_filesize) {
$max_filesize = $max_size;
}
$max_filesize_txt = self::show_bytes($max_filesize);
$rcmail->output->set_env('max_filesize', $max_filesize);
$rcmail->output->set_env('filesizeerror', $rcmail->gettext(array(
'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize_txt))));
if ($max_filecount = ini_get('max_file_uploads')) {
$rcmail->output->set_env('max_filecount', $max_filecount);
$rcmail->output->set_env('filecounterror', $rcmail->gettext(array(
'name' => 'filecounterror', 'vars' => array('count' => $max_filecount))));
}
$rcmail->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B');
return $max_filesize_txt;
}
/**
* Upload form object
*
* @param array $attrib Object attributes
* @param string $name Form object name
* @param string $action Form action name
* @param array $input_attr File input attributes
* @param int $max_size Maximum upload size
*
* @return string HTML output
*/
public static function upload_form($attrib, $name, $action, $input_attr = [], $max_size = null)
{
$rcmail = rcmail::get_instance();
// Get filesize, enable upload progress bar
$max_filesize = self::upload_init($max_size);
$hint = html::div('hint', $rcmail->gettext(['name' => 'maxuploadsize', 'vars' => ['size' => $max_filesize]]));
if (!empty($attrib['mode']) && $attrib['mode'] == 'hint') {
return $hint;
}
// set defaults
$attrib += ['id' => 'rcmUploadbox', 'buttons' => 'yes'];
$event = rcmail_output::JS_OBJECT_NAME . ".command('$action', this.form)";
$form_id = $attrib['id'] . 'Frm';
// Default attributes of file input and form
$input_attr += [
'id' => $attrib['id'] . 'Input',
'type' => 'file',
'name' => '_attachments[]',
'class' => 'form-control',
];
$form_attr = [
'id' => $form_id,
'name' => $name,
'method' => 'post',
'enctype' => 'multipart/form-data'
];
if (!empty($attrib['mode']) && $attrib['mode'] == 'smart') {
unset($attrib['buttons']);
$form_attr['class'] = 'smart-upload';
$input_attr = array_merge($input_attr, [
// #5854: Chrome does not execute onchange when selecting the same file.
// To fix this we reset the input using null value.
'onchange' => "$event; this.value=null",
'class' => 'smart-upload',
'tabindex' => '-1',
]);
}
$input = new html_inputfield($input_attr);
$content = $attrib['prefix'] . $input->show();
if (empty($attrib['mode']) || $attrib['mode'] != 'smart') {
$content = html::div(null, $content . $hint);
}
if (self::get_bool_attr($attrib, 'buttons')) {
$button = new html_inputfield(['type' => 'button']);
$content .= html::div('buttons',
$button->show($rcmail->gettext('close'), ['class' => 'button', 'onclick' => "$('#{$attrib['id']}').hide()"])
. ' ' .
$button->show($rcmail->gettext('upload'), ['class' => 'button mainaction', 'onclick' => $event])
);
}
$rcmail->output->add_gui_object($name, $form_id);
return html::div($attrib, $rcmail->output->form_tag($form_attr, $content));
}
/**
* Common file upload error handler
*
* @param int $php_error PHP error from $_FILES
* @param array $attachment Attachment data from attachment_upload hook
* @param string $add_error Additional error label (highest prio)
*/
public static function upload_error($php_error, $attachment = null, $add_error = null)
{
$rcmail = rcmail::get_instance();
if ($add_error) {
$msg = $rcmail->gettext($add_error);
}
else if ($attachment && !empty($attachment['error'])) {
$msg = $attachment['error'];
}
else if ($php_error == UPLOAD_ERR_INI_SIZE || $php_error == UPLOAD_ERR_FORM_SIZE) {
$post_size = self::show_bytes(rcube_utils::max_upload_size());
$msg = $rcmail->gettext(['name' => 'filesizeerror', 'vars' => ['size' => $post_size]]);
}
else {
$msg = $rcmail->gettext('fileuploaderror');
}
$rcmail->output->command('display_message', $msg, 'error');
}
/**
* Common POST file upload error handler
*
* @return bool True if it was a POST request, False otherwise
*/
public static function upload_failure()
{
if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] != 'POST') {
return false;
}
$rcmail = rcmail::get_instance();
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size')) {
$msg = $rcmail->gettext([
'name' => 'filesizeerror',
'vars' => ['size' => self::show_bytes(parse_bytes($maxsize))]
]);
}
else {
$msg = $rcmail->gettext('fileuploaderror');
}
$rcmail->output->command('display_message', $msg, 'error');
return true;
}
/**
* Outputs uploaded file content (with image thumbnails support
*
* @param array $file Upload file data
*/
public static function display_uploaded_file($file)
{
if (empty($file)) {
return;
}
$rcmail = rcmail::get_instance();
$file = $rcmail->plugins->exec_hook('attachment_display', $file);
if ($file['status']) {
if (empty($file['size'])) {
$file['size'] = $file['data'] ? strlen($file['data']) : @filesize($file['path']);
}
// generate image thumbnail for file browser in HTML editor
if (!empty($_GET['_thumbnail'])) {
$thumbnail_size = 80;
$mimetype = $file['mimetype'];
$file_ident = $file['id'] . ':' . $file['mimetype'] . ':' . $file['size'];
$thumb_name = 'thumb' . md5($file_ident . ':' . $rcmail->user->ID . ':' . $thumbnail_size);
$cache_file = rcube_utils::temp_filename($thumb_name, false, false);
// render thumbnail image if not done yet
if (!is_file($cache_file)) {
if (!$file['path']) {
$orig_name = $filename = $cache_file . '.tmp';
file_put_contents($orig_name, $file['data']);
}
else {
$filename = $file['path'];
}
$image = new rcube_image($filename);
if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) {
$mimetype = 'image/' . $imgtype;
if (!empty($orig_name)) {
unlink($orig_name);
}
}
}
if (is_file($cache_file)) {
// cache for 1h
$rcmail->output->future_expire_header(3600);
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($cache_file));
readfile($cache_file);
exit;
}
}
header('Content-Type: ' . $file['mimetype']);
header('Content-Length: ' . $file['size']);
if ($file['data']) {
echo $file['data'];
}
else if ($file['path']) {
readfile($file['path']);
}
}
}
/**
* Initializes client-side autocompletion.
*/
public static function autocomplete_init()
{
static $init;
if ($init) {
return;
}
$init = 1;
$rcmail = rcmail::get_instance();
if (($threads = (int) $rcmail->config->get('autocomplete_threads')) > 0) {
$book_types = (array) $rcmail->config->get('autocomplete_addressbooks', 'sql');
if (count($book_types) > 1) {
$rcmail->output->set_env('autocomplete_threads', $threads);
$rcmail->output->set_env('autocomplete_sources', $book_types);
}
}
$rcmail->output->set_env('autocomplete_max', (int) $rcmail->config->get('autocomplete_max', 15));
$rcmail->output->set_env('autocomplete_min_length', $rcmail->config->get('autocomplete_min_length'));
$rcmail->output->add_label('autocompletechars', 'autocompletemore');
}
/**
* Returns supported font-family specifications
*
* @param string $font Font name
*
* @return string|array Font-family specification array or string (if $font is used)
*/
public static function font_defs($font = null)
{
$fonts = [
'Andale Mono' => '"Andale Mono",Times,monospace',
'Arial' => 'Arial,Helvetica,sans-serif',
'Arial Black' => '"Arial Black","Avant Garde",sans-serif',
'Book Antiqua' => '"Book Antiqua",Palatino,serif',
'Courier New' => '"Courier New",Courier,monospace',
'Georgia' => 'Georgia,Palatino,serif',
'Helvetica' => 'Helvetica,Arial,sans-serif',
'Impact' => 'Impact,Chicago,sans-serif',
'Tahoma' => 'Tahoma,Arial,Helvetica,sans-serif',
'Terminal' => 'Terminal,Monaco,monospace',
'Times New Roman' => '"Times New Roman",Times,serif',
'Trebuchet MS' => '"Trebuchet MS",Geneva,sans-serif',
'Verdana' => 'Verdana,Geneva,sans-serif',
];
if ($font) {
return !empty($fonts[$font]) ? $fonts[$font] : null;
}
return $fonts;
}
/**
* Create a human readable string for a number of bytes
*
* @param int $bytes Number of bytes
* @param string &$unit Size unit
*
* @return string Byte string
*/
public static function show_bytes($bytes, &$unit = null)
{
$rcmail = rcmail::get_instance();
// Plugins may want to display different units
$plugin = $rcmail->plugins->exec_hook('show_bytes', ['bytes' => $bytes, 'unit' => null]);
$unit = $plugin['unit'];
if (isset($plugin['result'])) {
return $plugin['result'];
}
if ($bytes >= 1073741824) {
$unit = 'GB';
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . $rcmail->gettext($unit);
}
else if ($bytes >= 1048576) {
$unit = 'MB';
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . $rcmail->gettext($unit);
}
else if ($bytes >= 1024) {
$unit = 'KB';
$str = sprintf("%d ", round($bytes/1024)) . $rcmail->gettext($unit);
}
else {
$unit = 'B';
$str = sprintf('%d ', $bytes) . $rcmail->gettext($unit);
}
return $str;
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_part $part Message part
*
* @return string Part size (and unit)
*/
public static function message_part_size($part)
{
if (isset($part->d_parameters['size'])) {
$size = self::show_bytes((int) $part->d_parameters['size']);
}
else {
$size = $part->size;
if ($size === 0) {
$part->exact_size = true;
}
if ($part->encoding == 'base64') {
$size = $size / 1.33;
}
$size = self::show_bytes($size);
}
if (!$part->exact_size) {
$size = '~' . $size;
}
return $size;
}
/**
* Returns message UID(s) and IMAP folder(s) from GET/POST data
*
* @param string $uids UID value to decode
* @param string $mbox Default mailbox value (if not encoded in UIDs)
* @param bool $is_multifolder Will be set to True if multi-folder request
* @param int $mode Request mode. Default: rcube_utils::INPUT_GPC.
*
* @return array List of message UIDs per folder
*/
public static function get_uids($uids = null, $mbox = null, &$is_multifolder = false, $mode = null)
{
// message UID (or comma-separated list of IDs) is provided in
// the form of <ID>-<MBOX>[,<ID>-<MBOX>]*
$_uid = $uids ?: rcube_utils::get_input_value('_uid', $mode ?: rcube_utils::INPUT_GPC);
$_mbox = $mbox ?: (string) rcube_utils::get_input_value('_mbox', $mode ?: rcube_utils::INPUT_GPC);
// already a hash array
if (is_array($_uid) && !isset($_uid[0])) {
return $_uid;
}
$result = [];
// special case: *
if ($_uid == '*' && is_object($_SESSION['search'][1]) && $_SESSION['search'][1]->multi) {
$is_multifolder = true;
// extract the full list of UIDs per folder from the search set
foreach ($_SESSION['search'][1]->sets as $subset) {
$mbox = $subset->get_parameters('MAILBOX');
$result[$mbox] = $subset->get();
}
}
else {
if (is_string($_uid)) {
$_uid = explode(',', $_uid);
}
// create a per-folder UIDs array
foreach ((array) $_uid as $uid) {
$tokens = explode('-', $uid, 2);
$uid = $tokens[0];
if (!isset($tokens[1]) || !strlen($tokens[1])) {
$mbox = $_mbox;
}
else {
$mbox = $tokens[1];
$is_multifolder = true;
}
if ($uid == '*') {
$result[$mbox] = $uid;
}
else if (preg_match('/^[0-9:.]+$/', $uid)) {
$result[$mbox][] = $uid;
}
}
}
return $result;
}
/**
* Get resource file content (with assets_dir support)
*
* @param string $name File name
*
* @return string File content
*/
public static function get_resource_content($name)
{
if (!strpos($name, '/')) {
$name = "program/resources/$name";
}
$assets_dir = rcmail::get_instance()->config->get('assets_dir');
if ($assets_dir) {
$path = slashify($assets_dir) . $name;
if (@file_exists($path)) {
$name = $path;
}
}
return file_get_contents($name, false);
}
public static function get_form_tags($attrib, $action, $id = null, $hidden = null)
{
static $edit_form;
$rcmail = rcmail::get_instance();
$form_start = $form_end = '';
if (empty($edit_form)) {
$request_key = $action . (isset($id) ? '.'.$id : '');
$form_start = $rcmail->output->request_form([
'name' => 'form',
'method' => 'post',
'task' => $rcmail->task,
'action' => $action,
'request' => $request_key,
'noclose' => true
] + $attrib
);
if (is_array($hidden)) {
$hiddenfields = new html_hiddenfield($hidden);
$form_start .= $hiddenfields->show();
}
- $form_end = !strlen($attrib['form']) ? '</form>' : '';
+ $form_end = empty($attrib['form']) ? '</form>' : '';
$edit_form = !empty($attrib['form']) ? $attrib['form'] : 'form';
$rcmail->output->add_gui_object('editform', $edit_form);
}
return array($form_start, $form_end);
}
/**
* Return folders list in HTML
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public static function folder_list($attrib)
{
static $a_mailboxes;
$attrib += ['maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)'];
$type = !empty($attrib['type']) ? $attrib['type'] : 'ul';
unset($attrib['type']);
if ($type == 'ul' && empty($attrib['id'])) {
$attrib['id'] = 'rcmboxlist';
}
if (empty($attrib['folder_name'])) {
$attrib['folder_name'] = '*';
}
// get current folder
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox_name = $storage->get_folder();
$delimiter = $storage->get_hierarchy_delimiter();
// build the folders tree
if (empty($a_mailboxes)) {
// get mailbox list
$a_mailboxes = array();
$a_folders = $storage->list_folders_subscribed(
'',
$attrib['folder_name'],
isset($attrib['folder_filter']) ? $attrib['folder_filter'] : null
);
foreach ($a_folders as $folder) {
self::build_folder_tree($a_mailboxes, $folder, $delimiter);
}
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $rcmail->plugins->exec_hook('render_mailboxlist', [
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'type' => $type,
'attribs' => $attrib,
]);
$a_mailboxes = $hook['list'];
$attrib = $hook['attribs'];
if ($type == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
// add no-selection option
if (!empty($attrib['noselection'])) {
$select->add(html::quote($rcmail->gettext($attrib['noselection'])), '');
}
$maxlength = isset($attrib['maxlength']) ? $attrib['maxlength'] : null;
$realnames = isset($attrib['realnames']) ? $attrib['realnames'] : null;
$default = isset($attrib['default']) ? $attrib['default'] : null;
self::render_folder_tree_select($a_mailboxes, $mbox_name, $maxlength, $select, $realnames);
$out = $select->show($default);
}
else {
$out = '';
$js_mailboxlist = [];
$tree = self::render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
if ($type != 'js') {
$out = html::tag('ul', $attrib, $tree, html::$common_attrib);
$rcmail->output->include_script('treelist.js');
$rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
$rcmail->output->set_env('unreadwrap', isset($attrib['unreadwrap']) ? $attrib['unreadwrap'] : false);
$rcmail->output->set_env('collapsed_folders', (string) $rcmail->config->get('collapsed_folders'));
}
$rcmail->output->set_env('mailboxes', $js_mailboxlist);
// we can't use object keys in javascript because they are unordered
// we need sorted folders list for folder-selector widget
$rcmail->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
}
// add some labels to client
$rcmail->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
return $out;
}
/**
* Return folders list as html_select object
*
* @param array $p Named parameters
*
* @return html_select HTML drop-down object
*/
public static function folder_selector($p = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$realnames = $rcmail->config->get('show_real_foldernames');
$p += ['maxlength' => 100, 'realnames' => $realnames, 'is_escaped' => true];
$a_mailboxes = [];
if (empty($p['folder_name'])) {
$p['folder_name'] = '*';
}
+ $f_filter = isset($p['folder_filter']) ? $p['folder_filter'] : null;
+ $f_rights = isset($p['folder_rights']) ? $p['folder_rights'] : null;
+
if ($p['unsubscribed']) {
- $list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
+ $list = $storage->list_folders('', $p['folder_name'], $f_filter, $f_rights);
}
else {
- $list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
+ $list = $storage->list_folders_subscribed('', $p['folder_name'], $f_filter, $f_rights);
}
$delimiter = $storage->get_hierarchy_delimiter();
if (!empty($p['exceptions'])) {
$list = array_diff($list, (array) $p['exceptions']);
}
if (!empty($p['additional'])) {
foreach ($p['additional'] as $add_folder) {
$add_items = explode($delimiter, $add_folder);
$folder = '';
while (count($add_items)) {
$folder .= array_shift($add_items);
// @TODO: sorting
if (!in_array($folder, $list)) {
$list[] = $folder;
}
$folder .= $delimiter;
}
}
}
foreach ($list as $folder) {
self::build_folder_tree($a_mailboxes, $folder, $delimiter);
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $rcmail->plugins->exec_hook('render_folder_selector', [
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'attribs' => $p,
]);
$a_mailboxes = $hook['list'];
$p = $hook['attribs'];
$select = new html_select($p);
if ($p['noselection']) {
$select->add(html::quote($p['noselection']), '');
}
self::render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
return $select;
}
/**
* Create a hierarchical array of the mailbox list
*/
protected static function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
// Handle namespace prefix
$prefix = '';
if (!$path) {
$n_folder = $folder;
$folder = $storage->mod_folder($folder);
if ($n_folder != $folder) {
$prefix = substr($n_folder, 0, -strlen($folder));
}
}
$pos = strpos($folder, $delm);
if ($pos !== false) {
$subFolders = substr($folder, $pos+1);
$currentFolder = substr($folder, 0, $pos);
// sometimes folder has a delimiter as the last character
if (!strlen($subFolders)) {
$virtual = false;
}
else if (!isset($arrFolders[$currentFolder])) {
$virtual = true;
}
else {
$virtual = $arrFolders[$currentFolder]['virtual'];
}
}
else {
$subFolders = false;
$currentFolder = $folder;
$virtual = false;
}
$path .= $prefix . $currentFolder;
if (!isset($arrFolders[$currentFolder])) {
$arrFolders[$currentFolder] = [
'id' => $path,
'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
'virtual' => $virtual,
'folders' => []
];
}
else {
$arrFolders[$currentFolder]['virtual'] = $virtual;
}
if (strlen($subFolders)) {
self::build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
}
}
/**
* Return html for a structured list &lt;ul&gt; for the mailbox tree
*/
protected static function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$maxlength = intval($attrib['maxlength']);
$realnames = (bool)$attrib['realnames'];
$msgcounts = $storage->get_cache('messagecount');
$collapsed = $rcmail->config->get('collapsed_folders');
$realnames = $rcmail->config->get('show_real_foldernames');
$out = '';
foreach ($arrFolders as $folder) {
$title = null;
$folder_class = self::folder_classname($folder['id']);
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
if ($folder_class && !$realnames && $rcmail->text_exists($folder_class)) {
$foldername = $rcmail->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$fname = abbreviate_string($foldername, $maxlength);
if ($fname != $foldername) {
$title = $foldername;
}
$foldername = $fname;
}
}
// make folder name safe for ids and class names
$folder_id = rcube_utils::html_identifier($folder['id'], true);
$classes = ['mailbox'];
// set special class for Sent, Drafts, Trash and Junk
if ($folder_class) {
$classes[] = $folder_class;
}
if ($folder['id'] == $mbox_name) {
$classes[] = 'selected';
}
if ($folder['virtual']) {
$classes[] = 'virtual';
}
else if ($unread) {
$classes[] = 'unread';
}
$js_name = rcube::JQ($folder['id']);
$html_name = rcube::Q($foldername) . ($unread ? html::span('unreadcount skip-content', sprintf($attrib['unreadwrap'], $unread)) : '');
$link_attrib = $folder['virtual'] ? [] : [
'href' => $rcmail->url(['_mbox' => $folder['id']]),
'onclick' => sprintf("return %s.command('list','%s',this,event)", rcmail_output::JS_OBJECT_NAME, $js_name),
'rel' => $folder['id'],
'title' => $title,
];
$out .= html::tag('li', [
'id' => "rcmli" . $folder_id,
'class' => implode(' ', $classes),
'noclose' => true
],
html::a($link_attrib, $html_name)
);
if (!empty($folder['folders'])) {
$out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
}
$jslist[$folder['id']] = [
'id' => $folder['id'],
'name' => $foldername,
'virtual' => $folder['virtual'],
];
if (!empty($folder_class)) {
$jslist[$folder['id']]['class'] = $folder_class;
}
if (!empty($folder['folders'])) {
$out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
self::render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
}
$out .= "</li>\n";
}
return $out;
}
/**
* Return html for a flat list <select> for the mailbox tree
*/
protected static function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
{
$out = '';
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
foreach ($arrFolders as $folder) {
// skip exceptions (and its subfolders)
if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
continue;
}
// skip folders in which it isn't possible to create subfolders
if (!empty($opts['skip_noinferiors'])) {
$attrs = $storage->folder_attributes($folder['id']);
if ($attrs && in_array_nocase('\\Noinferiors', $attrs)) {
continue;
}
}
if (!$realnames && ($folder_class = self::folder_classname($folder['id'])) && $rcmail->text_exists($folder_class)) {
$foldername = $rcmail->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$foldername = abbreviate_string($foldername, $maxlength);
}
}
$select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
if (!empty($folder['folders'])) {
$out .= self::render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
$select, $realnames, $nestLevel+1, $opts);
}
}
return $out;
}
/**
* Returns class name for the given folder if it is a special folder
* (including shared/other users namespace roots).
*
* @param string $folder_id IMAP Folder name
*
* @return string|null CSS class name
*/
public static function folder_classname($folder_id)
{
static $classes;
if ($classes === null) {
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$classes = ['INBOX' => 'inbox'];
// for these mailboxes we have css classes
foreach (['sent', 'drafts', 'trash', 'junk'] as $type) {
if (($mbox = $rcmail->config->get($type . '_mbox')) && !isset($classes[$mbox])) {
$classes[$mbox] = $type;
}
}
// add classes for shared/other user namespace roots
foreach (['other', 'shared'] as $ns_name) {
if ($ns = $storage->get_namespace($ns_name)) {
foreach ($ns as $root) {
$root = substr($root[0], 0, -1);
if (strlen($root) && !isset($classes[$root])) {
$classes[$root] = "ns-$ns_name";
}
}
}
}
}
return !empty($classes[$folder_id]) ? $classes[$folder_id] : null;
}
/**
* Try to localize the given IMAP folder name.
* UTF-7 decode it in case no localized text was found
*
* @param string $name Folder name
* @param bool $with_path Enable path localization
* @param bool $path_remove Remove the path
*
* @return string Localized folder name in UTF-8 encoding
*/
public static function localize_foldername($name, $with_path = false, $path_remove = false)
{
$rcmail = rcmail::get_instance();
$realnames = $rcmail->config->get('show_real_foldernames');
if (!$realnames && ($folder_class = self::folder_classname($name)) && $rcmail->text_exists($folder_class)) {
return $rcmail->gettext($folder_class);
}
$storage = $rcmail->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
// Remove the path
if ($path_remove) {
if (strpos($name, $delimiter)) {
$path = explode($delimiter, $name);
$name = array_pop($path);
}
}
// try to localize path of the folder
else if ($with_path && !$realnames) {
$path = explode($delimiter, $name);
$count = count($path);
if ($count > 1) {
for ($i = 1; $i < $count; $i++) {
$folder = implode($delimiter, array_slice($path, 0, -$i));
$folder_class = self::folder_classname($folder);
if ($folder_class && $rcmail->text_exists($folder_class)) {
$name = implode($delimiter, array_slice($path, $count - $i));
$name = rcube_charset::convert($name, 'UTF7-IMAP');
return $rcmail->gettext($folder_class) . $delimiter . $name;
}
}
}
}
return rcube_charset::convert($name, 'UTF7-IMAP');
}
/**
* Localize folder path
*/
public static function localize_folderpath($path)
{
$rcmail = rcmail::get_instance();
$protect_folders = $rcmail->config->get('protect_default_folders');
$delimiter = $rcmail->storage->get_hierarchy_delimiter();
$path = explode($delimiter, $path);
$result = [];
foreach ($path as $idx => $dir) {
$directory = implode($delimiter, array_slice($path, 0, $idx+1));
if ($protect_folders && $rcmail->storage->is_special_folder($directory)) {
unset($result);
$result[] = self::localize_foldername($directory);
}
else {
$result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
}
}
return implode($delimiter, $result);
}
/**
* Gets a value of a boolean attribute from template object attributes
*
* @param array $attributes Template object attributes
* @param string $name Attribute name
*/
public static function get_bool_attr($attributes, $name)
{
if (!isset($attributes[$name])) {
return false;
}
return rcube_utils::get_boolean($attributes[$name]);
}
}
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index 2c9c290b3..5fcb2c4c9 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2633 +1,2633 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to handle HTML page output using a skin template. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create HTML page output using a skin template
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_html extends rcmail_output
{
public $type = 'html';
protected $message;
protected $template_name;
protected $objects = array();
protected $js_env = array();
protected $js_labels = array();
protected $js_commands = array();
protected $skin_paths = array();
protected $skin_name = '';
protected $scripts_path = '';
protected $script_files = array();
protected $css_files = array();
protected $scripts = array();
protected $meta_tags = array();
protected $link_tags = array('shortcut icon' => '');
protected $header = '';
protected $footer = '';
protected $body = '';
protected $base_path = '';
protected $assets_path;
protected $assets_dir = RCUBE_INSTALL_PATH;
protected $devel_mode = false;
protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
// deprecated names of templates used before 0.5
protected $deprecated_templates = array(
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
);
// deprecated names of template objects used before 1.4
protected $deprecated_template_objects = array(
'addressframe' => 'contentframe',
'messagecontentframe' => 'contentframe',
'prefsframe' => 'contentframe',
'folderframe' => 'contentframe',
'identityframe' => 'contentframe',
'responseframe' => 'contentframe',
'keyframe' => 'contentframe',
'filterframe' => 'contentframe',
);
/**
* Constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = $this->config->get('devel_mode');
$this->set_env('task', $task);
$this->set_env('standard_windows', (bool) $this->config->get('standard_windows'));
$this->set_env('locale', !empty($_SESSION['language']) ? $_SESSION['language'] : 'en_US');
$this->set_env('devel_mode', $this->devel_mode);
// Version number e.g. 1.4.2 will be 10402
$version = explode('.', preg_replace('/[^0-9.].*/', '', RCMAIL_VERSION));
$this->set_env('rcversion', $version[0] * 10000 + $version[1] * 100 + (isset($version[2]) ? $version[2] : 0));
// add cookie info
$this->set_env('cookie_domain', ini_get('session.cookie_domain'));
$this->set_env('cookie_path', ini_get('session.cookie_path'));
$this->set_env('cookie_secure', filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN));
// Easy way to change skin via GET argument, for developers
if ($this->devel_mode && !empty($_GET['skin']) && preg_match('/^[a-z0-9-_]+$/i', $_GET['skin'])) {
if ($this->check_skin($_GET['skin'])) {
$this->set_skin($_GET['skin']);
$this->app->user->save_prefs(array('skin' => $_GET['skin']));
}
}
// load and setup the skin
$this->set_skin($this->config->get('skin'));
$this->set_assets_path($this->config->get('assets_path'), $this->config->get('assets_dir'));
if (!empty($_REQUEST['_extwin']))
$this->set_env('extwin', 1);
if ($this->framed || $framed)
$this->set_env('framed', 1);
$lic = <<<EOF
/*
@licstart The following is the entire license notice for the
JavaScript code in this page.
Copyright (C) The Roundcube Dev Team
The JavaScript code in this page is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
The code is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU GPL for more details.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
EOF;
// add common javascripts
$this->add_script($lic, 'head_top');
$this->add_script('var '.self::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
// don't wait for page onload. Call init at the bottom of the page (delayed)
$this->add_script(self::JS_OBJECT_NAME.'.init();', 'docready');
$this->scripts_path = 'program/js/';
$this->include_script('jquery.min.js');
$this->include_script('common.js');
$this->include_script('app.js');
// register common UI objects
$this->add_handlers(array(
'loginform' => array($this, 'login_form'),
'preloader' => array($this, 'preloader'),
'username' => array($this, 'current_username'),
'message' => array($this, 'message_container'),
'charsetselector' => array($this, 'charset_selector'),
'aboutcontent' => array($this, 'about_content'),
));
// set blankpage (watermark) url
$blankpage = $this->config->get('blankpage_url', '/watermark.html');
$this->set_env('blankpage', $blankpage);
}
/**
* Set environment variable
*
* @param string $name Property name
* @param mixed $value Property value
* @param boolean $addtojs True if this property should be added
* to client environment
*/
public function set_env($name, $value, $addtojs = true)
{
$this->env[$name] = $value;
if ($addtojs || isset($this->js_env[$name])) {
$this->js_env[$name] = $value;
}
}
/**
* Parse and set assets path
*
* @param string $path Assets path URL (relative or absolute)
* @param string $fs_dif Assets path in filesystem
*/
public function set_assets_path($path, $fs_dir = null)
{
if (empty($path)) {
return;
}
$path = rtrim($path, '/') . '/';
// handle relative assets path
if (!preg_match('|^https?://|', $path) && $path[0] != '/') {
// save the path to search for asset files later
$this->assets_dir = $path;
$base = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
$base = rtrim($base, '/');
// remove url token if exists
if ($len = intval($this->config->get('use_secure_urls'))) {
$_base = explode('/', $base);
$last = count($_base) - 1;
$length = $len > 1 ? $len : 16; // as in rcube::get_secure_url_token()
// we can't use real token here because it
// does not exists in unauthenticated state,
// hope this will not produce false-positive matches
if ($last > -1 && preg_match('/^[a-f0-9]{' . $length . '}$/', $_base[$last])) {
$path = '../' . $path;
}
}
}
// set filesystem path for assets
if ($fs_dir) {
if ($fs_dir[0] != '/') {
$fs_dir = realpath(RCUBE_INSTALL_PATH . $fs_dir);
}
// ensure the path ends with a slash
$this->assets_dir = rtrim($fs_dir, '/') . '/';
}
$this->assets_path = $path;
$this->set_env('assets_path', $path);
}
/**
* Getter for the current page title
*
* @param bool $full Prepend title with product/user name
*
* @return string The page title
*/
protected function get_pagetitle($full = true)
{
if (!empty($this->pagetitle)) {
$title = $this->pagetitle;
}
else if (isset($this->env['task'])) {
if ($this->env['task'] == 'login') {
$title = $this->app->gettext(array(
'name' => 'welcome',
'vars' => array('product' => $this->config->get('product_name')
)));
}
else {
$title = ucfirst($this->env['task']);
}
}
else {
$title = '';
}
if ($full) {
if ($this->devel_mode && !empty($_SESSION['username'])) {
$title = $_SESSION['username'] . ' :: ' . $title;
}
else if ($prod_name = $this->config->get('product_name')) {
$title = $prod_name . ' :: ' . $title;
}
}
return $title;
}
/**
* Getter for the current skin path property
*/
public function get_skin_path()
{
return $this->skin_paths[0];
}
/**
* Set skin
*
* @param string $skin Skin name
*/
public function set_skin($skin)
{
if (!$this->check_skin($skin)) {
// If the skin does not exist (could be removed or invalid),
// fallback to the skin set in the system configuration (#7271)
$skin = $this->config->system_skin;
}
$skin_path = 'skins/' . $skin;
$this->config->set('skin_path', $skin_path);
$this->base_path = $skin_path;
// register skin path(s)
$this->skin_paths = array();
$this->skins = array();
$this->load_skin($skin_path);
$this->skin_name = $skin;
$this->set_env('skin', $skin);
}
/**
* Check skin validity/existence
*
* @param string $skin Skin name
*
* @return bool True if the skin exist and is readable, False otherwise
*/
public function check_skin($skin)
{
// Sanity check to prevent from path traversal vulnerability (#1490620)
if (strpos($skin, '/') !== false || strpos($skin, "\\") !== false) {
rcube::raise_error(array(
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Invalid skin name'
), true, false);
return false;
}
$skins_allowed = $this->config->get('skins_allowed');
if (!empty($skins_allowed) && !in_array($skin, (array) $skins_allowed)) {
return false;
}
$path = RCUBE_INSTALL_PATH . 'skins/';
return !empty($skin) && is_dir($path . $skin) && is_readable($path . $skin);
}
/**
* Helper method to recursively read skin meta files and register search paths
*/
private function load_skin($skin_path)
{
$this->skin_paths[] = $skin_path;
// read meta file and check for dependencies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
$meta['path'] = $skin_path;
$path_elements = explode('/', $skin_path);
$skin_id = end($path_elements);
if (empty($meta['name'])) {
$meta['name'] = $skin_id;
}
$this->skins[$skin_id] = $meta;
// Keep skin config for ajax requests (#6613)
$_SESSION['skin_config'] = array();
if (!empty($meta['extends'])) {
$path = RCUBE_INSTALL_PATH . 'skins/';
if (is_dir($path . $meta['extends']) && is_readable($path . $meta['extends'])) {
$_SESSION['skin_config'] = $this->load_skin('skins/' . $meta['extends']);
}
}
if (!empty($meta['config'])) {
foreach ($meta['config'] as $key => $value) {
$this->config->set($key, $value, true);
$_SESSION['skin_config'][$key] = $value;
}
$value = array_merge((array) $this->config->get('dont_override'), array_keys($meta['config']));
$this->config->set('dont_override', $value, true);
}
if (!empty($meta['localization'])) {
$locdir = $meta['localization'] === true ? 'localization' : $meta['localization'];
if ($texts = $this->app->read_localization(RCUBE_INSTALL_PATH . $skin_path . '/' . $locdir)) {
$this->app->load_language($_SESSION['language'], $texts);
}
}
// Use array_merge() here to allow for global default and extended skins
if (!empty($meta['meta'])) {
$this->meta_tags = array_merge($this->meta_tags, (array) $meta['meta']);
}
if (!empty($meta['links'])) {
$this->link_tags = array_merge($this->link_tags, (array) $meta['links']);
}
return $_SESSION['skin_config'];
}
/**
* Check if a specific template exists
*
* @param string $name Template name
*
* @return boolean True if template exists
*/
public function template_exists($name)
{
foreach ($this->skin_paths as $skin_path) {
$filename = RCUBE_INSTALL_PATH . $skin_path . '/templates/' . $name . '.html';
if ((is_file($filename) && is_readable($filename))
|| ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name]))
) {
return true;
}
}
return false;
}
/**
* Find the given file in the current skin path stack
*
* @param string $file File name/path to resolve (starting with /)
* @param string &$skin_path Reference to the base path of the matching skin
* @param string $add_path Additional path to search in
* @param bool $minified Fallback to a minified version of the file
*
* @return mixed Relative path to the requested file or False if not found
*/
public function get_skin_file($file, &$skin_path = null, $add_path = null, $minified = false)
{
$skin_paths = $this->skin_paths;
if ($add_path) {
array_unshift($skin_paths, $add_path);
$skin_paths = array_unique($skin_paths);
}
if ($skin_path = $this->find_file_path($file, $skin_paths)) {
return $skin_path . $file;
}
if ($minified && preg_match('/(?<!\.min)\.(js|css)$/', $file)) {
$file = preg_replace('/\.(js|css)$/', '.min.\\1', $file);
if ($skin_path = $this->find_file_path($file, $skin_paths)) {
return $skin_path . $file;
}
}
return false;
}
/**
* Find path of the asset file
*/
protected function find_file_path($file, $skin_paths)
{
foreach ($skin_paths as $skin_path) {
if ($this->assets_dir != RCUBE_INSTALL_PATH) {
if (realpath($this->assets_dir . $skin_path . $file)) {
return $skin_path;
}
}
if (realpath(RCUBE_INSTALL_PATH . $skin_path . $file)) {
return $skin_path;
}
}
}
/**
* Register a GUI object to the client script
*
* @param string $obj Object name
* @param string $id Object ID
*/
public function add_gui_object($obj, $id)
{
$this->add_script(self::JS_OBJECT_NAME.".gui_object('$obj', '$id');");
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
public function command()
{
$cmd = func_get_args();
if (strpos($cmd[0], 'plugin.') !== false)
$this->js_commands[] = array('triggerEvent', $cmd[0], $cmd[1]);
else
$this->js_commands[] = $cmd;
}
/**
* Add a localized label to the client environment
*/
public function add_label()
{
$args = func_get_args();
if (count($args) == 1 && is_array($args[0])) {
$args = $args[0];
}
foreach ($args as $name) {
$this->js_labels[$name] = $this->app->gettext($name);
}
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message display time in seconds
*
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
{
if ($override || !$this->message) {
if ($this->app->text_exists($message)) {
if (!empty($vars)) {
$vars = array_map(array('rcube','Q'), $vars);
}
$msgtext = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
else {
$msgtext = $message;
}
$this->message = $message;
$this->command('display_message', $msgtext, $type, $timeout * 1000);
}
}
/**
* Delete all stored env variables and commands
*
* @param bool $all Reset all env variables (including internal)
*/
public function reset($all = false)
{
$framed = $this->framed;
$task = isset($this->env['task']) ? $this->env['task'] : '';
$env = $all ? null : array_intersect_key($this->env, array('extwin' => 1, 'framed' => 1));
// keep jQuery-UI files
$css_files = $script_files = array();
foreach ($this->css_files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$css_files[] = $file;
}
}
foreach ($this->script_files as $position => $files) {
foreach ($files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$script_files[$position][] = $file;
}
}
}
parent::reset();
// let some env variables survive
$this->env = $this->js_env = $env;
$this->framed = $framed || !empty($this->env['framed']);
$this->js_labels = array();
$this->js_commands = array();
$this->scripts = array();
$this->header = '';
$this->footer = '';
$this->body = '';
$this->css_files = array();
$this->script_files = array();
// load defaults
if (!$all) {
$this->__construct();
}
// Note: we merge jQuery-UI scripts after jQuery...
$this->css_files = array_merge($this->css_files, $css_files);
$this->script_files = array_merge_recursive($this->script_files, $script_files);
$this->set_env('orig_task', $task);
}
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
* @param bool $secure Redirect to secure location (see rcmail::url())
*/
public function redirect($p = array(), $delay = 1, $secure = false)
{
if (!empty($this->env['extwin'])) {
$p['extwin'] = 1;
}
$location = $this->app->url($p, false, false, $secure);
$this->header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin template.
*
* @param string $templ Template name
* @param boolean $exit True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
if ($templ != 'iframe') {
// prevent from endless loops
if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {
rcube::raise_error(array('code' => 505, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => 'Recursion alert: ignoring output->send()'), true, false);
return;
}
$this->parse($templ, false);
}
else {
$this->framed = true;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
if (!empty($this->script_files)) {
$this->set_env('request_token', $this->app->get_request_token());
}
// Fix assets path on blankpage
if (!empty($this->js_env['blankpage'])) {
$this->js_env['blankpage'] = $this->asset_url($this->abs_url($this->js_env['blankpage'], true));
}
$commands = $this->get_js_commands($framed);
// if all js commands go to parent window we can ignore all
// script files and skip rcube_webmail initialization (#1489792)
// but not on error pages where skins may need jQuery, etc.
if ($framed && empty($this->js_env['server_error'])) {
$this->scripts = array();
$this->script_files = array();
$this->header = '';
$this->footer = '';
}
// write all javascript commands
if (!empty($commands)) {
$this->add_script($commands, 'head_top');
}
$this->page_headers();
// call super method
$this->_write($template);
}
/**
* Send common page headers
* For now it only (re)sets X-Frame-Options when needed
*/
public function page_headers()
{
if (headers_sent()) {
return;
}
// allow (legal) iframe content to be loaded
$framed = $this->framed || !empty($this->env['framed']);
if ($framed && ($xopt = $this->app->config->get('x_frame_options', 'sameorigin'))) {
if (strtolower($xopt) === 'deny') {
$this->header('X-Frame-Options: sameorigin', true);
}
}
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string $name Template name
* @param boolean $exit Exit script
* @param boolean $write Don't write to stdout, return parsed content instead
*
* @link http://php.net/manual/en/function.exit.php
*/
function parse($name = 'main', $exit = true, $write = true)
{
$plugin = false;
$realname = $name;
$skin_dir = '';
$plugin_skin_paths = array();
$this->template_name = $realname;
$temp = explode('.', $name, 2);
if (count($temp) > 1) {
$plugin = $temp[0];
$name = $temp[1];
$skin_dir = $plugin . '/skins/' . $this->config->get('skin');
// apply skin search escalation list to plugin directory
foreach ($this->skin_paths as $skin_path) {
$plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path;
}
// prepend plugin skin paths to search list
$this->skin_paths = array_merge($plugin_skin_paths, $this->skin_paths);
}
// find skin template
$path = false;
foreach ($this->skin_paths as $skin_path) {
// when requesting a plugin template ignore global skin path(s)
if ($plugin && strpos($skin_path, $this->app->plugins->url) !== 0) {
continue;
}
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$name.html";
// fallback to deprecated template names
if (!is_readable($path) && ($dname = $this->deprecated_templates[$realname])) {
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$dname.html";
if (is_readable($path)) {
rcube::raise_error(array(
'code' => 502, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Using deprecated template '$dname' in $skin_path/templates. Please rename to '$realname'"
), true, false);
}
}
if (is_readable($path)) {
$this->config->set('skin_path', $skin_path);
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$skin_dir = preg_replace('!^plugins/!', '', $skin_path);
break;
}
else {
$path = false;
}
}
// read template file
if (!$path || ($templ = @file_get_contents($path)) === false) {
rcube::raise_error(array(
'code' => 404,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$realname
), true, $write);
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
return false;
}
// replace all path references to plugins/... with the configured plugins dir
// and /this/ to the current plugin skin directory
if ($plugin) {
$templ = preg_replace(
array('/\bplugins\//', '/(["\']?)\/this\//'),
array($this->app->plugins->url, '\\1'.$this->app->plugins->url.$skin_dir.'/'),
$templ
);
}
// parse for specialtags
$output = $this->parse_conditions($templ);
$output = $this->parse_xml($output);
// trigger generic hook where plugins can put additional content to the page
$hook = $this->app->plugins->exec_hook("render_page", array(
'template' => $realname, 'content' => $output, 'write' => $write));
// save some memory
$output = $hook['content'];
unset($hook['content']);
// remove plugin skin paths from current context
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
if (!$write) {
return $this->postrender($output);
}
$this->write(trim($output));
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*/
protected function get_js_commands(&$framed = null)
{
$out = '';
$parent_commands = 0;
$parent_prefix = '';
$top_commands = array();
// these should be always on top,
// e.g. hide_message() below depends on env.framed
if (!$this->framed && !empty($this->js_env)) {
$top_commands[] = array('set_env', $this->js_env);
}
if (!empty($this->js_labels)) {
$top_commands[] = array('add_label', $this->js_labels);
}
// unlock interface after iframe load
$unlock = isset($_REQUEST['_unlock']) ? preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']) : 0;
if ($this->framed) {
$top_commands[] = array('iframe_loaded', $unlock);
}
else if ($unlock) {
$top_commands[] = array('hide_message', $unlock);
}
$commands = array_merge($top_commands, $this->js_commands);
foreach ($commands as $i => $args) {
$method = array_shift($args);
$parent = $this->framed || preg_match('/^parent\./', $method);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg, $this->devel_mode);
}
if ($parent) {
$parent_commands++;
$method = preg_replace('/^parent\./', '', $method);
$parent_prefix = 'if (window.parent && parent.' . self::JS_OBJECT_NAME . ') parent.';
$method = $parent_prefix . self::JS_OBJECT_NAME . '.' . $method;
}
else {
$method = self::JS_OBJECT_NAME . '.' . $method;
}
$out .= sprintf("%s(%s);\n", $method, implode(',', $args));
}
$framed = $parent_prefix && $parent_commands == count($commands);
// make the output more compact if all commands go to parent window
if ($framed) {
$out = "if (window.parent && parent." . self::JS_OBJECT_NAME . ") {\n"
. str_replace($parent_prefix, "\tparent.", $out)
. "}\n";
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string $str Input string
* @param bool $search_path True if URL should be resolved using the current skin path stack
*
* @return string URL
*/
public function abs_url($str, $search_path = false)
{
if ($str[0] == '/') {
if ($search_path && ($file_url = $this->get_skin_file($str))) {
return $file_url;
}
return $this->base_path . $str;
}
return $str;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
$args = [
'code' => $code,
'message' => $message,
];
$page = new rcmail_action_utils_error;
$page->run($args);
}
/**
* Modify path by adding URL prefix if configured
*
* @param string $path Asset path
* @param bool $abs_url Pass to self::abs_url() first
*
* @return string Asset path
*/
public function asset_url($path, $abs_url = false)
{
// iframe content can't be in a different domain
// @TODO: check if assests are on a different domain
if ($abs_url) {
$path = $this->abs_url($path, true);
}
if (!$this->assets_path || in_array($path[0], array('?', '/', '.')) || strpos($path, '://')) {
return $path;
}
return $this->assets_path . $path;
}
/***** Template parsing methods *****/
/**
* Replace all strings ($varname)
* with the content of the according global variable.
*/
protected function parse_with_globals($input)
{
$GLOBALS['__version'] = html::quote(RCMAIL_VERSION);
$GLOBALS['__comm_path'] = html::quote($this->app->comm_path);
$GLOBALS['__skin_path'] = html::quote($this->base_path);
return preg_replace_callback('/\$(__[a-z0-9_\-]+)/',
array($this, 'globals_callback'), $input);
}
/**
* Callback function for preg_replace_callback() in parse_with_globals()
*/
protected function globals_callback($matches)
{
return $GLOBALS[$matches[1]];
}
/**
* Correct absolute paths in images and other tags (add cache busters)
*/
protected function fix_paths($output)
{
return preg_replace_callback(
'!(src|href|background|data-src-[a-z]+)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
array($this, 'file_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_paths()
*
* @return string Parsed string
*/
protected function file_callback($matches)
{
$file = $matches[3];
$file = preg_replace('!^/this/!', '/', $file);
// correct absolute paths
if ($file[0] == '/') {
$this->get_skin_file($file, $skin_path, $this->base_path);
$file = ($skin_path ?: $this->base_path) . $file;
}
// add file modification timestamp
if (preg_match('/\.(js|css|less|ico|png|svg|jpeg)$/', $file)) {
$file = $this->file_mod($file);
}
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Correct paths of asset files according to assets_path
*/
protected function fix_assets_paths($output)
{
return preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.?=-]+)(["\'\s>])!i',
array($this, 'assets_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_assets_paths()
*
* @return string Parsed string
*/
protected function assets_callback($matches)
{
$file = $this->asset_url($matches[3]);
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Modify file by adding mtime indicator
*/
protected function file_mod($file)
{
$fs = false;
$ext = substr($file, strrpos($file, '.') + 1);
// use minified file if exists (not in development mode)
if (!$this->devel_mode && !preg_match('/\.min\.' . $ext . '$/', $file)) {
$minified_file = substr($file, 0, strlen($ext) * -1) . 'min.' . $ext;
if ($fs = @filemtime($this->assets_dir . $minified_file)) {
return $minified_file . '?s=' . $fs;
}
}
if ($fs = @filemtime($this->assets_dir . $file)) {
$file .= '?s=' . $fs;
}
return $file;
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input Template content
*
* @return string
* @uses rcmail_output_html::parse_xml()
* @since 0.1-rc1
*/
public function just_parse($input)
{
$input = $this->parse_conditions($input);
$input = $this->parse_xml($input);
$input = $this->postrender($input);
return $input;
}
/**
* Parse for conditional tags
*/
protected function parse_conditions($input)
{
while (preg_match('/<roundcube:if\s+[^>]+>(((?!<roundcube:(if|endif)).)*)<roundcube:endif[^>]*>/is', $input, $conditions)) {
$result = $this->eval_condition($conditions[0]);
$input = str_replace($conditions[0], $result, $input);
}
return $input;
}
/**
* Process & evaluate conditional tags
*/
protected function eval_condition($input)
{
$matches = preg_split('/<roundcube:(if|elseif|else|endif)\s*([^>]*)>\n?/is', $input, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($matches && count($matches) == 4) {
if (preg_match('/^(else|endif)$/i', $matches[1])) {
return $matches[0] . $this->eval_condition($matches[3]);
}
$attrib = html::parse_attrib_string($matches[2]);
if (isset($attrib['condition'])) {
$condmet = $this->check_condition($attrib['condition']);
$condparts = preg_split('/<roundcube:((elseif|else|endif)[^>]*)>\n?/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE);
if ($condmet) {
$result = $condparts[0];
if ($condparts[2] != 'endif') {
$result .= preg_replace('/.*<roundcube:endif[^>]*>\n?/Uis', '', $condparts[3], 1);
}
else {
$result .= $condparts[3];
}
}
else {
$result = "<roundcube:$condparts[1]>" . $condparts[3];
}
return $matches[0] . $this->eval_condition($result);
}
rcube::raise_error(array(
'code' => 500, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @param string $condition Condition statement
*
* @return boolean True if condition is met, False if not
* @todo Extend this to allow real conditions, not just "set"
*/
protected function check_condition($condition)
{
return $this->eval_expression($condition);
}
/**
* Inserts hidden field with CSRF-prevention-token into POST forms
*/
protected function alter_form_tag($matches)
{
$out = $matches[0];
$attrib = html::parse_attrib_string($matches[1]);
if (strtolower($attrib['method']) == 'post') {
$hidden = new html_hiddenfield(array('name' => '_token', 'value' => $this->app->get_request_token()));
$out .= "\n" . $hidden->show();
}
return $out;
}
/**
* Parse & evaluate a given expression and return its result.
*
* @param string $expression Expression statement
*
* @return mixed Expression result
*/
protected function eval_expression($expression)
{
$expression = preg_replace(
array(
'/session:([a-z0-9_]+)/i',
'/config:([a-z0-9_]+)(:([a-z0-9_]+))?/i',
'/env:([a-z0-9_]+)/i',
'/request:([a-z0-9_]+)/i',
'/cookie:([a-z0-9_]+)/i',
'/browser:([a-z0-9_]+)/i',
'/template:name/i',
),
array(
"(isset(\$_SESSION['\\1']) ? \$_SESSION['\\1'] : null)",
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"(isset(\$this->env['\\1']) ? \$this->env['\\1'] : null)",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"(isset(\$_COOKIE['\\1']) ? \$_COOKIE['\\1'] : null)",
"(isset(\$this->browser->{'\\1'}) ? \$this->browser->{'\\1'} : null)",
"'{$this->template_name}'",
),
$expression
);
// Note: We used create_function() before but it's deprecated in PHP 7.2
// and really it was just a wrapper on eval().
return eval("return ($expression);");
}
/**
* Parse variable strings
*
* @param string $type Variable type (env, config etc)
* @param string $name Variable name
*
* @return mixed Variable value
*/
protected function parse_variable($type, $name)
{
$value = '';
switch ($type) {
case 'env':
- $value = $this->env[$name];
+ $value = isset($this->env[$name]) ? $this->env[$name] : null;
break;
case 'config':
$value = $this->config->get($name);
if (is_array($value) && $value[$_SESSION['storage_host']]) {
$value = $value[$_SESSION['storage_host']];
}
break;
case 'request':
$value = rcube_utils::get_input_value($name, rcube_utils::INPUT_GPC);
break;
case 'session':
$value = $_SESSION[$name];
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name], ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
return $value;
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string $input Input string to parse
*
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
protected function parse_xml($input)
{
return preg_replace_callback('/<roundcube:([-_a-z]+)\s+((?:[^>]|\\\\>)+)(?<!\\\\)>/Ui', array($this, 'xml_command'), $input);
}
/**
* Callback function for parsing an xml command tag
* and turn it into real html content
*
* @param array $matches Matches array of preg_replace_callback
*
* @return string Tag/Object content
*/
protected function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = html::parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// localize title and summary attributes
if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
$attrib['title'] = $this->app->gettext($attrib['title']);
}
if ($command != 'button' && !empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) {
$attrib['summary'] = $this->app->gettext($attrib['summary']);
}
// execute command
switch ($command) {
// return a button
case 'button':
if (!empty($attrib['name']) || !empty($attrib['command'])) {
return $this->button($attrib);
}
break;
// frame
case 'frame':
return $this->frame($attrib);
break;
// show a label
case 'label':
if (!empty($attrib['expression'])) {
$attrib['name'] = $this->eval_expression($attrib['expression']);
}
if (!empty($attrib['name']) || !empty($attrib['command'])) {
$vars = $attrib + array('product' => $this->config->get('product_name'));
unset($vars['name'], $vars['command']);
$label = $this->app->gettext($attrib + array('vars' => $vars));
$quoting = null;
if (!empty($attrib['quoting'])) {
$quoting = strtolower($attrib['quoting']);
}
else if (isset($attrib['html'])) {
$quoting = rcube_utils::get_boolean((string) $attrib['html']) ? 'no' : '';
}
// 'noshow' can be used in skins to define new labels
if (!empty($attrib['noshow'])) {
return '';
}
switch ($quoting) {
case 'no':
case 'raw':
break;
case 'javascript':
case 'js':
$label = rcube::JQ($label);
break;
default:
$label = html::quote($label);
break;
}
return $label;
}
break;
case 'add_label':
$this->add_label($attrib['name']);
break;
// include a file
case 'include':
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
break;
}
if ($attrib['file'][0] != '/') {
$attrib['file'] = '/templates/' . $attrib['file'];
}
$old_base_path = $this->base_path;
$include = '';
$attr_skin_path = !empty($attrib['skinpath']) ? $attrib['skinpath'] : null;
if (!empty($attrib['skin_path'])) {
$attr_skin_path = $attrib['skin_path'];
}
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attr_skin_path)) {
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$path = realpath(RCUBE_INSTALL_PATH . $path);
}
if (is_readable($path)) {
$allow_php = $this->config->get('skin_include_php');
$include = $allow_php ? $this->include_php($path) : file_get_contents($path);
$include = $this->parse_conditions($include);
$include = $this->parse_xml($include);
$include = $this->fix_paths($include);
}
$this->base_path = $old_base_path;
return $include;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib);
return $hook['content'];
// define a container block
case 'container':
if ($attrib['name'] && $attrib['id']) {
$this->command('gui_container', $attrib['name'], $attrib['id']);
// let plugins insert some content here
$hook = $this->app->plugins->exec_hook("template_container", $attrib);
return $hook['content'];
}
break;
// return code for a specific application object
case 'object':
$object = strtolower($attrib['name']);
$content = '';
$handler = null;
// correct deprecated object names
if (!empty($this->deprecated_template_objects[$object])) {
$object = $this->deprecated_template_objects[$object];
}
if (!empty($this->object_handlers[$object])) {
$handler = $this->object_handlers[$object];
}
// execute object handler function
if (is_callable($handler)) {
$this->prepare_object_attribs($attrib);
// We assume that objects with src attribute are internal (in most
// cases this is a watermark frame). We need this to make sure assets_path
// is added to the internal assets paths
$external = empty($attrib['src']);
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"')));
// 'type' attribute added in 1.4 was renamed 'logo-type' in 1.5
// check both for backwards compatibility
$logo_type = !empty($attrib['logo-type']) ? $attrib['logo-type'] : null;
$logo_match = !empty($attrib['logo-match']) ? $attrib['logo-match'] : null;
if (!empty($attrib['type']) && empty($logo_type)) {
$logo_type = $attrib['type'];
}
if (($template_logo = $this->get_template_logo($logo_type, $logo_match)) !== null) {
$attrib['src'] = $template_logo;
}
$additional_logos = array();
$logo_types = (array) $this->config->get('additional_logo_types');
foreach ($logo_types as $type) {
if (($template_logo = $this->get_template_logo($type)) !== null) {
$additional_logos[$type] = $this->abs_url($template_logo);
}
}
if (!empty($additional_logos)) {
$this->set_env('additional_logos', $additional_logos);
}
if (!empty($attrib['src'])) {
$content = html::img($attrib);
}
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle(false));
}
else if ($object == 'pagetitle') {
// Deprecated, <title> will be added automatically
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'contentframe') {
if (empty($attrib['id'])) {
$attrib['id'] = 'rcm' . $this->env['task'] . 'frame';
}
// parse variables
if (preg_match('/^(config|env):([a-z0-9_]+)$/i', $attrib['src'], $matches)) {
$attrib['src'] = $this->parse_variable($matches[1], $matches[2]);
}
$content = $this->frame($attrib, true);
}
else if ($object == 'meta' || $object == 'links') {
if ($object == 'meta') {
$source = 'meta_tags';
$tag = 'meta';
$key = 'name';
$param = 'content';
}
else {
$source = 'link_tags';
$tag = 'link';
$key = 'rel';
$param = 'href';
}
foreach ($this->$source as $name => $vars) {
// $vars can be in many forms:
// - string
// - array('key' => 'val')
// - array(string, string)
// - array(array(), string)
// - array(array('key' => 'val'), array('key' => 'val'))
// normalise this for processing by checking for string array keys
$vars = is_array($vars) ? (count(array_filter(array_keys($vars), 'is_string')) > 0 ? array($vars) : $vars) : array($vars);
foreach ($vars as $args) {
// skip unset headers e.g. when extending a skin and removing a header defined in the parent
if ($args === false) {
continue;
}
$args = is_array($args) ? $args : array($param => $args);
// special handling for favicon
if ($object == 'links' && $name == 'shortcut icon' && empty($args[$param])) {
if ($href = $this->get_template_logo('favicon')) {
$args[$param] = $href;
}
else if ($href = $this->config->get('favicon', '/images/favicon.ico')) {
$args[$param] = $href;
}
}
$content .= html::tag($tag, array($key => $name, 'nl' => true) + $args);
}
}
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
if (strlen($hook['content']) && !empty($external)) {
$object_id = uniqid('TEMPLOBJECT:', true);
$this->objects[$object_id] = $hook['content'];
$hook['content'] = $object_id;
}
return $hook['content'];
// return <link> element
case 'link':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
unset($attrib['condition']);
return html::tag('link', $attrib);
// return code for a specified eval expression
case 'exp':
return html::quote($this->eval_expression($attrib['expression']));
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$value = $this->parse_variable($var[0], $var[1]);
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
case 'form':
return $this->form_tag($attrib);
}
return '';
}
/**
* Prepares template object attributes
*
* @param array &$attribs Attributes
*/
protected function prepare_object_attribs(&$attribs)
{
foreach ($attribs as $key => &$value) {
if (strpos($key, 'data-label-') === 0) {
// Localize data-label-* attributes
$value = $this->app->gettext($value);
}
elseif ($key[0] == ':') {
// Evaluate attributes with expressions and remove special character from attribute name
$attribs[substr($key, 1)] = $this->eval_expression($value);
unset($attribs[$key]);
}
}
}
/**
* Include a specific file and return it's contents
*
* @param string $file File path
*
* @return string Contents of the processed file
*/
protected function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Put objects' content back into template output
*/
protected function postrender($output)
{
// insert objects' contents
foreach ($this->objects as $key => $val) {
$output = str_replace($key, $val, $output, $count);
if ($count) {
$this->objects[$key] = null;
}
}
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
return $output;
}
/**
* Create and register a button
*
* @param array $attrib Named button attributes
*
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $s_button_count = 100;
static $disabled_actions = null;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (empty($attrib['command']) && empty($attrib['name']) && empty($attrib['href'])) {
return '';
}
$command = !empty($attrib['command']) ? $attrib['command'] : null;
$action = $command ?: (!empty($attrib['name']) ? $attrib['name'] : null);
if (!empty($attrib['task'])) {
$command = $attrib['task'] . '.' . $command;
$element = $attrib['task'] . '.' . $action;
}
else {
$element = (!empty($this->env['task']) ? $this->env['task'] . '.' : '') . $action;
}
if ($disabled_actions === null) {
$disabled_actions = (array) $this->config->get('disabled_actions');
}
// remove buttons for disabled actions
if (in_array($element, $disabled_actions) || in_array($action, $disabled_actions)) {
return '';
}
// try to find out the button type
if (!empty($attrib['type'])) {
$attrib['type'] = strtolower($attrib['type']);
if (strpos($attrib['type'], '-menuitem')) {
$attrib['type'] = substr($attrib['type'], 0, -9);
$menuitem = true;
}
}
else if (!empty($attrib['image']) || !empty($attrib['imagepas']) || !empty($attrib['imageact'])) {
$attrib['type'] = 'image';
}
else {
$attrib['type'] = 'button';
}
if (empty($attrib['image'])) {
if (!empty($attrib['imagepas'])) {
$attrib['image'] = $attrib['imagepas'];
}
else if (!empty($attrib['imageact'])) {
$attrib['image'] = $attrib['imageact'];
}
}
if (empty($attrib['id'])) {
// ensure auto generated IDs are unique between main window and content frame
// Elastic skin duplicates buttons between the two on smaller screens (#7618)
$prefix = ($this->framed || !empty($this->env['framed'])) ? 'frm' : '';
$attrib['id'] = sprintf('rcmbtn%s%d', $prefix, $s_button_count++);
}
// get localized text for labels and titles
$domain = !empty($attrib['domain']) ? $attrib['domain'] : null;
if (!empty($attrib['title'])) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $domain));
}
if (!empty($attrib['label'])) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $domain));
}
if (!empty($attrib['alt'])) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $domain));
}
// set accessibility attributes
if (empty($attrib['role'])) {
$attrib['role'] = 'button';
}
if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
if (array_key_exists('tabindex', $attrib)) {
$attrib['data-tabindex'] = $attrib['tabindex'];
}
$attrib['tabindex'] = '-1'; // disable button by default
$attrib['aria-disabled'] = 'true';
}
// set title to alt attribute for IE browsers
if ($this->browser->ie && empty($attrib['title']) && !empty($attrib['alt'])) {
$attrib['title'] = $attrib['alt'];
}
// add empty alt attribute for XHTML compatibility
if (!isset($attrib['alt'])) {
$attrib['alt'] = '';
}
// register button in the system
if (!empty($attrib['command'])) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
self::JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
!empty($attrib['imageact']) ? $this->abs_url($attrib['imageact']) : (!empty($attrib['classact']) ? $attrib['classact'] : ''),
!empty($attrib['imagesel']) ? $this->abs_url($attrib['imagesel']) : (!empty($attrib['classsel']) ? $attrib['classsel'] : ''),
!empty($attrib['imageover']) ? $this->abs_url($attrib['imageover']) : ''
));
// make valid href to specific buttons
if (in_array($attrib['command'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('task' => $attrib['command']));
$attrib['onclick'] = sprintf("return %s.command('switch-task','%s',this,event)", self::JS_OBJECT_NAME, $attrib['command']);
}
else if (!empty($attrib['task']) && in_array($attrib['task'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task']));
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command']));
}
else if (($attrib['command'] == 'permaurl' || $attrib['command'] == 'extwin') && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (empty($attrib['href'])) {
$attrib['href'] = '#';
}
if (!empty($attrib['task'])) {
if (!empty($attrib['classact'])) {
$attrib['class'] = $attrib['classact'];
}
}
else if ($command && empty($attrib['onclick'])) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
self::JS_OBJECT_NAME,
$command,
!empty($attrib['prop']) ? $attrib['prop'] : ''
);
}
$out = '';
$btn_content = null;
$link_attrib = array();
// generate image tag
if ($attrib['type'] == 'image') {
$attrib_str = html::attrib_string(
$attrib,
array(
'style', 'class', 'id', 'width', 'height', 'border', 'hspace',
'vspace', 'align', 'alt', 'tabindex', 'title'
)
);
$btn_content = sprintf('<img src="%s"%s />', $this->abs_url($attrib['image']), $attrib_str);
if (!empty($attrib['label'])) {
$btn_content .= ' '.$attrib['label'];
}
$link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'target');
}
else if ($attrib['type'] == 'link') {
$btn_content = isset($attrib['content']) ? $attrib['content'] : (!empty($attrib['label']) ? $attrib['label'] : $attrib['command']);
$link_attrib = array_merge(html::$common_attrib, array('href', 'onclick', 'tabindex', 'target', 'rel'));
if (!empty($attrib['innerclass'])) {
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if (!empty($attrib['label'])) {
$attrib['value'] = $attrib['label'];
}
if (!empty($attrib['command'])) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
else {
if (!empty($attrib['label'])) {
$attrib['value'] = $attrib['label'];
}
if (!empty($attrib['command'])) {
$attrib['disabled'] = 'disabled';
}
$content = isset($attrib['content']) ? $attrib['content'] : $attrib['label'];
$out = html::tag('button', $attrib, $content, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, $link_attrib);
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
if (!empty($attrib['wrapper'])) {
$out = html::tag($attrib['wrapper'], null, $out);
}
if (!empty($menuitem)) {
$class = !empty($attrib['menuitem-class']) ? ' class="' . $attrib['menuitem-class'] . '"' : '';
$out = '<li role="menuitem"' . $class . '>' . $out . '</li>';
}
return $out;
}
/**
* Link an external script file
*
* @param string $file File URL
* @param string $position Target position [head|head_bottom|foot]
*/
public function include_script($file, $position = 'head', $add_path = true)
{
if ($add_path && !preg_match('|^https?://|i', $file) && $file[0] != '/') {
$file = $this->file_mod($this->scripts_path . $file);
}
if (!isset($this->script_files[$position]) || !is_array($this->script_files[$position])) {
$this->script_files[$position] = array();
}
if (!in_array($file, $this->script_files[$position])) {
$this->script_files[$position][] = $file;
}
}
/**
* Add inline javascript code
*
* @param string $script JS code snippet
* @param string $position Target position [head|head_top|foot|docready]
*/
public function add_script($script, $position = 'head')
{
if (!isset($this->scripts[$position])) {
$this->scripts[$position] = rtrim($script);
}
else {
$this->scripts[$position] .= "\n" . rtrim($script);
}
}
/**
* Link an external css file
*
* @param string $file File URL
*/
public function include_css($file)
{
$this->css_files[] = $file;
}
/**
* Add HTML code to the page header
*
* @param string $str HTML code
*/
public function add_header($str)
{
$this->header .= "\n" . $str;
}
/**
* Add HTML code to the page footer
* To be added right befor </body>
*
* @param string $str HTML code
*/
public function add_footer($str)
{
$this->footer .= "\n" . $str;
}
/**
* Process template and write to stdOut
*
* @param string $output HTML output
*/
protected function _write($output = '')
{
$output = trim($output);
if (empty($output)) {
$output = html::doctype('html5') . "\n" . $this->default_template;
$is_empty = true;
}
$merge_script_files = function($output, $script) {
return $output . html::script($script);
};
$merge_scripts = function($output, $script) {
return $output . html::script(array(), $script);
};
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script("\$(function() {\n" . $this->scripts['docready'] . "\n});", 'foot');
}
$page_header = '';
$page_footer = '';
$meta = '';
// declare page language
if (!empty($_SESSION['language'])) {
$lang = substr($_SESSION['language'], 0, 2);
$output = preg_replace('/<html/', '<html lang="' . html::quote($lang) . '"', $output, 1);
if (!headers_sent()) {
$this->header('Content-Language: ' . $lang);
}
}
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
$this->header('Content-Type: text/html; charset=' . $this->charset);
}
$meta .= html::tag('meta', array(
'http-equiv' => 'content-type',
'content' => "text/html; charset={$this->charset}",
'nl' => true
));
}
// include page title (after charset specification)
$meta .= '<title>' . html::quote($this->get_pagetitle()) . "</title>\n";
$output = preg_replace('/(<head[^>]*>)\n*/i', "\\1\n{$meta}", $output, 1, $count);
if (!$count) {
$page_header .= $meta;
}
// include scripts into header/footer
if (!empty($this->script_files['head'])) {
$page_header .= array_reduce((array) $this->script_files['head'], $merge_script_files);
}
$head = $this->scripts['head_top'] . (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 hader
if ($hpos) {
$output = substr_replace($output, $page_header, $hpos, 0);
}
else {
$output = $page_header . $output;
}
// add page footer
if (($fpos = strripos($output, '</body>')) || ($fpos = strripos($output, '</html>'))) {
// for Elastic: put footer content before "footer scripts"
while (($npos = strripos($output, "\n", -strlen($output) + $fpos - 1))
&& $npos != $fpos
&& ($chunk = substr($output, $npos, $fpos - $npos)) !== ''
&& (trim($chunk) === '' || preg_match('/\s*<script[^>]+><\/script>\s*/', $chunk))
) {
$fpos = $npos;
}
$output = substr_replace($output, $page_footer."\n", $fpos, 0);
}
else {
$output .= "\n".$page_footer;
}
// add css files in head, before scripts, for speed up with parallel downloads
if (!empty($this->css_files) && empty($is_empty)
&& (($pos = stripos($output, '<script ')) || ($pos = stripos($output, '</head>')))
) {
$css = '';
foreach ($this->css_files as $file) {
$is_less = substr_compare($file, '.less', -5, 5, true) === 0;
$css .= html::tag('link', array(
'rel' => $is_less ? 'stylesheet/less' : 'stylesheet',
'type' => 'text/css',
'href' => $file,
'nl' => true,
));
}
$output = substr_replace($output, $css, $pos, 0);
}
$output = $this->parse_with_globals($this->fix_paths($output));
if ($this->assets_path) {
$output = $this->fix_assets_paths($output);
}
$output = $this->postrender($output);
// trigger hook with final HTML content to be sent
$hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
if (!$hook['abort']) {
if ($this->charset != RCUBE_CHARSET) {
echo rcube_charset::convert($hook['content'], RCUBE_CHARSET, $this->charset);
}
else {
echo $hook['content'];
}
}
}
/**
* Returns iframe object, registers some related env variables
*
* @param array $attrib HTML attributes
* @param boolean $is_contentframe Register this iframe as the 'contentframe' gui object
*
* @return string IFRAME element
*/
public function frame($attrib, $is_contentframe = false)
{
static $idcount = 0;
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmframe' . ++$idcount;
}
$attrib['name'] = $attrib['id'];
$attrib['src'] = !empty($attrib['src']) ? $this->abs_url($attrib['src'], true) : 'about:blank';
// register as 'contentframe' object
if ($is_contentframe || !empty($attrib['contentframe'])) {
$this->set_env('contentframe', !empty($attrib['contentframe']) ? $attrib['contentframe'] : $attrib['name']);
}
return html::iframe($attrib);
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array $attrib Named tag parameters
* @param string $content HTML content of the form
*
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
$hidden = '';
if (!empty($this->env['extwin'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_extwin', 'value' => '1'));
$hidden = $hiddenfield->show();
}
else if ($this->framed || !empty($this->env['framed'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
$hidden = $hiddenfield->show();
}
if (!$content) {
$attrib['noclose'] = true;
}
return html::tag('form',
$attrib + array('action' => $this->app->comm_path, 'method' => "get"),
$hidden . $content,
array('id','class','style','name','method','action','enctype','onsubmit')
);
}
/**
* Build a form tag with a unique request token
*
* @param array $attrib Named tag parameters including 'action' and 'task' values
* which will be put into hidden fields
* @param string $content Form content
*
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if (!empty($attrib['task'])) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if (!empty($attrib['action'])) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
// we already have a <form> tag
if (!empty($attrib['form'])) {
if ($this->framed || !empty($this->env['framed'])) {
$hidden->add(array('name' => '_framed', 'value' => '1'));
}
return $hidden->show() . $content;
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array $attrib Named tag parameters (currently not used)
*
* @return string HTML code for the gui object
*/
public function current_username($attrib)
{
static $username;
// alread fetched
if (!empty($username)) {
return $username;
}
// Current username is an e-mail address
if (strpos($_SESSION['username'], '@')) {
$username = $_SESSION['username'];
}
// get e-mail address from default identity
else if ($sql_arr = $this->app->user->get_identity()) {
$username = $sql_arr['email'];
}
else {
$username = $this->app->user->get_username();
}
$username = rcube_utils::idn_to_utf8($username);
return html::quote($username);
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
protected function login_form($attrib)
{
$default_host = $this->config->get('default_host');
$autocomplete = (int) $this->config->get('login_autocomplete');
$username_filter = $this->config->get('login_username_filter');
$_SESSION['temp'] = true;
// save original url
$url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING'])) {
$url = $_SERVER['QUERY_STRING'];
}
// Disable autocapitalization on iPad/iPhone (#1488609)
$attrib['autocapitalize'] = 'off';
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
// set atocomplete attribute
$user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');
if ($username_filter && strtolower($username_filter) == 'email') {
$user_attrib['type'] = 'email';
}
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
$input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required')
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'required' => 'required')
+ $attrib + $pass_attrib);
$input_host = null;
$hide_host = false;
if (is_array($default_host) && count($default_host) > 1) {
$input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost', 'class' => 'custom-select'));
foreach ($default_host as $key => $value) {
if (!is_array($value)) {
$input_host->add($value, (is_numeric($key) ? $value : $key));
}
else {
$input_host = null;
break;
}
}
}
else if (is_array($default_host) && ($host = key($default_host)) !== null) {
$hide_host = true;
$input_host = new html_hiddenfield(array(
'name' => '_host', 'id' => 'rcmloginhost', 'value' => is_numeric($host) ? $default_host[$host] : $host) + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost', 'class' => 'form-control')
+ $attrib + $host_attrib);
}
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', html::quote($this->app->gettext('username'))));
$table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
$table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
$table->add('input', $input_pass->show());
// add host selection row
if (is_object($input_host) && !$hide_host) {
$table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
$table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
if ($hide_host) {
$out .= $input_host->show();
}
if (rcube_utils::get_boolean($attrib['submit'])) {
$button_attr = array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit');
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// add oauth login button
if ($this->config->get('oauth_auth_uri') && $this->config->get('oauth_provider')) {
$link_attr = array('href' => $this->app->url(array('action' => 'oauth')), 'id' => 'rcmloginoauth', 'class' => 'button oauth ' . $this->config->get('oauth_provider'));
$out .= html::p('oauthlogin', html::a($link_attr, $this->app->gettext(array('name' => 'oauthlogin', 'vars' => array('provider' => $this->config->get('oauth_provider_name', 'OAuth'))))));
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
}
// include script for timezone detection
$this->include_script('jstz.min.js');
return $out;
}
/**
* GUI object 'preloader'
* Loads javascript code for images preloading
*
* @param array $attrib Named parameters
* @return void
*/
protected function preloader($attrib)
{
$images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
$images = array_map(array($this, 'abs_url'), $images);
$images = array_map(array($this, 'asset_url'), $images);
if (empty($images) || $_REQUEST['_task'] == 'logout') {
return;
}
$this->add_script('var images = ' . self::json_serialize($images, $this->devel_mode) .';
for (var i=0; i<images.length; i++) {
img = new Image();
img.src = images[i];
}', 'docready');
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
$attrib['class'] = trim((!empty($attrib['class']) ? $attrib['class'] : '') . ' no-bs');
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if (isset($attrib['type']) && $attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
if (empty($attrib['placeholder'])) {
$attrib['placeholder'] = $this->app->gettext('searchplaceholder');
}
$label = html::label(array('for' => $attrib['id'], 'class' => 'voice'), rcube::Q($this->app->gettext('arialabelsearchterms')));
$input_q = new html_inputfield($attrib);
$out = $label . $input_q->show();
$name = 'qsearchbox';
// Support for multiple searchforms on the same page
if (isset($attrib['gui-object']) && $attrib['gui-object'] !== false && $attrib['gui-object'] !== 'false') {
$name = $attrib['gui-object'];
}
$this->add_gui_object($name, $attrib['id']);
// add form tag around text field
if (empty($attrib['form']) && empty($attrib['no-form'])) {
$out = $this->form_tag(array(
'name' => !empty($attrib['form-name']) ? $attrib['form-name'] : 'rcmqsearchform',
'onsubmit' => sprintf(
"%s.command('%s'); return false",
self::JS_OBJECT_NAME,
!empty($attrib['command']) ? $attrib['command'] : 'search'
),
// 'style' => "display:inline"
), $out);
}
if (!empty($attrib['wrapper'])) {
$options_button = '';
$ariatag = !empty($attrib['ariatag']) ? $attrib['ariatag'] : 'h2';
$domain = !empty($attrib['label-domain']) ? $attrib['label-domain'] : null;
$options = !empty($attrib['options']) ? $attrib['options'] : null;
$header_label = $this->app->gettext('arialabel' . $attrib['label'], $domain);
$header_attrs = array(
'id' => 'aria-label-' . $attrib['label'],
'class' => 'voice'
);
$header = html::tag($ariatag, $header_attrs, rcube::Q($header_label));
if ($attrib['options']) {
$options_button = $this->button(array(
'type' => 'link',
'href' => '#search-filter',
'class' => 'button options',
'label' => 'options',
'title' => 'options',
'tabindex' => '0',
'innerclass' => 'inner',
'data-target' => $options
));
}
$search_button = $this->button(array(
'type' => 'link',
'href' => '#search',
'class' => 'button search',
'label' => $attrib['buttontitle'],
'title' => $attrib['buttontitle'],
'tabindex' => '0',
'innerclass' => 'inner',
));
$reset_button = $this->button(array(
'type' => 'link',
'command' => !empty($attrib['reset-command']) ? $attrib['reset-command'] : 'reset-search',
'class' => 'button reset',
'label' => 'resetsearch',
'title' => 'resetsearch',
'tabindex' => '0',
'innerclass' => 'inner',
));
$out = html::div(array(
'role' => 'search',
'aria-labelledby' => !empty($attrib['label']) ? 'aria-label-' . $attrib['label'] : null,
'class' => $attrib['wrapper'],
), "$header$out\n$reset_button\n$options_button\n$search_button");
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
protected function message_container($attrib)
{
if (isset($attrib['id']) === false) {
$attrib['id'] = 'rcmMessageContainer';
}
$this->add_gui_object('message', $attrib['id']);
return html::div($attrib, '');
}
/**
* GUI object 'charsetselector'
*
* @param array $attrib Named parameters for the select tag
*
* @return string HTML code for the gui object
*/
public function charset_selector($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_charset');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'name', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
$charsets = array(
'UTF-8' => 'UTF-8 ('.$this->app->gettext('unicode').')',
'US-ASCII' => 'ASCII ('.$this->app->gettext('english').')',
'ISO-8859-1' => 'ISO-8859-1 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-2' => 'ISO-8859-2 ('.$this->app->gettext('easterneuropean').')',
'ISO-8859-4' => 'ISO-8859-4 ('.$this->app->gettext('baltic').')',
'ISO-8859-5' => 'ISO-8859-5 ('.$this->app->gettext('cyrillic').')',
'ISO-8859-6' => 'ISO-8859-6 ('.$this->app->gettext('arabic').')',
'ISO-8859-7' => 'ISO-8859-7 ('.$this->app->gettext('greek').')',
'ISO-8859-8' => 'ISO-8859-8 ('.$this->app->gettext('hebrew').')',
'ISO-8859-9' => 'ISO-8859-9 ('.$this->app->gettext('turkish').')',
'ISO-8859-10' => 'ISO-8859-10 ('.$this->app->gettext('nordic').')',
'ISO-8859-11' => 'ISO-8859-11 ('.$this->app->gettext('thai').')',
'ISO-8859-13' => 'ISO-8859-13 ('.$this->app->gettext('baltic').')',
'ISO-8859-14' => 'ISO-8859-14 ('.$this->app->gettext('celtic').')',
'ISO-8859-15' => 'ISO-8859-15 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-16' => 'ISO-8859-16 ('.$this->app->gettext('southeasterneuropean').')',
'WINDOWS-1250' => 'Windows-1250 ('.$this->app->gettext('easterneuropean').')',
'WINDOWS-1251' => 'Windows-1251 ('.$this->app->gettext('cyrillic').')',
'WINDOWS-1252' => 'Windows-1252 ('.$this->app->gettext('westerneuropean').')',
'WINDOWS-1253' => 'Windows-1253 ('.$this->app->gettext('greek').')',
'WINDOWS-1254' => 'Windows-1254 ('.$this->app->gettext('turkish').')',
'WINDOWS-1255' => 'Windows-1255 ('.$this->app->gettext('hebrew').')',
'WINDOWS-1256' => 'Windows-1256 ('.$this->app->gettext('arabic').')',
'WINDOWS-1257' => 'Windows-1257 ('.$this->app->gettext('baltic').')',
'WINDOWS-1258' => 'Windows-1258 ('.$this->app->gettext('vietnamese').')',
'ISO-2022-JP' => 'ISO-2022-JP ('.$this->app->gettext('japanese').')',
'ISO-2022-KR' => 'ISO-2022-KR ('.$this->app->gettext('korean').')',
'ISO-2022-CN' => 'ISO-2022-CN ('.$this->app->gettext('chinese').')',
'EUC-JP' => 'EUC-JP ('.$this->app->gettext('japanese').')',
'EUC-KR' => 'EUC-KR ('.$this->app->gettext('korean').')',
'EUC-CN' => 'EUC-CN ('.$this->app->gettext('chinese').')',
'BIG5' => 'BIG5 ('.$this->app->gettext('chinese').')',
'GB2312' => 'GB2312 ('.$this->app->gettext('chinese').')',
'KOI8-R' => 'KOI8-R ('.$this->app->gettext('cyrillic').')',
);
if ($post = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST)) {
$set = $post;
}
else if (!empty($attrib['selected'])) {
$set = $attrib['selected'];
}
else {
$set = $this->get_charset();
}
$set = strtoupper($set);
if (!isset($charsets[$set]) && preg_match('/^[A-Z0-9-]+$/', $set)) {
$charsets[$set] = $set;
}
$select = new html_select($field_attrib);
$select->add(array_values($charsets), array_keys($charsets));
return $select->show($set);
}
/**
* Include content from config/about.<LANG>.html if available
*/
protected function about_content($attrib)
{
$content = '';
$filenames = array(
'about.' . $_SESSION['language'] . '.html',
'about.' . substr($_SESSION['language'], 0, 2) . '.html',
'about.html',
);
foreach ($filenames as $file) {
$fn = RCUBE_CONFIG_DIR . $file;
if (is_readable($fn)) {
$content = file_get_contents($fn);
$content = $this->parse_conditions($content);
$content = $this->parse_xml($content);
break;
}
}
return $content;
}
/**
* Get logo URL for current template based on skin_logo config option
*
* @param string $type Type of the logo to check for (e.g. 'print' or 'small')
* default is null (no special type)
* @param string $match (optional) 'all' = type, template or wildcard, 'template' = type or template
* Note: when type is specified matches are limited to type only unless $match is defined
*
* @return string image URL
*/
protected function get_template_logo($type = null, $match = null)
{
$template_logo = null;
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
$template_names = array(
$this->skin_name . ':' . $this->template_name . '[' . $type . ']',
$this->skin_name . ':' . $this->template_name,
$this->skin_name . ':*[' . $type . ']',
$this->skin_name . ':[' . $type . ']',
$this->skin_name . ':*',
'*:' . $this->template_name . '[' . $type . ']',
'*:' . $this->template_name,
'*:*[' . $type . ']',
'*:[' . $type . ']',
$this->template_name . '[' . $type . ']',
$this->template_name,
'*[' . $type . ']',
'[' . $type . ']',
'*',
);
if (empty($type)) {
// If no type provided then remove those options from the list
$template_names = preg_grep("/\]$/", $template_names, PREG_GREP_INVERT);
}
elseif ($match === null) {
// Type specified with no special matching requirements so remove all none type specific options from the list
$template_names = preg_grep("/\]$/", $template_names);
}
if ($match == 'template') {
// Match only specific type or template name
$template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
}
foreach ($template_names as $key) {
if (isset($logo[$key])) {
$template_logo = $logo[$key];
break;
}
}
}
else {
$template_logo = $logo;
}
}
return $template_logo;
}
}
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 208cbe348..4da03a2af 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -1,4604 +1,4605 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| IMAP Storage Engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing an IMAP server
*
* @package Framework
* @subpackage Storage
*/
class rcube_imap extends rcube_storage
{
/**
* Instance of rcube_imap_generic
*
* @var rcube_imap_generic
*/
public $conn;
/**
* Instance of rcube_imap_cache
*
* @var rcube_imap_cache
*/
protected $mcache;
/**
* Instance of rcube_cache
*
* @var rcube_cache
*/
protected $cache;
protected $plugins;
protected $delimiter;
protected $namespace;
protected $struct_charset;
protected $search_set;
protected $search_string = '';
protected $search_charset = '';
protected $search_sort_field = '';
protected $search_threads = false;
protected $search_sorted = false;
protected $sort_field = '';
protected $sort_order = 'DESC';
protected $options = array('auth_type' => 'check');
protected $caching = false;
protected $messages_caching = false;
protected $threading = false;
protected $connect_done = false;
protected $list_excludes = array();
protected $list_root;
protected $msg_uid;
protected $sort_folder_collator;
/**
* Object constructor.
*/
public function __construct()
{
$this->conn = new rcube_imap_generic();
$this->plugins = rcube::get_instance()->plugins;
// Set namespace and delimiter from session,
// so some methods would work before connection
if (isset($_SESSION['imap_namespace'])) {
$this->namespace = $_SESSION['imap_namespace'];
}
if (isset($_SESSION['imap_delimiter'])) {
$this->delimiter = $_SESSION['imap_delimiter'];
}
if (!empty($_SESSION['imap_list_conf'])) {
list($this->list_root, $this->list_excludes) = $_SESSION['imap_list_conf'];
}
}
/**
* Magic getter for backward compat.
*
* @deprecated.
*/
public function __get($name)
{
if (isset($this->{$name})) {
return $this->{$name};
}
}
/**
* Connect to an IMAP server
*
* @param string $host Host to connect
* @param string $user Username for IMAP account
* @param string $pass Password for IMAP account
* @param integer $port Port to connect to
* @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
*
* @return boolean True on success, False on failure
*/
public function connect($host, $user, $pass, $port=143, $use_ssl=null)
{
// check for OpenSSL support in PHP build
if ($use_ssl && extension_loaded('openssl')) {
$this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
}
else if ($use_ssl) {
rcube::raise_error(array('code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "OpenSSL not available"), true, false);
$port = 143;
}
$this->options['port'] = $port;
if ($this->options['debug']) {
$this->set_debug(true);
$this->options['ident'] = array(
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
);
}
$attempt = 0;
do {
$data = $this->plugins->exec_hook('storage_connect',
array_merge($this->options, array('host' => $host, 'user' => $user,
'attempt' => ++$attempt)));
if (!empty($data['pass'])) {
$pass = $data['pass'];
}
// Handle per-host socket options
rcube_utils::parse_socket_options($data['socket_options'], $data['host']);
$this->conn->connect($data['host'], $data['user'], $pass, $data);
} while(!$this->conn->connected() && $data['retry']);
$config = array(
'host' => $data['host'],
'user' => $data['user'],
'password' => $pass,
'port' => $port,
'ssl' => $use_ssl,
);
$this->options = array_merge($this->options, $config);
$this->connect_done = true;
if ($this->conn->connected()) {
// check for session identifier
$session = null;
if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) {
$session = $m[1];
}
// get namespace and delimiter
$this->set_env();
// trigger post-connect hook
$this->plugins->exec_hook('storage_connected', array(
'host' => $host, 'user' => $user, 'session' => $session
));
return true;
}
// write error log
else if ($this->conn->error) {
if ($pass && $user) {
$message = sprintf("Login failed for %s against %s from %s. %s",
$user, $host, rcube_utils::remote_ip(), $this->conn->error);
rcube::raise_error(array('code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => $message), true, false);
}
}
return false;
}
/**
* Close IMAP connection.
* Usually done on script shutdown
*/
public function close()
{
$this->connect_done = false;
$this->conn->closeConnection();
if ($this->mcache) {
$this->mcache->close();
}
}
/**
* Check connection state, connect if not connected.
*
* @return bool Connection state.
*/
public function check_connection()
{
// Establish connection if it wasn't done yet
if (!$this->connect_done && !empty($this->options['user'])) {
return $this->connect(
$this->options['host'],
$this->options['user'],
$this->options['password'],
$this->options['port'],
$this->options['ssl']
);
}
return $this->is_connected();
}
/**
* Checks IMAP connection.
*
* @return boolean TRUE on success, FALSE on failure
*/
public function is_connected()
{
return $this->conn->connected();
}
/**
* Returns code of last error
*
* @return int Error code
*/
public function get_error_code()
{
return $this->conn->errornum;
}
/**
* Returns text of last error
*
* @return string Error string
*/
public function get_error_str()
{
return $this->conn->error;
}
/**
* Returns code of last command response
*
* @return int Response code
*/
public function get_response_code()
{
switch ($this->conn->resultcode) {
case 'NOPERM':
return self::NOPERM;
case 'READ-ONLY':
return self::READONLY;
case 'TRYCREATE':
return self::TRYCREATE;
case 'INUSE':
return self::INUSE;
case 'OVERQUOTA':
return self::OVERQUOTA;
case 'ALREADYEXISTS':
return self::ALREADYEXISTS;
case 'NONEXISTENT':
return self::NONEXISTENT;
case 'CONTACTADMIN':
return self::CONTACTADMIN;
default:
return self::UNKNOWN;
}
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if IMAP conversation should be logged
*/
public function set_debug($dbg = true)
{
$this->options['debug'] = $dbg;
$this->conn->setDebug($dbg, array($this, 'debug_handler'));
}
/**
* Set internal folder reference.
* All operations will be performed on this folder.
*
* @param string $folder Folder name
*/
public function set_folder($folder)
{
$this->folder = $folder;
}
/**
* Save a search result for future message listing methods
*
* @param array $set Search set, result from rcube_imap::get_search_set():
* 0 - searching criteria, string
* 1 - search result, rcube_result_index|rcube_result_thread
* 2 - searching character set, string
* 3 - sorting field, string
* 4 - true if sorted, bool
*/
public function set_search_set($set)
{
$set = (array)$set;
$this->search_string = $set[0];
$this->search_set = $set[1];
$this->search_charset = $set[2];
$this->search_sort_field = $set[3];
$this->search_sorted = $set[4];
$this->search_threads = is_a($this->search_set, 'rcube_result_thread');
if (is_a($this->search_set, 'rcube_result_multifolder')) {
$this->set_threading(false);
}
}
/**
* Return the saved search set as hash array
*
* @return array Search set
*/
public function get_search_set()
{
if (empty($this->search_set)) {
return null;
}
return array(
$this->search_string,
$this->search_set,
$this->search_charset,
$this->search_sort_field,
$this->search_sorted,
);
}
/**
* Returns the IMAP server's capability.
*
* @param string $cap Capability name
*
* @return mixed Capability value or TRUE if supported, FALSE if not
*/
public function get_capability($cap)
{
$cap = strtoupper($cap);
$sess_key = "STORAGE_$cap";
if (!isset($_SESSION[$sess_key])) {
if (!$this->check_connection()) {
return false;
}
if ($cap == rcube_storage::DUAL_USE_FOLDERS) {
$_SESSION[$sess_key] = $this->detect_dual_use_folders();
}
else {
$_SESSION[$sess_key] = $this->conn->getCapability($cap);
}
}
return $_SESSION[$sess_key];
}
/**
* Checks the PERMANENTFLAGS capability of the current folder
* and returns true if the given flag is supported by the IMAP server
*
* @param string $flag Permanentflag name
*
* @return boolean True if this flag is supported
*/
public function check_permflag($flag)
{
$flag = strtoupper($flag);
$perm_flags = $this->get_permflags($this->folder);
$imap_flag = $this->conn->flags[$flag];
return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
}
/**
* Returns PERMANENTFLAGS of the specified folder
*
* @param string $folder Folder name
*
* @return array Flags
*/
public function get_permflags($folder)
{
if (!strlen($folder)) {
return array();
}
if (!$this->check_connection()) {
return array();
}
if ($this->conn->select($folder)) {
$permflags = $this->conn->data['PERMANENTFLAGS'];
}
else {
return array();
}
if (!is_array($permflags)) {
$permflags = array();
}
return $permflags;
}
/**
* Returns the delimiter that is used by the IMAP server for folder separation
*
* @return string Delimiter string
*/
public function get_hierarchy_delimiter()
{
return $this->delimiter;
}
/**
* Get namespace
*
* @param string $name Namespace array index: personal, other, shared, prefix
*
* @return array Namespace data
*/
public function get_namespace($name = null)
{
$ns = $this->namespace;
if ($name) {
// an alias for BC
if ($name == 'prefix') {
$name = 'prefix_in';
}
return isset($ns[$name]) ? $ns[$name] : null;
}
unset($ns['prefix_in'], $ns['prefix_out']);
return $ns;
}
/**
* Sets delimiter and namespaces
*/
protected function set_env()
{
if ($this->delimiter !== null && $this->namespace !== null) {
return;
}
$config = rcube::get_instance()->config;
$imap_personal = $config->get('imap_ns_personal');
$imap_other = $config->get('imap_ns_other');
$imap_shared = $config->get('imap_ns_shared');
$imap_delimiter = $config->get('imap_delimiter');
if (!$this->check_connection()) {
return;
}
$ns = $this->conn->getNamespace();
// Set namespaces (NAMESPACE supported)
if (is_array($ns)) {
$this->namespace = $ns;
}
else {
$this->namespace = array(
'personal' => null,
'other' => null,
'shared' => null,
);
}
if ($imap_delimiter) {
$this->delimiter = $imap_delimiter;
}
if (empty($this->delimiter) && !empty($this->namespace['personal'][0][1])) {
$this->delimiter = $this->namespace['personal'][0][1];
}
if (empty($this->delimiter)) {
$this->delimiter = $this->conn->getHierarchyDelimiter();
}
if (empty($this->delimiter)) {
$this->delimiter = '/';
}
$this->list_root = null;
$this->list_excludes = array();
// Overwrite namespaces
if ($imap_personal !== null) {
$this->namespace['personal'] = null;
foreach ((array)$imap_personal as $dir) {
$this->namespace['personal'][] = array($dir, $this->delimiter);
}
}
if ($imap_other === false) {
foreach ((array) $this->namespace['other'] as $dir) {
if (is_array($dir) && $dir[0]) {
$this->list_excludes[] = $dir[0];
}
}
$this->namespace['other'] = null;
}
else if ($imap_other !== null) {
$this->namespace['other'] = null;
foreach ((array)$imap_other as $dir) {
if ($dir) {
$this->namespace['other'][] = array($dir, $this->delimiter);
}
}
}
if ($imap_shared === false) {
foreach ((array) $this->namespace['shared'] as $dir) {
if (is_array($dir) && $dir[0]) {
$this->list_excludes[] = $dir[0];
}
}
$this->namespace['shared'] = null;
}
else if ($imap_shared !== null) {
$this->namespace['shared'] = null;
foreach ((array)$imap_shared as $dir) {
if ($dir) {
$this->namespace['shared'][] = array($dir, $this->delimiter);
}
}
}
// Performance optimization for case where we have no shared/other namespace
// and personal namespace has one prefix (#5073)
// In such a case we can tell the server to return only content of the
// specified folder in LIST/LSUB, no post-filtering
if (empty($this->namespace['other']) && empty($this->namespace['shared'])
&& !empty($this->namespace['personal']) && count($this->namespace['personal']) === 1
&& strlen($this->namespace['personal'][0][0]) > 1
) {
$this->list_root = $this->namespace['personal'][0][0];
$this->list_excludes = array();
}
// Find personal namespace prefix(es) for self::mod_folder()
if (is_array($this->namespace['personal']) && !empty($this->namespace['personal'])) {
// There can be more than one namespace root,
// - for prefix_out get the first one but only
// if there is only one root
// - for prefix_in get the first one but only
// if there is no non-prefixed namespace root (#5403)
$roots = array();
foreach ($this->namespace['personal'] as $ns) {
$roots[] = $ns[0];
}
if (!in_array('', $roots)) {
$this->namespace['prefix_in'] = $roots[0];
}
if (count($roots) == 1) {
$this->namespace['prefix_out'] = $roots[0];
}
}
$_SESSION['imap_namespace'] = $this->namespace;
$_SESSION['imap_delimiter'] = $this->delimiter;
$_SESSION['imap_list_conf'] = array($this->list_root, $this->list_excludes);
}
/**
* Returns IMAP server vendor name
*
* @return string Vendor name
* @since 1.2
*/
public function get_vendor()
{
if (isset($_SESSION['imap_vendor'])) {
return $_SESSION['imap_vendor'];
}
$config = rcube::get_instance()->config;
$imap_vendor = $config->get('imap_vendor');
if ($imap_vendor) {
return $imap_vendor;
}
if (!$this->check_connection()) {
return;
}
if (isset($this->conn->data['ID'])) {
$ident = $this->conn->data['ID'];
}
else if ($this->get_capability('ID')) {
$ident = $this->conn->id(array(
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
));
}
else {
$ident = null;
}
$vendor = (string) (!empty($ident) ? $ident['name'] : '');
$ident = strtolower($vendor . ' ' . $this->conn->data['GREETING']);
$vendors = array('cyrus', 'dovecot', 'uw-imap', 'gimap', 'hmail', 'greenmail');
foreach ($vendors as $v) {
if (strpos($ident, $v) !== false) {
$vendor = $v;
break;
}
}
return $_SESSION['imap_vendor'] = $vendor;
}
/**
* Get message count for a specific folder
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
*/
public function count($folder='', $mode='ALL', $force=false, $status=true)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->countmessages($folder, $mode, $force, $status);
}
/**
* Protected method for getting number of messages
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
* @param boolean $no_search Ignore current search result
*
* @return int Number of messages
* @see rcube_imap::count()
*/
protected function countmessages($folder, $mode = 'ALL', $force = false, $status = true, $no_search = false)
{
$mode = strtoupper($mode);
// Count search set, assume search set is always up-to-date (don't check $force flag)
// @TODO: this could be handled in more reliable way, e.g. a separate method
// maybe in rcube_imap_search
if (!$no_search && $this->search_string && $folder == $this->folder) {
if ($mode == 'ALL') {
return $this->search_set->count_messages();
}
else if ($mode == 'THREADS') {
return $this->search_set->count();
}
}
// EXISTS is a special alias for ALL, it allows to get the number
// of all messages in a folder also when search is active and with
// any skip_deleted setting
$a_folder_cache = $this->get_cache('messagecount');
// return cached value
if (!$force && isset($a_folder_cache[$folder][$mode])) {
return $a_folder_cache[$folder][$mode];
}
if (!isset($a_folder_cache[$folder]) || !is_array($a_folder_cache[$folder])) {
$a_folder_cache[$folder] = array();
}
if ($mode == 'THREADS') {
$res = $this->threads($folder);
$count = $res->count();
if ($status) {
$msg_count = $res->count_messages();
$this->set_folder_stats($folder, 'cnt', $msg_count);
$this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0);
}
}
// Need connection here
else if (!$this->check_connection()) {
return 0;
}
// RECENT count is fetched a bit different
else if ($mode == 'RECENT') {
$count = $this->conn->countRecent($folder);
}
// use SEARCH for message counting
else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) {
$search_str = "ALL UNDELETED";
$keys = array('COUNT');
if ($mode == 'UNSEEN') {
$search_str .= " UNSEEN";
}
else {
if ($this->messages_caching) {
$keys[] = 'ALL';
}
if ($status) {
$keys[] = 'MAX';
}
}
// @TODO: if $mode == 'ALL' we could try to use cache index here
// get message count using (E)SEARCH
// not very performant but more precise (using UNDELETED)
$index = $this->conn->search($folder, $search_str, true, $keys);
$count = $index->count();
if ($mode == 'ALL') {
// Cache index data, will be used in index_direct()
$this->icache['undeleted_idx'] = $index;
if ($status) {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $index->max());
}
}
}
else {
if ($mode == 'UNSEEN') {
$count = $this->conn->countUnseen($folder);
}
else {
$count = $this->conn->countMessages($folder);
if ($status && $mode == 'ALL') {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0);
}
}
}
$count = (int) $count;
if (!isset($a_folder_cache[$folder][$mode]) || $a_folder_cache[$folder][$mode] !== $count) {
$a_folder_cache[$folder][$mode] = $count;
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
}
return $count;
}
/**
* Public method for listing message flags
*
* @param string $folder Folder name
* @param array $uids Message UIDs
* @param int $mod_seq Optional MODSEQ value (of last flag update)
*
* @return array Indexed array with message flags
*/
public function list_flags($folder, $uids, $mod_seq = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return array();
}
// @TODO: when cache was synchronized in this request
// we might already have asked for flag updates, use it.
$flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
$result = array();
if (!empty($flags)) {
foreach ($flags as $message) {
$result[$message->uid] = $message->flags;
}
}
return $result;
}
/**
* Public method for listing headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
}
/**
* protected method for listing message headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($folder)) {
return array();
}
$this->set_sort_order($sort_field, $sort_order);
$page = $page ? $page : $this->list_page;
// use saved message set
if ($this->search_string) {
return $this->list_search_messages($folder, $page, $slice);
}
if ($this->threading) {
return $this->list_thread_messages($folder, $page, $slice);
}
// get UIDs of all messages in the folder, sorted
$index = $this->index($folder, $this->sort_field, $this->sort_order);
if ($index->is_empty()) {
return array();
}
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$index->slice($from, $to - $from);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch reqested messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
/**
* protected method for listing message headers using threads
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function list_thread_messages($folder, $page, $slice=0)
{
// get all threads (not sorted)
if ($mcache = $this->get_mcache_engine()) {
$threads = $mcache->get_thread($folder);
}
else {
$threads = $this->threads($folder);
}
return $this->fetch_thread_headers($folder, $threads, $page, $slice);
}
/**
* Method for fetching threads data
*
* @param string $folder Folder name
*
* @return rcube_result_thread Thread data object
*/
function threads($folder)
{
if ($mcache = $this->get_mcache_engine()) {
// don't store in self's internal cache, cache has it's own internal cache
return $mcache->get_thread($folder);
}
if (!empty($this->icache['threads'])) {
if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
return $this->icache['threads'];
}
}
// get all threads
$result = $this->threads_direct($folder);
// add to internal (fast) cache
return $this->icache['threads'] = $result;
}
/**
* Method for direct fetching of threads data
*
* @param string $folder Folder name
*
* @return rcube_result_thread Thread data object
*/
function threads_direct($folder)
{
if (!$this->check_connection()) {
return new rcube_result_thread();
}
// get all threads
return $this->conn->thread($folder, $this->threading,
$this->options['skip_deleted'] ? 'UNDELETED' : '', true);
}
/**
* protected method for fetching threaded messages headers
*
* @param string $folder Folder name
* @param rcube_result_thread $threads Threads data object
* @param int $page List page number
* @param int $slice Number of threads to slice
*
* @return array Messages headers
*/
protected function fetch_thread_headers($folder, $threads, $page, $slice=0)
{
// Sort thread structure
$this->sort_threads($threads);
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$threads->slice($from, $to - $from);
if ($slice) {
$threads->slice(-$slice, $slice);
}
// Get UIDs of all messages in all threads
$a_index = $threads->get();
// fetch reqested headers from server
$a_msg_headers = $this->fetch_headers($folder, $a_index);
unset($a_index);
// Set depth, has_children and unread_children fields in headers
$this->set_thread_flags($a_msg_headers, $threads);
return array_values($a_msg_headers);
}
/**
* protected method for setting threaded messages flags:
* depth, has_children, unread_children, flagged_children
*
* @param array $headers Reference to headers array indexed by message UID
* @param rcube_result_thread $threads Threads data object
*
* @return array Message headers array indexed by message UID
*/
protected function set_thread_flags(&$headers, $threads)
{
$parents = array();
list ($msg_depth, $msg_children) = $threads->get_thread_data();
foreach ($headers as $uid => $header) {
$depth = $msg_depth[$uid];
$parents = array_slice($parents, 0, $depth);
if (!empty($parents)) {
$headers[$uid]->parent_uid = end($parents);
if (empty($header->flags['SEEN'])) {
$headers[$parents[0]]->unread_children++;
}
if (!empty($header->flags['FLAGGED'])) {
$headers[$parents[0]]->flagged_children++;
}
}
array_push($parents, $uid);
$headers[$uid]->depth = $depth;
$headers[$uid]->has_children = $msg_children[$uid];
}
}
/**
* A protected method for listing a set of message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from the result array
*
* @return array Indexed array with message header objects
*/
protected function list_search_messages($folder, $page, $slice = 0)
{
if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
return array();
}
$from = ($page-1) * $this->page_size;
// gather messages from a multi-folder search
if ($this->search_set->multi) {
$page_size = $this->page_size;
$sort_field = $this->sort_field;
$search_set = $this->search_set;
// fetch resultset headers, sort and slice them
if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) {
$this->sort_field = null;
$this->page_size = 1000; // fetch up to 1000 matching messages per folder
$this->threading = false;
$a_msg_headers = array();
foreach ($search_set->sets as $resultset) {
if (!$resultset->is_empty()) {
$this->search_set = $resultset;
$this->search_threads = $resultset instanceof rcube_result_thread;
$a_headers = $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1);
$a_msg_headers = array_merge($a_msg_headers, $a_headers);
unset($a_headers);
}
}
// sort headers
if (!empty($a_msg_headers)) {
$a_msg_headers = rcube_imap_generic::sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
}
// store (sorted) message index
$search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
// only return the requested part of the set
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $page_size);
}
else {
if ($this->sort_order != $search_set->get_parameters('ORDER')) {
$search_set->revert();
}
// slice resultset first...
$index = array_slice($search_set->get(), $from, $page_size);
$fetch = array();
foreach ($index as $msg_id) {
list($uid, $folder) = explode('-', $msg_id, 2);
$fetch[$folder][] = $uid;
}
// ... and fetch the requested set of headers
$a_msg_headers = array();
foreach ($fetch as $folder => $a_index) {
$a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
}
// Re-sort the result according to the original search set order
usort($a_msg_headers, function($a, $b) use ($index) {
return array_search($a->uid . '-' . $a->folder, $index) - array_search($b->uid . '-' . $b->folder, $index);
});
}
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
// restore members
$this->sort_field = $sort_field;
$this->page_size = $page_size;
$this->search_set = $search_set;
return $a_msg_headers;
}
// use saved messages from searching
if ($this->threading) {
return $this->list_search_thread_messages($folder, $page, $slice);
}
// search set is threaded, we need a new one
if ($this->search_threads) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
$index = clone $this->search_set;
// return empty array if no messages found
if ($index->is_empty()) {
return array();
}
// quickest method (default sorting)
if (!$this->search_sort_field && !$this->sort_field) {
$got_index = true;
}
// sorted messages, so we can first slice array and then fetch only wanted headers
else if ($this->search_sorted) { // SORT searching result
$got_index = true;
// reset search set if sorting field has been changed
if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
$index = clone $this->search_set;
// return empty array if no messages found
if ($index->is_empty()) {
return array();
}
}
}
if (!empty($got_index)) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
// get messages uids for one page
$index->slice($from, $this->page_size);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
// SEARCH result, need sorting
$cnt = $index->count();
// 300: experimantal value for best result
if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
// use memory less expensive (and quick) method for big result set
$index = clone $this->index('', $this->sort_field, $this->sort_order);
// get messages uids for one page...
$index->slice($from, $this->page_size);
if ($slice) {
$index->slice(-$slice, $slice);
}
// ...and fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
else {
// for small result set we can fetch all messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index, false);
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers)) {
return array();
}
// if not already sorted
$a_msg_headers = rcube_imap_generic::sortHeaders(
$a_msg_headers, $this->sort_field, $this->sort_order);
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $this->page_size);
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
return $a_msg_headers;
}
}
/**
* protected method for listing a set of threaded message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_search_messages()
*/
protected function list_search_thread_messages($folder, $page, $slice=0)
{
// update search_set if previous data was fetched with disabled threading
if (!$this->search_threads) {
if ($this->search_set->is_empty()) {
return array();
}
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
}
/**
* Fetches messages headers (by UID)
*
* @param string $folder Folder name
* @param array $msgs Message UIDs
* @param bool $sort Enables result sorting by $msgs
* @param bool $force Disables cache use
*
* @return array Messages headers indexed by UID
*/
function fetch_headers($folder, $msgs, $sort = true, $force = false)
{
if (empty($msgs)) {
return array();
}
if (!$force && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_messages($folder, $msgs);
}
else if (!$this->check_connection()) {
return array();
}
else {
// fetch reqested headers from server
$headers = $this->conn->fetchHeaders(
$folder, $msgs, true, false, $this->get_fetch_headers());
}
if (empty($headers)) {
return array();
}
$msg_headers = array();
foreach ($headers as $h) {
$h->folder = $folder;
$msg_headers[$h->uid] = $h;
}
if ($sort) {
// use this class for message sorting
$sorter = new rcube_message_header_sorter();
$sorter->set_index($msgs);
$sorter->sort_headers($msg_headers);
}
return $msg_headers;
}
/**
* Returns current status of a folder (compared to the last time use)
*
* We compare the maximum UID to determine the number of
* new messages because the RECENT flag is not reliable.
*
* @param string $folder Folder name
* @param array $diff Difference data
*
* @return int Folder status
*/
public function folder_status($folder = null, &$diff = array())
{
if (!strlen($folder)) {
$folder = $this->folder;
}
$old = $this->get_folder_stats($folder);
// refresh message count -> will update
$this->countmessages($folder, 'ALL', true, true, true);
$result = 0;
if (empty($old)) {
return $result;
}
$new = $this->get_folder_stats($folder);
// got new messages
if ($new['maxuid'] > $old['maxuid']) {
$result += 1;
// get new message UIDs range, that can be used for example
// to get the data of these messages
$diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid'];
}
// some messages has been deleted
if ($new['cnt'] < $old['cnt']) {
$result += 2;
}
// @TODO: optional checking for messages flags changes (?)
// @TODO: UIDVALIDITY checking
return $result;
}
/**
* Stores folder statistic data in session
* @TODO: move to separate DB table (cache?)
*
* @param string $folder Folder name
* @param string $name Data name
* @param mixed $data Data value
*/
protected function set_folder_stats($folder, $name, $data)
{
$_SESSION['folders'][$folder][$name] = $data;
}
/**
* Gets folder statistic data
*
* @param string $folder Folder name
*
* @return array Stats data
*/
protected function get_folder_stats($folder)
{
if (isset($_SESSION['folders'][$folder])) {
return (array) $_SESSION['folders'][$folder];
}
return array();
}
/**
* Return sorted list of message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param bool $no_threads Get not threaded index
* @param bool $no_search Get index not limited to search result (optionally)
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
$no_threads = false, $no_search = false
) {
if (!$no_threads && $this->threading) {
return $this->thread_index($folder, $sort_field, $sort_order);
}
$this->set_sort_order($sort_field, $sort_order);
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string) {
if ($this->search_set->is_empty()) {
return new rcube_result_index($folder, '* SORT');
}
if ($this->search_set instanceof rcube_result_multifolder) {
$index = $this->search_set;
$index->folder = $folder;
// TODO: handle changed sorting
}
// search result is an index with the same sorting?
else if (($this->search_set instanceof rcube_result_index)
&& ((!$this->sort_field && !$this->search_sorted) ||
($this->search_sorted && $this->search_sort_field == $this->sort_field))
) {
$index = $this->search_set;
}
// $no_search is enabled when we are not interested in
// fetching index for search result, e.g. to sort
// threaded search result we can use full mailbox index.
// This makes possible to use index from cache
else if (!$no_search) {
if (!$this->sort_field) {
// No sorting needed, just build index from the search result
// @TODO: do we need to sort by UID here?
$search = $this->search_set->get_compressed();
$index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
}
else {
$index = $this->index_direct($folder, $this->sort_field, $this->sort_order, $this->search_set);
}
}
if (isset($index)) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
}
// check local cache
if ($mcache = $this->get_mcache_engine()) {
return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
}
// fetch from IMAP server
return $this->index_direct($folder, $this->sort_field, $this->sort_order);
}
/**
* Return sorted list of message UIDs ignoring current search settings.
* Doesn't uses cache by default.
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param rcube_result_* $search Optional messages set to limit the result
*
* @return rcube_result_index Sorted list of message UIDs
*/
public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
{
if (!empty($search)) {
$search = $search->get_compressed();
}
// use message index sort as default sorting
if (!$sort_field) {
// use search result from count() if possible
if (empty($search) && $this->options['skip_deleted']
&& !empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('ALL') !== null
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$index = $this->icache['undeleted_idx'];
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
else {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->search($folder, $query, true);
}
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
// fetch complete message index
else {
if ($this->get_capability('SORT')) {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->sort($folder, $sort_field, $query, true);
}
if (empty($index) || $index->is_error()) {
$index = $this->conn->index($folder, $search ? $search : "1:*",
$sort_field, $this->options['skip_deleted'],
$search ? true : false, true);
}
}
if ($sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
/**
* Return index of threaded message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
*
* @return rcube_result_thread Message UIDs
*/
public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string && $this->search_threads && $folder == $this->folder) {
$threads = $this->search_set;
}
else {
// get all threads (default sort order)
$threads = $this->threads($folder);
}
$this->set_sort_order($sort_field, $sort_order);
$this->sort_threads($threads);
return $threads;
}
/**
* Sort threaded result, using THREAD=REFS method if available.
* If not, use any method and re-sort the result in THREAD=REFS way.
*
* @param rcube_result_thread $threads Threads result set
*/
protected function sort_threads($threads)
{
if ($threads->is_empty()) {
return;
}
// THREAD=ORDEREDSUBJECT: sorting by sent date of root message
// THREAD=REFERENCES: sorting by sent date of root message
// THREAD=REFS: sorting by the most recent date in each thread
if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
$sortby = $this->sort_field ? $this->sort_field : 'date';
$index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
if (!$index->is_empty()) {
$threads->sort($index);
}
}
else if ($this->sort_order != $threads->get_parameters('ORDER')) {
$threads->revert();
}
}
/**
* Invoke search request to IMAP server
*
* @param string $folder Folder name to search in
* @param string $search Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
*
* @return rcube_result_index Search result object
* @todo: Search criteria should be provided in non-IMAP format, eg. array
*/
public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
{
if (!$search) {
$search = 'ALL';
}
if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
$folder = $this->folder;
}
$plugin = $this->plugins->exec_hook('imap_search_before', array(
'folder' => $folder,
'search' => $search,
'charset' => $charset,
'sort_field' => $sort_field,
'threading' => $this->threading,
));
$folder = $plugin['folder'];
$search = $plugin['search'];
$charset = $plugin['charset'];
$sort_field = $plugin['sort_field'];
$results = $plugin['result'];
// multi-folder search
if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
// connect IMAP to have all the required classes and settings loaded
$this->check_connection();
// disable threading
$this->threading = false;
$searcher = new rcube_imap_search($this->options, $this->conn);
// set limit to not exceed the client's request timeout
$searcher->set_timelimit(60);
// continue existing incomplete search
if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) {
$searcher->set_results($this->search_set);
}
// execute the search
$results = $searcher->exec(
$folder,
$search,
$charset ? $charset : $this->default_charset,
$sort_field && $this->get_capability('SORT') ? $sort_field : null,
$this->threading
);
}
else if (!$results) {
$folder = is_array($folder) ? $folder[0] : $folder;
$search = is_array($search) ? $search[$folder] : $search;
$results = $this->search_index($folder, $search, $charset, $sort_field);
}
$sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
$this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
return $results;
}
/**
* Direct (real and simple) SEARCH request (without result sorting and caching).
*
* @param string $mailbox Mailbox name to search in
* @param string $str Search string
*
* @return rcube_result_index Search result (UIDs)
*/
public function search_once($folder = null, $str = 'ALL')
{
if (!$this->check_connection()) {
return new rcube_result_index();
}
if (!$str) {
$str = 'ALL';
}
// multi-folder search
if (is_array($folder) && count($folder) > 1) {
$searcher = new rcube_imap_search($this->options, $this->conn);
$index = $searcher->exec($folder, $str, $this->default_charset);
}
else {
$folder = is_array($folder) ? $folder[0] : $folder;
if (!strlen($folder)) {
$folder = $this->folder;
}
$index = $this->conn->search($folder, $str, true);
}
return $index;
}
/**
* protected search method
*
* @param string $folder Folder name
* @param string $criteria Search criteria
* @param string $charset Charset
* @param string $sort_field Sorting field
*
* @return rcube_result_index|rcube_result_thread Search results (UIDs)
* @see rcube_imap::search()
*/
protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
{
if (!$this->check_connection()) {
if ($this->threading) {
return new rcube_result_thread();
}
else {
return new rcube_result_index();
}
}
if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
$criteria = 'UNDELETED '.$criteria;
}
// unset CHARSET if criteria string is ASCII, this way
// SEARCH won't be re-sent after "unsupported charset" response
if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
$charset = 'US-ASCII';
}
if ($this->threading) {
$threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen that Courier doesn't support UTF-8)
if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
$threads = $this->conn->thread($folder, $this->threading,
self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
return $threads;
}
if ($sort_field && $this->get_capability('SORT')) {
$charset = $charset ? $charset : $this->default_charset;
$messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen Courier with disabled UTF-8 support)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->sort($folder, $sort_field,
self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
if (!$messages->is_error()) {
$this->search_sorted = true;
return $messages;
}
}
$messages = $this->conn->search($folder,
($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
// Error, try with US-ASCII (some servers may support only US-ASCII)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->search($folder,
self::convert_criteria($criteria, $charset), true);
}
$this->search_sorted = false;
return $messages;
}
/**
* Converts charset of search criteria string
*
* @param string $str Search string
* @param string $charset Original charset
* @param string $dest_charset Destination charset (default US-ASCII)
*
* @return string Search string
*/
public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
{
// convert strings to US_ASCII
if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
$last = 0; $res = '';
foreach ($matches[1] as $m) {
$string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
$string = substr($str, $string_offset - 1, $m[0]);
$string = rcube_charset::convert($string, $charset, $dest_charset);
if ($string === false || !strlen($string)) {
continue;
}
$res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
$last = $m[0] + $string_offset - 1;
}
if ($last < strlen($str)) {
$res .= substr($str, $last, strlen($str)-$last);
}
}
// strings for conversion not found
else {
$res = $str;
}
return $res;
}
/**
* Refresh saved search set
*
* @return array Current search set
*/
public function refresh_search()
{
if (!empty($this->search_string)) {
$this->search(
is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
$this->search_string,
$this->search_charset,
$this->search_sort_field
);
}
return $this->get_search_set();
}
/**
* Flag certain result subsets as 'incomplete'.
* For subsequent refresh_search() calls to only refresh the updated parts.
*/
protected function set_search_dirty($folder)
{
if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
if ($subset = $this->search_set->get_set($folder)) {
$subset->incomplete = $this->search_set->incomplete = true;
}
}
}
/**
* Return message headers object of a specific message
*
* @param int $id Message UID
* @param string $folder Folder to read from
* @param bool $force True to skip cache
*
* @return rcube_message_header Message headers
*/
public function get_message_headers($uid, $folder = null, $force = false)
{
// decode combined UID-folder identifier
if (preg_match('/^\d+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
if (!strlen($folder)) {
$folder = $this->folder;
}
// get cached headers
if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_message($folder, $uid);
}
else if (!$this->check_connection()) {
$headers = false;
}
else {
$headers = $this->conn->fetchHeader(
$folder, $uid, true, true, $this->get_fetch_headers());
if (is_object($headers))
$headers->folder = $folder;
}
return $headers;
}
/**
* Fetch message headers and body structure from the IMAP server and build
* an object structure.
*
* @param int $uid Message UID to fetch
* @param string $folder Folder to read from
*
* @return object rcube_message_header Message data
*/
public function get_message($uid, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// decode combined UID-folder identifier
if (preg_match('/^\d+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
// Check internal cache
if (!empty($this->icache['message']) && ($headers = $this->icache['message'])) {
// Make sure the folder and UID is what we expect.
// In case when the same process works with folders that are personal
// and shared two folders can contain the same UIDs.
if ($headers->uid == $uid && $headers->folder == $folder) {
return $headers;
}
}
$headers = $this->get_message_headers($uid, $folder);
// message doesn't exist?
if (empty($headers)) {
return null;
}
// structure might be cached
if (!empty($headers->structure)) {
return $headers;
}
$this->msg_uid = $uid;
if (!$this->check_connection()) {
return $headers;
}
if (empty($headers->bodystructure)) {
$headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
}
$structure = $headers->bodystructure;
if (empty($structure)) {
return $headers;
}
// set message charset from message headers
if ($headers->charset) {
$this->struct_charset = $headers->charset;
}
else {
$this->struct_charset = $this->structure_charset($structure);
}
$headers->ctype = @strtolower($headers->ctype);
// Here we can recognize malformed BODYSTRUCTURE and
// 1. [@TODO] parse the message in other way to create our own message structure
// 2. or just show the raw message body.
// Example of structure for malformed MIME message:
// ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
&& strtolower($structure[0].'/'.$structure[1]) == 'text/plain'
) {
// A special known case "Content-type: text" (#1488968)
if ($headers->ctype == 'text') {
$structure[1] = 'plain';
$headers->ctype = 'text/plain';
}
// we can handle single-part messages, by simple fix in structure (#1486898)
else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
$structure[0] = $m[1];
$structure[1] = $m[2];
}
else {
// Try to parse the message using rcube_mime_decode.
// We need a better solution, it parses message
// in memory, which wouldn't work for very big messages,
// (it uses up to 10x more memory than the message size)
// it's also buggy and not actively developed
if ($headers->size && rcube_utils::mem_check($headers->size * 10)) {
$raw_msg = $this->get_raw_body($uid);
$struct = rcube_mime::parse_message($raw_msg);
}
else {
return $headers;
}
}
}
if (empty($struct)) {
$struct = $this->structure_part($structure, 0, '', $headers);
}
// some workarounds on simple messages...
if (empty($struct->parts)) {
// ...don't trust given content-type
if (!empty($headers->ctype)) {
$struct->mime_id = '1';
$struct->mimetype = strtolower($headers->ctype);
list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
}
// ...and charset (there's a case described in #1488968 where invalid content-type
// results in invalid charset in BODYSTRUCTURE)
if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) {
$struct->charset = $headers->charset;
$struct->ctype_parameters['charset'] = $headers->charset;
}
}
$headers->structure = $struct;
return $this->icache['message'] = $headers;
}
/**
* Build message part object
*
* @param array $part
* @param int $count
* @param string $parent
*/
protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null)
{
$struct = new rcube_message_part;
$struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
// multipart
if (is_array($part[0])) {
$struct->ctype_primary = 'multipart';
/* RFC3501: BODYSTRUCTURE fields of multipart part
part1 array
part2 array
part3 array
....
1. subtype
2. parameters (optional)
3. description (optional)
4. language (optional)
5. location (optional)
*/
// find first non-array entry
for ($i=1; $i<count($part); $i++) {
if (!is_array($part[$i])) {
$struct->ctype_secondary = strtolower($part[$i]);
// read content type parameters
if (is_array($part[$i+1])) {
$struct->ctype_parameters = array();
for ($j=0; $j<count($part[$i+1]); $j+=2) {
$param = strtolower($part[$i+1][$j]);
$struct->ctype_parameters[$param] = $part[$i+1][$j+1];
}
}
break;
}
}
$struct->mimetype = 'multipart/'.$struct->ctype_secondary;
// build parts list for headers pre-fetching
for ($i=0; $i<count($part); $i++) {
// fetch message headers if message/rfc822 or named part
if (is_array($part[$i]) && !is_array($part[$i][0])) {
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
$mime_part_headers[] = $tmp_part_id;
}
else if (!empty($part[$i][2]) && empty($part[$i][3])) {
$params = array_map('strtolower', (array) $part[$i][2]);
$find = array('name', 'filename', 'name*', 'filename*', 'name*0', 'filename*0', 'name*0*', 'filename*0*');
// In case of malformed header check disposition. E.g. some servers for
// "Content-Type: PDF; name=test.pdf" may return text/plain and ignore name argument
if (count(array_intersect($params, $find)) > 0
|| (is_array($part[$i][9]) && stripos($part[$i][9][0], 'attachment') === 0)
) {
$mime_part_headers[] = $tmp_part_id;
}
}
}
}
// pre-fetch headers of all parts (in one command for better performance)
// @TODO: we could do this before _structure_part() call, to fetch
// headers for parts on all levels
if (!empty($mime_part_headers)) {
$mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder,
$this->msg_uid, $mime_part_headers);
}
$struct->parts = array();
for ($i=0, $count=0; $i<count($part); $i++) {
if (!is_array($part[$i])) {
break;
}
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
$struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id,
!empty($mime_part_headers[$tmp_part_id]) ? $mime_part_headers[$tmp_part_id] : null);
}
return $struct;
}
/* RFC3501: BODYSTRUCTURE fields of non-multipart part
0. type
1. subtype
2. parameters
3. id
4. description
5. encoding
6. size
-- text
7. lines
-- message/rfc822
7. envelope structure
8. body structure
9. lines
--
x. md5 (optional)
x. disposition (optional)
x. language (optional)
x. location (optional)
*/
// regular part
$struct->ctype_primary = strtolower($part[0]);
$struct->ctype_secondary = strtolower($part[1]);
$struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
// read content type parameters
if (is_array($part[2])) {
$struct->ctype_parameters = array();
for ($i=0; $i<count($part[2]); $i+=2) {
$struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
}
if (isset($struct->ctype_parameters['charset'])) {
$struct->charset = $struct->ctype_parameters['charset'];
}
}
// #1487700: workaround for lack of charset in malformed structure
if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
$struct->charset = $mime_headers->charset;
}
// read content encoding
if (!empty($part[5])) {
$struct->encoding = strtolower($part[5]);
$struct->headers['content-transfer-encoding'] = $struct->encoding;
}
// get part size
if (!empty($part[6])) {
$struct->size = intval($part[6]);
}
// read part disposition
$di = 8;
if ($struct->ctype_primary == 'text') {
$di += 1;
}
else if ($struct->mimetype == 'message/rfc822') {
$di += 3;
}
if (is_array($part[$di]) && count($part[$di]) == 2) {
$struct->disposition = strtolower($part[$di][0]);
if ($struct->disposition && $struct->disposition !== 'inline' && $struct->disposition !== 'attachment') {
// RFC2183, Section 2.8 - unrecognized type should be treated as "attachment"
$struct->disposition = 'attachment';
}
if (is_array($part[$di][1])) {
for ($n=0; $n<count($part[$di][1]); $n+=2) {
$struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
}
}
}
// get message/rfc822's child-parts
if (is_array($part[8]) && $di != 8) {
$struct->parts = array();
for ($i=0, $count=0; $i<count($part[8]); $i++) {
if (!is_array($part[8][$i])) {
break;
}
$struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id);
}
}
// get part ID
if (!empty($part[3])) {
$struct->content_id = $struct->headers['content-id'] = trim($part[3]);
if (empty($struct->disposition)) {
$struct->disposition = 'inline';
}
}
// fetch message headers if message/rfc822 or named part (could contain Content-Location header)
if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
if (empty($mime_headers)) {
$mime_headers = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $struct->mime_id);
}
if (is_string($mime_headers)) {
$struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
}
else if (is_object($mime_headers)) {
$struct->headers = get_object_vars($mime_headers) + $struct->headers;
}
// get real content-type of message/rfc822
if ($struct->mimetype == 'message/rfc822') {
// single-part
if (!is_array($part[8][0])) {
$struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
}
// multi-part
else {
for ($n=0; $n<count($part[8]); $n++) {
if (!is_array($part[8][$n])) {
break;
}
}
$struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
}
}
if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
if (is_array($part[8]) && $di != 8) {
$struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id);
}
}
}
// normalize filename property
$this->set_part_filename($struct, $mime_headers);
return $struct;
}
/**
* Set attachment filename from message part structure
*
* @param rcube_message_part $part Part object
* @param string $headers Part's raw headers
*/
protected function set_part_filename(&$part, $headers = null)
{
// Some IMAP servers do not support RFC2231, if we have
// part headers we'll get attachment name from them, not the BODYSTRUCTURE
$rfc2231_params = array();
if (!empty($headers) || !empty($part->headers)) {
if (is_object($headers)) {
$headers = get_object_vars($headers);
}
else {
$headers = !empty($headers) ? rcube_mime::parse_headers($headers) : $part->headers;
}
$tokens = preg_split('/;[\s\r\n\t]*/', $headers['content-type'] . ';' . $headers['content-disposition']);
foreach ($tokens as $token) {
// TODO: Use order defined by the parameter name not order of occurrence in the header
if (preg_match('/^(name|filename)\*([0-9]*)\*?="*([^"]+)"*/i', $token, $matches)) {
$rfc2231_params[strtolower($matches[1])] .= $matches[3];
}
}
}
if (isset($rfc2231_params['name'])) {
$filename_encoded = $rfc2231_params['name'];
}
else if (isset($rfc2231_params['filename'])) {
$filename_encoded = $rfc2231_params['filename'];
}
else if (!empty($part->d_parameters['filename'])) {
$filename_mime = $part->d_parameters['filename'];
}
// read 'name' after rfc2231 parameters as it may contain truncated filename (from Thunderbird)
else if (!empty($part->ctype_parameters['name'])) {
$filename_mime = $part->ctype_parameters['name'];
}
// Content-Disposition
else if (!empty($part->headers['content-description'])) {
$filename_mime = $part->headers['content-description'];
}
else {
return;
}
// decode filename
if (isset($filename_mime)) {
if (!empty($part->charset)) {
$charset = $part->charset;
}
else if (!empty($this->struct_charset)) {
$charset = $this->struct_charset;
}
else {
$charset = rcube_charset::detect($filename_mime, $this->default_charset);
}
$part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
}
else if (isset($filename_encoded)) {
// decode filename according to RFC 2231, Section 4
if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
$filename_charset = $fmatches[1];
$filename_encoded = $fmatches[2];
}
$part->filename = urldecode($filename_encoded);
if (!empty($filename_charset)) {
$part->filename = rcube_charset::convert($part->filename, $filename_charset);
}
}
// Workaround for invalid Content-Type (#6816)
// Some servers for "Content-Type: PDF; name=test.pdf" may return text/plain and ignore name argument
if ($part->mimetype == 'text/plain' && !empty($headers['content-type'])) {
$tokens = preg_split('/;[\s\r\n\t]*/', $headers['content-type']);
$type = rcube_mime::fix_mimetype($tokens[0]);
if ($type != $part->mimetype) {
$part->mimetype = $type;
list($part->ctype_primary, $part->ctype_secondary) = explode('/', $part->mimetype);
}
}
}
/**
* Get charset name from message structure (first part)
*
* @param array $structure Message structure
*
* @return string Charset name
*/
protected function structure_charset($structure)
{
while (is_array($structure)) {
if (is_array($structure[2]) && $structure[2][0] == 'charset') {
return $structure[2][1];
}
$structure = $structure[0];
}
}
/**
* Fetch message body of a specific message from the server
*
* @param int Message UID
* @param string Part number
* @param rcube_message_part Part object created by get_structure()
* @param mixed True to print part, resource to write part contents in
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
* @param int Only read this number of bytes
* @param boolean Enables formatting of text/* parts bodies
*
* @return string Message/part body if not printed
*/
public function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null,
$skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if (!$this->check_connection()) {
return null;
}
// get part data if not provided
if (!is_object($o_part)) {
$structure = $this->conn->getStructure($this->folder, $uid, true);
$part_data = rcube_imap_generic::getStructurePartData($structure, $part);
$o_part = new rcube_message_part;
$o_part->ctype_primary = $part_data['type'];
$o_part->ctype_secondary = $part_data['subtype'];
$o_part->encoding = $part_data['encoding'];
$o_part->charset = $part_data['charset'];
$o_part->size = $part_data['size'];
}
$body = '';
// Note: multipart/* parts will have size=0, we don't want to ignore them
if ($o_part && ($o_part->size || $o_part->ctype_primary == 'multipart')) {
$formatted = $formatted && $o_part->ctype_primary == 'text';
$body = $this->conn->handlePartBody($this->folder, $uid, true,
$part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
}
if ($fp || $print) {
return true;
}
// convert charset (if text or message part)
if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
// Remove NULL characters if any (#1486189)
if ($formatted && strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
if (!$skip_charset_conv) {
if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
// try to extract charset information from HTML meta tag (#1488125)
if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
$o_part->charset = strtoupper($m[1]);
}
else {
$o_part->charset = $this->default_charset;
}
}
$body = rcube_charset::convert($body, $o_part->charset);
}
}
return $body;
}
/**
* Returns the whole message source as string (or saves to a file)
*
* @param int $uid Message UID
* @param resource $fp File pointer to save the message
* @param string $part Optional message part ID
*
* @return string Message source string
*/
public function get_raw_body($uid, $fp=null, $part = null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->handlePartBody($this->folder, $uid,
true, $part, null, false, $fp);
}
/**
* Returns the message headers as string
*
* @param int $uid Message UID
* @param string $part Optional message part ID
*
* @return string Message headers string
*/
public function get_raw_headers($uid, $part = null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->fetchPartHeader($this->folder, $uid, true, $part);
}
/**
* Sends the whole message source to stdout
*
* @param int $uid Message UID
* @param bool $formatted Enables line-ending formatting
*/
public function print_raw_body($uid, $formatted = true)
{
if (!$this->check_connection()) {
return;
}
$this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
}
/**
* Set message flag to one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
* @param boolean $skip_cache True to skip message cache clean up
*
* @return boolean Operation status
*/
public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
$flag = strtoupper($flag);
list($uids, $all_mode) = $this->parse_uids($uids);
if (strpos($flag, 'UN') === 0) {
$result = $this->conn->unflag($folder, $uids, substr($flag, 2));
}
else {
$result = $this->conn->flag($folder, $uids, $flag);
}
if ($result && !$skip_cache) {
// reload message headers if cached
// update flags instead removing from cache
if ($mcache = $this->get_mcache_engine()) {
$status = strpos($flag, 'UN') !== 0;
$mflag = preg_replace('/^UN/', '', $flag);
$mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
$mflag, $status);
}
// clear cached counters
if ($flag == 'SEEN' || $flag == 'UNSEEN') {
$this->clear_messagecount($folder, array('SEEN', 'UNSEEN'));
}
else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
$this->clear_messagecount($folder, array('ALL', 'THREADS'));
if ($this->options['skip_deleted']) {
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
}
$this->set_search_dirty($folder);
}
return $result;
}
/**
* Append a mail message (source) to a specific folder
*
* @param string $folder Target folder
* @param string|array $message The message source string or filename
* or array (of strings and file pointers)
* @param string $headers Headers string if $message contains only the body
* @param boolean $is_file True if $message is a filename
* @param array $flags Message flags
* @param mixed $date Message internal date
* @param bool $binary Enables BINARY append
*
* @return int|bool Appended message UID or True on success, False on error
*/
public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// make sure folder exists
if (!$this->folder_exists($folder)) {
return false;
}
$date = $this->date_format($date);
if ($is_file) {
$saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary);
}
else {
$saved = $this->conn->append($folder, $message, $flags, $date, $binary);
}
if ($saved) {
// increase messagecount of the target folder
$this->set_messagecount($folder, 'ALL', 1);
$this->plugins->exec_hook('message_saved', array(
'folder' => $folder,
'message' => $message,
'headers' => $headers,
'is_file' => $is_file,
'flags' => $flags,
'date' => $date,
'binary' => $binary,
'result' => $saved,
));
}
return $saved;
}
/**
* Move a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return boolean True on success, False on error
*/
public function move_message($uids, $to_mbox, $from_mbox = '')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
if ($to_mbox === $from_mbox) {
return false;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$config = rcube::get_instance()->config;
$to_trash = $to_mbox == $config->get('trash_mbox');
// flag messages as read before moving them
if ($to_trash && $config->get('read_when_deleted')) {
// don't flush cache (4th argument)
$this->set_flag($uids, 'SEEN', $from_mbox, true);
}
// move messages
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
// when moving to Trash we make sure the folder exists
// as it's uncommon scenario we do this when MOVE fails, not before
if (!$moved && $to_trash && $this->get_response_code() == rcube_storage::TRYCREATE) {
if ($this->create_folder($to_mbox, true, 'trash')) {
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
}
}
if ($moved) {
$this->clear_messagecount($from_mbox);
$this->clear_messagecount($to_mbox);
$this->set_search_dirty($from_mbox);
$this->set_search_dirty($to_mbox);
// unset threads internal cache
unset($this->icache['threads']);
// remove message ids from search set
if ($this->search_set && $from_mbox == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids), $this->folder);
}
}
// remove cached messages
// @TODO: do cache update instead of clearing it
$this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
}
return $moved;
}
/**
* Copy a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return boolean True on success, False on error
*/
public function copy_message($uids, $to_mbox, $from_mbox = '')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
list($uids, ) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
// copy messages
$copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
if ($copied) {
$this->clear_messagecount($to_mbox);
}
return $copied;
}
/**
* Mark messages as deleted and expunge them
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Source folder
*
* @return boolean True on success, False on error
*/
public function delete_message($uids, $folder = '')
{
if (!strlen($folder)) {
$folder = $this->folder;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$deleted = $this->conn->flag($folder, $uids, 'DELETED');
if ($deleted) {
// send expunge command in order to have the deleted message
// really deleted from the folder
$this->expunge_message($uids, $folder, false);
$this->clear_messagecount($folder);
// unset threads internal cache
unset($this->icache['threads']);
$this->set_search_dirty($folder);
// remove message ids from search set
if ($this->search_set && $folder == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids));
}
}
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
return $deleted;
}
/**
* Send IMAP expunge command and clear cache
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on failure
*/
public function expunge_message($uids, $folder = null, $clear_cache = true)
{
if ($uids && $this->get_capability('UIDPLUS')) {
list($uids, $all_mode) = $this->parse_uids($uids);
}
else {
$uids = null;
}
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// force folder selection and check if folder is writeable
// to prevent a situation when CLOSE is executed on closed
// or EXPUNGE on read-only folder
$result = $this->conn->select($folder);
if (!$result) {
return false;
}
if (!$this->conn->data['READ-WRITE']) {
$this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
return false;
}
// CLOSE(+SELECT) should be faster than EXPUNGE
if (empty($uids) || !empty($all_mode)) {
$result = $this->conn->close();
}
else {
$result = $this->conn->expunge($folder, $uids);
}
if ($result && $clear_cache) {
$this->clear_message_cache($folder, !empty($all_mode) ? null : explode(',', $uids));
$this->clear_messagecount($folder);
}
return $result;
}
/* --------------------------------
* folder management
* --------------------------------*/
/**
* Public method for listing subscribed folders.
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
* @param string $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array List of folders
*/
public function list_folders_subscribed($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false)
{
$cache_key = rcube_cache::key_name('mailboxes', array($root, $name, $filter, $rights));
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
$data = $this->plugins->exec_hook('storage_folders',
array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
$a_mboxes = $this->list_folders_subscribed_direct($root, $name);
}
if (!is_array($a_mboxes)) {
return array();
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// INBOX should always be available
if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
&& (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
) {
array_unshift($a_mboxes, 'INBOX');
}
// sort folders (always sort for cache)
if (!$skip_sort || $this->cache) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LSUB)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of subscribed folders
* @see rcube_imap::list_folders_subscribed()
*/
public function list_folders_subscribed_direct($root = '', $name = '*')
{
if (!$this->check_connection()) {
return null;
}
$config = rcube::get_instance()->config;
$list_root = $root === '' && $this->list_root ? $this->list_root : $root;
// Server supports LIST-EXTENDED, we can use selection options
// #1486225: Some dovecot versions return wrong result using LIST-EXTENDED
$list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
if ($list_extended) {
// This will also set folder options, LSUB doesn't do that
$result = $this->conn->listMailboxes($list_root, $name,
NULL, array('SUBSCRIBED'));
}
else {
// retrieve list of folders from IMAP server using LSUB
$result = $this->conn->listSubscribed($list_root, $name);
}
if (!is_array($result)) {
return array();
}
// Add/Remove folders according to some configuration options
$this->list_folders_filter($result, $root . $name, ($list_extended ? 'ext-' : '') . 'subscribed');
// Save the last command state, so we can ignore errors on any following UNSEBSCRIBE calls
$state = $this->save_conn_state();
if ($list_extended) {
// unsubscribe non-existent folders, remove from the list
if ($name == '*' && !empty($this->conn->data['LIST'])) {
foreach ($result as $idx => $folder) {
if (($opts = $this->conn->data['LIST'][$folder])
&& in_array_nocase('\\NonExistent', $opts)
) {
$this->conn->unsubscribe($folder);
unset($result[$idx]);
}
}
}
}
else {
// unsubscribe non-existent folders, remove them from the list
if (!empty($result) && $name == '*') {
$existing = $this->list_folders($root, $name);
// Try to make sure the list of existing folders is not malformed,
// we don't want to unsubscribe existing folders on error
if (is_array($existing) && (!empty($root) || count($existing) > 1)) {
$nonexisting = array_diff($result, $existing);
$result = array_diff($result, $nonexisting);
foreach ($nonexisting as $folder) {
$this->conn->unsubscribe($folder);
}
}
}
}
$this->restore_conn_state($state);
return $result;
}
/**
* Get a list of all folders available on the server
*
* @param string $root IMAP root dir
* @param string $name Optional name pattern
* @param mixed $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array Indexed array with folder names
*/
public function list_folders($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false)
{
$cache_key = rcube_cache::key_name('mailboxes.list', array($root, $name, $filter, $rights));
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
$data = $this->plugins->exec_hook('storage_folders',
array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
// retrieve list of folders from IMAP server
$a_mboxes = $this->list_folders_direct($root, $name);
}
if (!is_array($a_mboxes)) {
$a_mboxes = array();
}
// INBOX should always be available
if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
&& (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
) {
array_unshift($a_mboxes, 'INBOX');
}
// cache folder attributes
if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
$this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// filter folders and sort them
if (!$skip_sort) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LIST)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of folders
* @see rcube_imap::list_folders()
*/
public function list_folders_direct($root = '', $name = '*')
{
if (!$this->check_connection()) {
return null;
}
$list_root = $root === '' && $this->list_root ? $this->list_root : $root;
$result = $this->conn->listMailboxes($list_root, $name);
if (!is_array($result)) {
return array();
}
// Add/Remove folders according to some configuration options
$this->list_folders_filter($result, $root . $name);
return $result;
}
/**
* Apply configured filters on folders list
*/
protected function list_folders_filter(&$result, $root, $update_type = null)
{
$config = rcube::get_instance()->config;
// #1486796: some server configurations doesn't return folders in all namespaces
if ($root === '*' && $config->get('imap_force_ns')) {
$this->list_folders_update($result, $update_type);
}
// Remove hidden folders
if ($config->get('imap_skip_hidden_folders')) {
$result = array_filter($result, function($v) { return $v[0] != '.'; });
}
// Remove folders in shared namespaces (if configured, see self::set_env())
if ($root === '*' && !empty($this->list_excludes)) {
$result = array_filter($result, function($v) {
foreach ($this->list_excludes as $prefix) {
if (strpos($v, $prefix) === 0) {
return false;
}
}
return true;
});
}
}
/**
* Fix folders list by adding folders from other namespaces.
* Needed on some servers eg. Courier IMAP
*
* @param array $result Reference to folders list
* @param string $type Listing type (ext-subscribed, subscribed or all)
*/
protected function list_folders_update(&$result, $type = null)
{
$namespace = $this->get_namespace();
$search = array();
// build list of namespace prefixes
foreach ((array)$namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $ns_data) {
if (strlen($ns_data[0])) {
$search[] = $ns_data[0];
}
}
}
}
if (!empty($search)) {
// go through all folders detecting namespace usage
foreach ($result as $folder) {
foreach ($search as $idx => $prefix) {
if (strpos($folder, $prefix) === 0) {
unset($search[$idx]);
}
}
if (empty($search)) {
break;
}
}
// get folders in hidden namespaces and add to the result
foreach ($search as $prefix) {
if ($type == 'ext-subscribed') {
$list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED'));
}
else if ($type == 'subscribed') {
$list = $this->conn->listSubscribed('', $prefix . '*');
}
else {
$list = $this->conn->listMailboxes('', $prefix . '*');
}
if (!empty($list)) {
$result = array_merge($result, $list);
}
}
}
}
/**
* Filter the given list of folders according to access rights
*
* For performance reasons we assume user has full rights
* on all personal folders.
*/
protected function filter_rights($a_folders, $rights)
{
$regex = '/('.$rights.')/';
foreach ($a_folders as $idx => $folder) {
if ($this->folder_namespace($folder) == 'personal') {
continue;
}
$myrights = implode('', (array)$this->my_rights($folder));
if ($myrights !== null && !preg_match($regex, $myrights)) {
unset($a_folders[$idx]);
}
}
return $a_folders;
}
/**
* Get mailbox quota information
*
* @param string $folder Folder name
*
* @return mixed Quota info or False if not supported
*/
public function get_quota($folder = null)
{
if ($this->get_capability('QUOTA') && $this->check_connection()) {
return $this->conn->getQuota($folder);
}
return false;
}
/**
* Get folder size (size of all messages in a folder)
*
* @param string $folder Folder name
*
* @return int Folder size in bytes, False on error
*/
public function folder_size($folder)
{
if (!strlen($folder)) {
return false;
}
if (!$this->check_connection()) {
return 0;
}
if ($this->get_capability('STATUS=SIZE')) {
$status = $this->conn->status($folder, array('SIZE'));
if (is_array($status) && array_key_exists('SIZE', $status)) {
return (int) $status['SIZE'];
}
}
// On Cyrus we can use special folder annotation, which should be much faster
if ($this->get_vendor() == 'cyrus') {
$idx = '/shared/vendor/cmu/cyrus-imapd/size';
$result = $this->get_metadata($folder, $idx, array(), true);
if (!empty($result) && is_numeric($result[$folder][$idx])) {
return $result[$folder][$idx];
}
}
// @TODO: could we try to use QUOTA here?
$result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
if (is_array($result)) {
$result = array_sum($result);
}
return $result;
}
/**
* Subscribe to a specific folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
public function subscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'subscribe');
}
/**
* Unsubscribe folder(s)
*
* @param array $a_mboxes Folder name(s)
*
* @return boolean True on success
*/
public function unsubscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'unsubscribe');
}
/**
* Create a new folder on the server and register it in local cache
*
* @param string $folder New folder name
* @param boolean $subscribe True if the new folder should be subscribed
* @param string $type Optional folder type (junk, trash, drafts, sent, archive)
* @param boolean $noselect Make the folder a \NoSelect folder by adding hierarchy
* separator at the end (useful for server that do not support
* both folders and messages as folder children)
*
* @return boolean True on success
*/
public function create_folder($folder, $subscribe = false, $type = null, $noselect = false)
{
if (!$this->check_connection()) {
return false;
}
if ($noselect) {
$folder .= $this->delimiter;
}
$result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
// Folder creation may fail when specific special-use flag is not supported.
// Try to create it anyway with no flag specified (#7147)
if (!$result && $type) {
$result = $this->conn->createFolder($folder);
}
// try to subscribe it
if ($result) {
// clear cache
$this->clear_cache('mailboxes', true);
if ($subscribe && !$noselect) {
$this->subscribe($folder);
}
}
return $result;
}
/**
* Set a new name to an existing folder
*
* @param string $folder Folder to rename
* @param string $new_name New folder name
*
* @return boolean True on success
*/
public function rename_folder($folder, $new_name)
{
if (!strlen($new_name)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of subscribed folders
if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
$a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
$subscribed = $this->folder_exists($folder, true);
}
else {
$a_subscribed = $this->list_folders_subscribed();
$subscribed = in_array($folder, $a_subscribed);
}
$result = $this->conn->renameFolder($folder, $new_name);
if ($result) {
// unsubscribe the old folder, subscribe the new one
if ($subscribed) {
$this->conn->unsubscribe($folder);
$this->conn->subscribe($new_name);
}
// check if folder children are subscribed
foreach ($a_subscribed as $c_subscribed) {
if (strpos($c_subscribed, $folder.$delm) === 0) {
$this->conn->unsubscribe($c_subscribed);
$this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
$new_name, $c_subscribed));
// clear cache
$this->clear_message_cache($c_subscribed);
}
}
// clear cache
$this->clear_message_cache($folder);
$this->clear_cache('mailboxes', true);
}
return $result;
}
/**
* Remove folder (with subfolders) from the server
*
* @param string $folder Folder name
*
* @return boolean True on success, False on failure
*/
public function delete_folder($folder)
{
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of sub-folders or all folders
// if folder name contains special characters
$path = strspn($folder, '%*') > 0 ? ($folder . $delm) : '';
$sub_mboxes = $this->list_folders('', $path . '*');
// According to RFC3501 deleting a \Noselect folder
// with subfolders may fail. To workaround this we delete
// subfolders first (in reverse order) (#5466)
if (!empty($sub_mboxes)) {
foreach (array_reverse($sub_mboxes) as $mbox) {
if (strpos($mbox, $folder . $delm) === 0) {
if ($this->conn->deleteFolder($mbox)) {
$this->conn->unsubscribe($mbox);
$this->clear_message_cache($mbox);
}
}
}
}
// delete the folder
if ($result = $this->conn->deleteFolder($folder)) {
// and unsubscribe it
$this->conn->unsubscribe($folder);
$this->clear_message_cache($folder);
}
$this->clear_cache('mailboxes', true);
return $result;
}
/**
* Detect special folder associations stored in storage backend
*/
public function get_special_folders($forced = false)
{
$result = parent::get_special_folders();
$rcube = rcube::get_instance();
// Lock SPECIAL-USE after user preferences change (#4782)
if ($rcube->config->get('lock_special_folders')) {
return $result;
}
if (isset($this->icache['special-use'])) {
return array_merge($result, $this->icache['special-use']);
}
if (!$forced || !$this->get_capability('SPECIAL-USE')) {
return $result;
}
if (!$this->check_connection()) {
return $result;
}
$types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
$special = array();
// request \Subscribed flag in LIST response as performance improvement for folder_exists()
$folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
if (!empty($folders)) {
foreach ($folders as $idx => $folder) {
if (is_array($folder)) {
$folder = $idx;
}
if ($flags = $this->conn->data['LIST'][$folder]) {
foreach ($types as $type) {
if (in_array($type, $flags)) {
$type = strtolower(substr($type, 1));
$special[$type] = $folder;
}
}
}
}
}
$this->icache['special-use'] = $special;
unset($this->icache['special-folders']);
return array_merge($result, $special);
}
/**
* Set special folder associations stored in storage backend
*/
public function set_special_folders($specials)
{
if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$folders = $this->get_special_folders(true);
$old = (array) $this->icache['special-use'];
foreach ($specials as $type => $folder) {
if (in_array($type, rcube_storage::$folder_types)) {
$old_folder = $old[$type];
if ($old_folder !== $folder) {
// unset old-folder metadata
if ($old_folder !== null) {
$this->delete_metadata($old_folder, array('/private/specialuse'));
}
// set new folder metadata
if ($folder) {
$this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
}
}
}
}
$this->icache['special-use'] = $specials;
unset($this->icache['special-folders']);
return true;
}
/**
* Checks if folder exists and is subscribed
*
* @param string $folder Folder name
* @param boolean $subscription Enable subscription checking
*
* @return boolean TRUE or FALSE
*/
public function folder_exists($folder, $subscription = false)
{
if ($folder == 'INBOX') {
return true;
}
$key = $subscription ? 'subscribed' : 'existing';
if (!empty($this->icache[$key]) && in_array($folder, (array) $this->icache[$key])) {
return true;
}
if (!$this->check_connection()) {
return false;
}
if ($subscription) {
// It's possible we already called LIST command, check LIST data
if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
&& in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
) {
$a_folders = array($folder);
}
else {
$a_folders = $this->conn->listSubscribed('', $folder);
}
}
else {
// It's possible we already called LIST command, check LIST data
if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
$a_folders = array($folder);
}
else {
$a_folders = $this->conn->listMailboxes('', $folder);
}
}
if (is_array($a_folders) && in_array($folder, $a_folders)) {
$this->icache[$key][] = $folder;
return true;
}
return false;
}
/**
* Returns the namespace where the folder is in
*
* @param string $folder Folder name
*
* @return string One of 'personal', 'other' or 'shared'
*/
public function folder_namespace($folder)
{
if ($folder == 'INBOX') {
return 'personal';
}
foreach ($this->namespace as $type => $namespace) {
if (is_array($namespace)) {
foreach ($namespace as $ns) {
if ($len = strlen($ns[0])) {
if (($len > 1 && $folder == substr($ns[0], 0, -1))
|| strpos($folder, $ns[0]) === 0
) {
return $type;
}
}
}
}
}
return 'personal';
}
/**
* Modify folder name according to personal namespace prefix.
* For output it removes prefix of the personal namespace if it's possible.
* For input it adds the prefix. Use it before creating a folder in root
* of the folders tree.
*
* @param string $folder Folder name
* @param string $mode Mode name (out/in)
*
* @return string Folder name
*/
public function mod_folder($folder, $mode = 'out')
{
$prefix = $this->namespace['prefix_' . $mode]; // see set_env()
if ($prefix === null || $prefix === ''
|| !($prefix_len = strlen($prefix)) || !strlen($folder)
) {
return $folder;
}
// remove prefix for output
if ($mode == 'out') {
if (substr($folder, 0, $prefix_len) === $prefix) {
return substr($folder, $prefix_len);
}
return $folder;
}
// add prefix for input (e.g. folder creation)
return $prefix . $folder;
}
/**
* Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
*
* @param string $folder Folder name
* @param bool $force Set to True if attributes should be refreshed
*
* @return array Options list
*/
- public function folder_attributes($folder, $force=false)
+ public function folder_attributes($folder, $force = false)
{
// get attributes directly from LIST command
if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
$opts = $this->conn->data['LIST'][$folder];
}
// get cached folder attributes
else if (!$force) {
$opts = $this->get_cache('mailboxes.attributes');
$opts = $opts[$folder];
}
if (!isset($opts) || !is_array($opts)) {
if (!$this->check_connection()) {
return array();
}
$this->conn->listMailboxes('', $folder);
$opts = $this->conn->data['LIST'][$folder];
}
return isset($opts) && is_array($opts) ? $opts : array();
}
/**
* Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
* PERMANENTFLAGS, UIDNEXT, UNSEEN
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_data($folder)
{
if (!strlen($folder)) {
$folder = $this->folder !== null ? $this->folder : 'INBOX';
}
if ($this->conn->selected != $folder) {
if (!$this->check_connection()) {
return array();
}
if ($this->conn->select($folder)) {
$this->folder = $folder;
}
else {
return null;
}
}
$data = $this->conn->data;
// add (E)SEARCH result for ALL UNDELETED query
if (!empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$data['UNDELETED'] = $this->icache['undeleted_idx'];
}
// dovecot does not return HIGHESTMODSEQ until requested, we use it though in our caching system
// calling STATUS is needed only once, after first use mod-seq db will be maintained
if (!isset($data['HIGHESTMODSEQ']) && empty($data['NOMODSEQ'])
&& ($this->get_capability('QRESYNC') || $this->get_capability('CONDSTORE'))
) {
if ($add_data = $this->conn->status($folder, array('HIGHESTMODSEQ'))) {
$data = array_merge($data, $add_data);
}
}
return $data;
}
/**
* Returns extended information about the folder
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_info($folder)
{
if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
return $this->icache['options'];
}
// get cached metadata
$cache_key = rcube_cache::key_name('mailboxes.folder-info', array($folder));
$cached = $this->get_cache($cache_key);
if (is_array($cached)) {
return $cached;
}
$acl = $this->get_capability('ACL');
$namespace = $this->get_namespace();
- $options = array();
+ $options = ['is_root' => false];
// check if the folder is a namespace prefix
if (!empty($namespace)) {
$mbox = $folder . $this->delimiter;
foreach ($namespace as $ns) {
if (!empty($ns)) {
foreach ($ns as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break 2;
}
}
}
}
}
// check if the folder is other user virtual-root
- if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
+ if ($options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
$parts = explode($this->delimiter, $folder);
if (count($parts) == 2) {
$mbox = $parts[0] . $this->delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break;
}
}
}
}
$options['name'] = $folder;
$options['attributes'] = $this->folder_attributes($folder, true);
$options['namespace'] = $this->folder_namespace($folder);
$options['special'] = $this->is_special_folder($folder);
+ $options['noselect'] = false;
// Set 'noselect' flag
if (is_array($options['attributes'])) {
foreach ($options['attributes'] as $attrib) {
$attrib = strtolower($attrib);
if ($attrib == '\noselect' || $attrib == '\nonexistent') {
$options['noselect'] = true;
}
}
}
else {
$options['noselect'] = true;
}
// Get folder rights (MYRIGHTS)
if ($acl && ($rights = $this->my_rights($folder))) {
$options['rights'] = $rights;
}
// Set 'norename' flag
if (!empty($options['rights'])) {
$rfc_4314 = is_array($this->get_capability('RIGHTS'));
$options['norename'] = ($rfc_4314 && !in_array('x', $options['rights']))
|| (!$rfc_4314 && !in_array('d', $options['rights']));
if (!$options['noselect']) {
$options['noselect'] = !in_array('r', $options['rights']);
}
}
else {
$options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
}
// update caches
$this->icache['options'] = $options;
$this->update_cache($cache_key, $options);
return $options;
}
/**
* Synchronizes messages cache.
*
* @param string $folder Folder name
*/
public function folder_sync($folder)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->synchronize($folder);
}
}
/**
* Check if the folder name is valid
*
* @param string $folder Folder name (UTF-8)
* @param string &$char First forbidden character found
*
* @return bool True if the name is valid, False otherwise
*/
public function folder_validate($folder, &$char = null)
{
if (parent::folder_validate($folder, $char)) {
$vendor = $this->get_vendor();
$regexp = '\\x00-\\x1F\\x7F%*';
if ($vendor == 'cyrus') {
// List based on testing Kolab's Cyrus-IMAP 2.5
$regexp .= '!`@(){}|\\?<;"';
}
if (!preg_match("/[$regexp]/", $folder, $m)) {
return true;
}
$char = $m[0];
}
return false;
}
/**
* Get message header names for rcube_imap_generic::fetchHeader(s)
*
* @return string Space-separated list of header names
*/
protected function get_fetch_headers()
{
if (!empty($this->options['fetch_headers'])) {
$headers = explode(' ', $this->options['fetch_headers']);
}
else {
$headers = array();
}
if ($this->messages_caching || !empty($this->options['all_headers'])) {
$headers = array_merge($headers, $this->all_headers);
}
return $headers;
}
/* -----------------------------------------
* ACL and METADATA/ANNOTATEMORE methods
* ----------------------------------------*/
/**
* Changes the ACL on the specified folder (SETACL)
*
* @param string $folder Folder name
* @param string $user User name
* @param string $acl ACL string
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function set_acl($folder, $user, $acl)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$this->clear_cache(rcube_cache::key_name('mailboxes.folder-info', array($folder)));
return $this->conn->setACL($folder, $user, $acl);
}
/**
* Removes any <identifier,rights> pair for the
* specified user from the ACL for the specified
* folder (DELETEACL)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function delete_acl($folder, $user)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
return $this->conn->deleteACL($folder, $user);
}
/**
* Returns the access control list for folder (GETACL)
*
* @param string $folder Folder name
*
* @return array User-rights array on success, NULL on error
* @since 0.5-beta
*/
public function get_acl($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->getACL($folder);
}
/**
* Returns information about what rights can be granted to the
* user (identifier) in the ACL for the folder (LISTRIGHTS)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return array List of user rights
* @since 0.5-beta
*/
public function list_rights($folder, $user)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->listRights($folder, $user);
}
/**
* Returns the set of rights that the current user has to
* folder (MYRIGHTS)
*
* @param string $folder Folder name
*
* @return array MYRIGHTS response on success, NULL on error
* @since 0.5-beta
*/
public function my_rights($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->myRights($folder);
}
/**
* Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function set_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->setMetadata($folder, $entries);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $entry => $value) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$entry] = array($ent, $attr, $value);
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function delete_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->deleteMetadata($folder, $entries);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $idx => $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$idx] = array($ent, $attr, NULL);
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
* @param bool $force Disables cache use
*
* @return array Metadata entry-value hash array on success, NULL on error
* @since 0.5-beta
*/
public function get_metadata($folder, $entries, $options = array(), $force = false)
{
$entries = (array) $entries;
if (!$force) {
$cache_key = rcube_cache::key_name('mailboxes.metadata', array($folder, $options, $entries));
// get cached data
$cached_data = $this->get_cache($cache_key);
if (is_array($cached_data)) {
return $cached_data;
}
}
if (!$this->check_connection()) {
return null;
}
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
$res = $this->conn->getMetadata($folder, $entries, $options);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
$queries = array();
$res = array();
// Convert entry names
foreach ($entries as $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$queries[$attr][] = $ent;
}
// @TODO: Honor MAXSIZE and DEPTH options
foreach ($queries as $attrib => $entry) {
$result = $this->conn->getAnnotation($folder, $entry, $attrib);
// an error, invalidate any previous getAnnotation() results
if (!is_array($result)) {
return null;
}
else {
foreach ($result as $fldr => $data) {
$res[$fldr] = array_merge((array) $res[$fldr], $data);
}
}
}
}
if (isset($res)) {
if (!$force && !empty($cache_key)) {
$this->update_cache($cache_key, $res);
}
return $res;
}
}
/**
* Converts the METADATA extension entry name into the correct
* entry-attrib names for older ANNOTATEMORE version.
*
* @param string $entry Entry name
*
* @return array Entry-attribute list, NULL if not supported (?)
*/
protected function md2annotate($entry)
{
if (substr($entry, 0, 7) == '/shared') {
return array(substr($entry, 7), 'value.shared');
}
else if (substr($entry, 0, 8) == '/private') {
return array(substr($entry, 8), 'value.priv');
}
// @TODO: log error
}
/* --------------------------------
* internal caching methods
* --------------------------------*/
/**
* Enable or disable indexes caching
*
* @param string $type Cache type (@see rcube::get_cache)
*/
public function set_caching($type)
{
if ($type) {
$this->caching = $type;
}
else {
if ($this->cache) {
$this->cache->close();
}
$this->cache = null;
$this->caching = false;
}
}
/**
* Getter for IMAP cache object
*/
protected function get_cache_engine()
{
if ($this->caching && !$this->cache) {
$rcube = rcube::get_instance();
$ttl = $rcube->config->get('imap_cache_ttl', '10d');
$this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
}
return $this->cache;
}
/**
* Returns cached value
*
* @param string $key Cache key
*
* @return mixed
*/
public function get_cache($key)
{
if ($cache = $this->get_cache_engine()) {
return $cache->get($key);
}
}
/**
* Update cache
*
* @param string $key Cache key
* @param mixed $data Data
*/
public function update_cache($key, $data)
{
if ($cache = $this->get_cache_engine()) {
$cache->set($key, $data);
}
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
public function clear_cache($key = null, $prefix_mode = false)
{
if ($cache = $this->get_cache_engine()) {
$cache->remove($key, $prefix_mode);
}
}
/* --------------------------------
* message caching methods
* --------------------------------*/
/**
* Enable or disable messages caching
*
* @param boolean $set Flag
* @param int $mode Cache mode
*/
public function set_messages_caching($set, $mode = null)
{
if ($set) {
$this->messages_caching = true;
if ($mode && ($cache = $this->get_mcache_engine())) {
$cache->set_mode($mode);
}
}
else {
if ($this->mcache) {
$this->mcache->close();
}
$this->mcache = null;
$this->messages_caching = false;
}
}
/**
* Getter for messages cache object
*/
protected function get_mcache_engine()
{
if ($this->messages_caching && !$this->mcache) {
$rcube = rcube::get_instance();
if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
$ttl = $rcube->config->get('messages_cache_ttl', '10d');
$threshold = $rcube->config->get('messages_cache_threshold', 50);
$this->mcache = new rcube_imap_cache(
$dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
}
}
return $this->mcache;
}
/**
* Clears the messages cache.
*
* @param string $folder Folder name
* @param array $uids Optional message UIDs to remove from cache
*/
protected function clear_message_cache($folder = null, $uids = null)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->clear($folder, $uids);
}
}
/**
* Delete outdated cache entries
*/
function cache_gc()
{
rcube_imap_cache::gc();
}
/* --------------------------------
* protected methods
* --------------------------------*/
/**
* Determines if server supports dual use folders (those can
* contain both sub-folders and messages).
*
* @return bool
*/
protected function detect_dual_use_folders()
{
$val = rcube::get_instance()->config->get('imap_dual_use_folders');
if ($val !== null) {
return (bool) $val;
}
if (!$this->check_connection()) {
return false;
}
$folder = str_replace('.', '', 'foldertest' . microtime(true));
$folder = $this->mod_folder($folder, 'in');
$subfolder = $folder . $this->delimiter . 'foldertest';
if ($this->conn->createFolder($folder)) {
if ($created = $this->conn->createFolder($subfolder)) {
$this->conn->deleteFolder($subfolder);
}
$this->conn->deleteFolder($folder);
return $created;
}
}
/**
* Validate the given input and save to local properties
*
* @param string $sort_field Sort column
* @param string $sort_order Sort order
*/
protected function set_sort_order($sort_field, $sort_order)
{
if ($sort_field != null) {
$this->sort_field = asciiwords($sort_field);
}
if ($sort_order != null) {
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
}
/**
* Sort folders in alphabethical order. Optionally put special folders
* first and other-users/shared namespaces last.
*
* @param array $a_folders Folders list
* @param bool $skip_special Skip special folders handling
*
* @return array Sorted list
*/
public function sort_folder_list($a_folders, $skip_special = false)
{
$folders = array();
// convert names to UTF-8
foreach ($a_folders as $folder) {
// for better performance skip encoding conversion
// if the string does not look like UTF7-IMAP
$folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
}
// sort folders
// asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
uasort($folders, array($this, 'sort_folder_comparator'));
$folders = array_keys($folders);
if ($skip_special || empty($folders)) {
return $folders;
}
// Collect special folders and non-personal namespace roots
$specials = array_merge(array('INBOX'), array_values($this->get_special_folders()));
$ns_roots = array();
foreach (array('other', 'shared') as $ns_name) {
if ($ns = $this->get_namespace($ns_name)) {
foreach ($ns as $root) {
$ns_roots[rtrim($root[0], $root[1])] = $root[0];
}
}
}
// Force the type of folder name variable (#1485527)
$folders = array_map('strval', $folders);
$out = array();
// Put special folders on top...
$specials = array_unique(array_intersect($specials, $folders));
$folders = array_merge($specials, array_diff($folders, $specials));
// ... and rebuild the list to move their subfolders where they belong
$this->sort_folder_specials(null, $folders, $specials, $out);
// Put other-user/shared namespaces at the end
if (!empty($ns_roots)) {
$folders = array();
foreach ($out as $folder) {
foreach ($ns_roots as $root => $prefix) {
if ($folder === $root || strpos($folder, $prefix) === 0) {
$folders[] = $folder;
}
}
}
if (!empty($folders)) {
$out = array_merge(array_diff($out, $folders), $folders);
}
}
return $out;
}
/**
* Recursive function to put subfolders of special folders in place
*/
protected function sort_folder_specials($folder, &$list, &$specials, &$out)
{
$count = count($list);
for ($i = 0; $i < $count; $i++) {
$name = $list[$i];
if ($name === null) {
continue;
}
if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
$out[] = $name;
$list[$i] = null;
if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
unset($specials[$found]);
$this->sort_folder_specials($name, $list, $specials, $out);
}
}
}
}
/**
* Callback for uasort() that implements correct
* locale-aware case-sensitive sorting
*/
protected function sort_folder_comparator($str1, $str2)
{
if ($this->sort_folder_collator === null) {
$this->sort_folder_collator = false;
// strcoll() does not work with UTF8 locale on Windows,
// use Collator from the intl extension
if (stripos(PHP_OS, 'win') === 0 && function_exists('collator_compare')) {
$locale = $this->options['language'] ?: 'en_US';
$this->sort_folder_collator = collator_create($locale) ?: false;
}
}
$path1 = explode($this->delimiter, $str1);
$path2 = explode($this->delimiter, $str2);
foreach ($path1 as $idx => $folder1) {
$folder2 = isset($path2[$idx]) ? $path2[$idx] : '';
if ($folder1 === $folder2) {
continue;
}
if ($this->sort_folder_collator) {
return collator_compare($this->sort_folder_collator, $folder1, $folder2);
}
return strcoll($folder1, $folder2);
}
}
/**
* Find UID of the specified message sequence ID
*
* @param int $id Message (sequence) ID
* @param string $folder Folder name
*
* @return int Message UID
*/
public function id2uid($id, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->ID2UID($folder, $id);
}
/**
* Subscribe/unsubscribe a list of folders and update local cache
*/
protected function change_subscription($folders, $mode)
{
$updated = 0;
$folders = (array) $folders;
if (!empty($folders)) {
if (!$this->check_connection()) {
return false;
}
foreach ($folders as $folder) {
$updated += (int) $this->conn->{$mode}($folder);
}
}
// clear cached folders list(s)
if ($updated) {
$this->clear_cache('mailboxes', true);
}
return $updated == count($folders);
}
/**
* Increde/decrese messagecount for a specific folder
*/
protected function set_messagecount($folder, $mode, $increment)
{
if (!is_numeric($increment)) {
return false;
}
$mode = strtoupper($mode);
$a_folder_cache = $this->get_cache('messagecount');
if (
!isset($a_folder_cache[$folder])
|| !is_array($a_folder_cache[$folder])
|| !isset($a_folder_cache[$folder][$mode])
) {
return false;
}
// add incremental value to messagecount
$a_folder_cache[$folder][$mode] += $increment;
// there's something wrong, delete from cache
if ($a_folder_cache[$folder][$mode] < 0) {
unset($a_folder_cache[$folder][$mode]);
}
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
return true;
}
/**
* Remove messagecount of a specific folder from cache
*/
protected function clear_messagecount($folder, $mode = array())
{
$a_folder_cache = $this->get_cache('messagecount');
if (isset($a_folder_cache[$folder]) && is_array($a_folder_cache[$folder])) {
if (!empty($mode)) {
foreach ((array) $mode as $key) {
unset($a_folder_cache[$folder][$key]);
}
}
else {
unset($a_folder_cache[$folder]);
}
$this->update_cache('messagecount', $a_folder_cache);
}
}
/**
* Converts date string/object into IMAP date/time format
*/
protected function date_format($date)
{
if (empty($date)) {
return null;
}
if (!is_object($date) || !is_a($date, 'DateTime')) {
try {
$timestamp = rcube_utils::strtotime($date);
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return null;
}
}
return $date->format('d-M-Y H:i:s O');
}
/**
* Remember state of the IMAP connection (last IMAP command).
* Use e.g. if you want to execute more commands and ignore results of these.
*
* @return array Connection state
*/
protected function save_conn_state()
{
return array(
$this->conn->error,
$this->conn->errornum,
$this->conn->resultcode,
);
}
/**
* Restore saved connection state.
*
* @param array $state Connection result
*/
protected function restore_conn_state($state)
{
list($this->conn->error, $this->conn->errornum, $this->conn->resultcode) = $state;
}
/**
* This is our own debug handler for the IMAP connection
*/
public function debug_handler($imap, $message)
{
rcube::write_log('imap', $message);
}
}
diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php
index f71afb8e4..af6885175 100644
--- a/program/lib/Roundcube/rcube_plugin_api.php
+++ b/program/lib/Roundcube/rcube_plugin_api.php
@@ -1,739 +1,742 @@
<?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: |
| Plugins repository |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// location where plugins are loaded from
if (!defined('RCUBE_PLUGINS_DIR')) {
define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/');
}
/**
* The plugin loader and global API
*
* @package Framework
* @subpackage PluginAPI
*/
class rcube_plugin_api
{
static protected $instance;
public $dir;
public $url = 'plugins/';
public $task = '';
public $initialized = false;
public $output;
public $handlers = array();
public $allowed_prefs = array();
public $allowed_session_prefs = array();
public $active_plugins = array();
protected $plugins = array();
protected $plugins_initialized = array();
protected $tasks = array();
protected $actions = array();
protected $actionmap = array();
protected $objectsmap = array();
protected $template_contents = array();
protected $exec_stack = array();
protected $deprecated_hooks = array();
/**
* This implements the 'singleton' design pattern
*
* @return rcube_plugin_api The one and only instance if this class
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new rcube_plugin_api();
}
return self::$instance;
}
/**
* Private constructor
*/
protected function __construct()
{
$this->dir = slashify(RCUBE_PLUGINS_DIR);
}
/**
* Initialize plugin engine
*
* This has to be done after rcmail::load_gui() or rcmail::json_init()
* was called because plugins need to have access to rcmail->output
*
* @param object rcube Instance of the rcube base class
* @param string Current application task (used for conditional plugin loading)
*/
public function init($app, $task = '')
{
$this->task = $task;
$this->output = $app->output;
// register an internal hook
$this->register_hook('template_container', array($this, 'template_container_hook'));
// maybe also register a shudown function which triggers
// shutdown functions of all plugin objects
foreach ($this->plugins as $plugin) {
// ... task, request type and framed mode
if (empty($this->plugins_initialized[$plugin->ID]) && !$this->filter($plugin)) {
$plugin->init();
$this->plugins_initialized[$plugin->ID] = $plugin;
}
}
// we have finished initializing all plugins
$this->initialized = true;
}
/**
* Load and init all enabled plugins
*
* This has to be done after rcmail::load_gui() or rcmail::json_init()
* was called because plugins need to have access to rcmail->output
*
* @param array List of configured plugins to load
* @param array List of plugins required by the application
*/
public function load_plugins($plugins_enabled, $required_plugins = array())
{
foreach ($plugins_enabled as $plugin_name) {
$this->load_plugin($plugin_name);
}
// check existance of all required core plugins
foreach ($required_plugins as $plugin_name) {
$loaded = false;
foreach ($this->plugins as $plugin) {
if ($plugin instanceof $plugin_name) {
$loaded = true;
break;
}
}
// load required core plugin if no derivate was found
if (!$loaded) {
$loaded = $this->load_plugin($plugin_name);
}
// trigger fatal error if still not loaded
if (!$loaded) {
rcube::raise_error(array(
'code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Requried plugin $plugin_name was not loaded"), true, true);
}
}
}
/**
* Load the specified plugin
*
* @param string Plugin name
* @param boolean Force loading of the plugin even if it doesn't match the filter
* @param boolean Require loading of the plugin, error if it doesn't exist
*
* @return boolean True on success, false if not loaded or failure
*/
public function load_plugin($plugin_name, $force = false, $require = true)
{
static $plugins_dir;
if (!$plugins_dir) {
$dir = dir($this->dir);
$plugins_dir = unslashify($dir->path);
}
// Validate the plugin name to prevent from path traversal
if (preg_match('/[^a-zA-Z0-9_-]/', $plugin_name)) {
rcube::raise_error(array('code' => 520,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid plugin name: $plugin_name"), true, false);
return false;
}
// plugin already loaded?
if (!isset($this->plugins[$plugin_name])) {
$fn = "$plugins_dir/$plugin_name/$plugin_name.php";
if (!is_readable($fn)) {
if ($require) {
rcube::raise_error(array('code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load plugin file $fn"), true, false);
}
return false;
}
if (!class_exists($plugin_name, false)) {
include $fn;
}
// instantiate class if exists
if (!class_exists($plugin_name, false)) {
rcube::raise_error(array('code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No plugin class $plugin_name found in $fn"),
true, false);
return false;
}
$plugin = new $plugin_name($this);
$this->active_plugins[] = $plugin_name;
// check inheritance...
if (is_subclass_of($plugin, 'rcube_plugin')) {
// call onload method on plugin if it exists.
// this is useful if you want to be called early in the boot process
if (method_exists($plugin, 'onload')) {
$plugin->onload();
}
if (!empty($plugin->allowed_prefs)) {
$this->allowed_prefs = array_merge($this->allowed_prefs, $plugin->allowed_prefs);
}
$this->plugins[$plugin_name] = $plugin;
}
}
if ($plugin = $this->plugins[$plugin_name]) {
// init a plugin only if $force is set or if we're called after initialization
if (($force || $this->initialized) && !$this->plugins_initialized[$plugin_name] && ($force || !$this->filter($plugin))) {
$plugin->init();
$this->plugins_initialized[$plugin_name] = $plugin;
}
}
return true;
}
/**
* check if we should prevent this plugin from initialising
*
* @param $plugin
* @return bool
*/
private function filter($plugin)
{
return ($plugin->noajax && !(is_object($this->output) && $this->output->type == 'html'))
|| ($plugin->task && !preg_match('/^('.$plugin->task.')$/i', $this->task))
|| ($plugin->noframe && !empty($_REQUEST['_framed']));
}
/**
* Get information about a specific plugin.
* This is either provided by a plugin's info() method or extracted from a package.xml or a composer.json file
*
* @param string Plugin name
* @return array Meta information about a plugin or False if plugin was not found
*/
public function get_info($plugin_name)
{
static $composer_lock, $license_uris = array(
'Apache' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
'Apache-2' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
'Apache-1' => 'http://www.apache.org/licenses/LICENSE-1.0',
'Apache-1.1' => 'http://www.apache.org/licenses/LICENSE-1.1',
'GPL' => 'http://www.gnu.org/licenses/gpl.html',
'GPLv2' => 'http://www.gnu.org/licenses/gpl-2.0.html',
'GPL-2.0' => 'http://www.gnu.org/licenses/gpl-2.0.html',
'GPLv3' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPLv3+' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html',
'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html',
'AGPLv3' => 'http://www.gnu.org/licenses/agpl.html',
'AGPLv3+' => 'http://www.gnu.org/licenses/agpl.html',
'AGPL-3.0' => 'http://www.gnu.org/licenses/agpl.html',
'LGPL' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPLv2' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
'LGPLv2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
'LGPLv3' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPL-2.0' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
'LGPL-2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
'LGPL-3.0' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPL-3.0+' => 'http://www.gnu.org/licenses/lgpl.html',
'BSD' => 'http://opensource.org/licenses/bsd-license.html',
'BSD-2-Clause' => 'http://opensource.org/licenses/BSD-2-Clause',
'BSD-3-Clause' => 'http://opensource.org/licenses/BSD-3-Clause',
'FreeBSD' => 'http://opensource.org/licenses/BSD-2-Clause',
'MIT' => 'http://www.opensource.org/licenses/mit-license.php',
'PHP' => 'http://opensource.org/licenses/PHP-3.0',
'PHP-3' => 'http://www.php.net/license/3_01.txt',
'PHP-3.0' => 'http://www.php.net/license/3_0.txt',
'PHP-3.01' => 'http://www.php.net/license/3_01.txt',
);
$dir = dir($this->dir);
$fn = unslashify($dir->path) . "/$plugin_name/$plugin_name.php";
$info = false;
// Validate the plugin name to prevent from path traversal
if (preg_match('/[^a-zA-Z0-9_-]/', $plugin_name)) {
rcube::raise_error(array('code' => 520,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid plugin name: $plugin_name"), true, false);
return false;
}
if (!class_exists($plugin_name, false)) {
if (is_readable($fn)) {
include($fn);
}
else {
return false;
}
}
if (class_exists($plugin_name)) {
$info = $plugin_name::info();
}
// fall back to composer.json file
if (!$info) {
$composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
// Build list of plugins required
$require = array();
foreach (array_keys((array) $json['require']) as $dname) {
if (!preg_match('|^([^/]+)/([a-zA-Z0-9_-]+)$|', $dname, $m)) {
continue;
}
$vendor = $m[1];
$name = $m[2];
if ($name != 'plugin-installer' && $vendor != 'pear' && $vendor != 'pear-pear') {
$dpath = unslashify($dir->path) . "/$name/$name.php";
if (is_readable($dpath)) {
$require[] = $name;
}
}
}
list($info['vendor'], $info['name']) = explode('/', $json['name']);
$info['version'] = $json['version'];
$info['license'] = $json['license'];
- $info['uri'] = $json['homepage'];
$info['require'] = $require;
+
+ if (!empty($json['homepage'])) {
+ $info['uri'] = $json['homepage'];
+ }
}
// read local composer.lock file (once)
if (!isset($composer_lock)) {
$composer_lock = @json_decode(@file_get_contents(INSTALL_PATH . "/composer.lock"), true);
if ($composer_lock['packages']) {
foreach ($composer_lock['packages'] as $i => $package) {
$composer_lock['installed'][$package['name']] = $package;
}
}
}
// load additional information from local composer.lock file
if (!empty($json['name']) && !empty($composer_lock['installed'])
&& !empty($composer_lock['installed'][$json['name']])
) {
$lock = $composer_lock['installed'][$json['name']];
$info['version'] = $lock['version'];
$info['uri'] = $lock['homepage'] ?: $lock['source']['uri'];
$info['src_uri'] = $lock['dist']['uri'] ?: $lock['source']['uri'];
}
}
// fall back to package.xml file
if (!$info) {
$package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
if (is_readable($package) && ($file = file_get_contents($package))) {
$doc = new DOMDocument();
$doc->loadXML($file);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('rc', "http://pear.php.net/dtd/package-2.0");
// XPaths of plugin metadata elements
$metadata = array(
'name' => 'string(//rc:package/rc:name)',
'version' => 'string(//rc:package/rc:version/rc:release)',
'license' => 'string(//rc:package/rc:license)',
'license_uri' => 'string(//rc:package/rc:license/@uri)',
'src_uri' => 'string(//rc:package/rc:srcuri)',
'uri' => 'string(//rc:package/rc:uri)',
);
foreach ($metadata as $key => $path) {
$info[$key] = $xpath->evaluate($path);
}
// dependent required plugins (can be used, but not included in config)
$deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name');
for ($i = 0; $i < $deps->length; $i++) {
$dn = $deps->item($i)->nodeValue;
$info['require'][] = $dn;
}
}
}
// At least provide the name
if (!$info && class_exists($plugin_name)) {
$info = array('name' => $plugin_name, 'version' => '--');
}
else if ($info['license'] && empty($info['license_uri']) && ($license_uri = $license_uris[$info['license']])) {
$info['license_uri'] = $license_uri;
}
return $info;
}
/**
* Allows a plugin object to register a callback for a certain hook
*
* @param string $hook Hook name
* @param mixed $callback String with global function name or array($obj, 'methodname')
*/
public function register_hook($hook, $callback)
{
if (is_callable($callback)) {
if (isset($this->deprecated_hooks[$hook])) {
rcube::raise_error(array('code' => 522, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Deprecated hook name. "
. $hook . ' -> ' . $this->deprecated_hooks[$hook]), true, false);
$hook = $this->deprecated_hooks[$hook];
}
$this->handlers[$hook][] = $callback;
}
else {
rcube::raise_error(array('code' => 521, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid callback function for $hook"), true, false);
}
}
/**
* Allow a plugin object to unregister a callback.
*
* @param string $hook Hook name
* @param mixed $callback String with global function name or array($obj, 'methodname')
*/
public function unregister_hook($hook, $callback)
{
$callback_id = array_search($callback, (array) $this->handlers[$hook]);
if ($callback_id !== false) {
// array_splice() removes the element and re-indexes keys
// that is required by the 'for' loop in exec_hook() below
array_splice($this->handlers[$hook], $callback_id, 1);
}
}
/**
* Triggers a plugin hook.
* This is called from the application and executes all registered handlers
*
* @param string $hook Hook name
* @param array $args Named arguments (key->value pairs)
*
* @return array The (probably) altered hook arguments
*/
public function exec_hook($hook, $args = array())
{
if (!is_array($args)) {
$args = array('arg' => $args);
}
// TODO: avoid recursion by checking in_array($hook, $this->exec_stack) ?
$args += array('abort' => false);
array_push($this->exec_stack, $hook);
// Use for loop here, so handlers added in the hook will be executed too
if (!empty($this->handlers[$hook])) {
for ($i = 0; $i < count($this->handlers[$hook]); $i++) {
$ret = call_user_func($this->handlers[$hook][$i], $args);
if ($ret && is_array($ret)) {
$args = $ret + $args;
}
if (!empty($args['break'])) {
break;
}
}
}
array_pop($this->exec_stack);
return $args;
}
/**
* Let a plugin register a handler for a specific request
*
* @param string $action Action name (_task=mail&_action=plugin.foo)
* @param string $owner Plugin name that registers this action
* @param mixed $callback Callback: string with global function name or array($obj, 'methodname')
* @param string $task Task name registered by this plugin
*/
public function register_action($action, $owner, $callback, $task = null)
{
// check action name
if ($task)
$action = $task.'.'.$action;
else if (strpos($action, 'plugin.') !== 0)
$action = 'plugin.'.$action;
// can register action only if it's not taken or registered by myself
if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) {
$this->actions[$action] = $callback;
$this->actionmap[$action] = $owner;
}
else {
rcube::raise_error(array('code' => 523, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register action $action;"
." already taken by another plugin"), true, false);
}
}
/**
* This method handles requests like _task=mail&_action=plugin.foo
* It executes the callback function that was registered with the given action.
*
* @param string $action Action name
*/
public function exec_action($action)
{
if (isset($this->actions[$action])) {
call_user_func($this->actions[$action]);
}
else if (rcube::get_instance()->action != 'refresh') {
rcube::raise_error(array('code' => 524, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No handler found for action $action"), true, true);
}
}
/**
* Register a handler function for template objects
*
* @param string $name Object name
* @param string $owner Plugin name that registers this action
* @param mixed $callback Callback: string with global function name or array($obj, 'methodname')
*/
public function register_handler($name, $owner, $callback)
{
// check name
if (strpos($name, 'plugin.') !== 0) {
$name = 'plugin.' . $name;
}
// can register handler only if it's not taken or registered by myself
if (is_object($this->output)
&& (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)
) {
$this->output->add_handler($name, $callback);
$this->objectsmap[$name] = $owner;
}
else {
rcube::raise_error(array('code' => 525, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register template handler $name;"
." already taken by another plugin or no output object available"), true, false);
}
}
/**
* Register this plugin to be responsible for a specific task
*
* @param string $task Task name (only characters [a-z0-9_-] are allowed)
* @param string $owner Plugin name that registers this action
*/
public function register_task($task, $owner)
{
// tasks are irrelevant in framework mode
if (!class_exists('rcmail', false)) {
return true;
}
if ($task != asciiwords($task, true)) {
rcube::raise_error(array('code' => 526, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid task name: $task."
." Only characters [a-z0-9_.-] are allowed"), true, false);
}
else if (in_array($task, rcmail::$main_tasks)) {
rcube::raise_error(array('code' => 526, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register taks $task;"
." already taken by another plugin or the application itself"), true, false);
}
else {
$this->tasks[$task] = $owner;
rcmail::$main_tasks[] = $task;
return true;
}
return false;
}
/**
* Checks whether the given task is registered by a plugin
*
* @param string $task Task name
*
* @return boolean True if registered, otherwise false
*/
public function is_plugin_task($task)
{
return !empty($this->tasks[$task]) ? true : false;
}
/**
* Check if a plugin hook is currently processing.
* Mainly used to prevent loops and recursion.
*
* @param string $hook Hook to check (optional)
*
* @return boolean True if any/the given hook is currently processed, otherwise false
*/
public function is_processing($hook = null)
{
return count($this->exec_stack) > 0 && (!$hook || in_array($hook, $this->exec_stack));
}
/**
* Include a plugin script file in the current HTML page
*
* @param string $fn Path to script
*/
public function include_script($fn)
{
if (is_object($this->output) && $this->output->type == 'html') {
$src = $this->resource_url($fn);
$this->output->include_script($src, 'head_bottom', false);
}
}
/**
* Include a plugin stylesheet in the current HTML page
*
* @param string $fn Path to stylesheet
*/
public function include_stylesheet($fn)
{
if (is_object($this->output) && $this->output->type == 'html') {
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) {
$rcube = rcube::get_instance();
$devel_mode = $rcube->config->get('devel_mode');
$assets_dir = $rcube->config->get('assets_dir');
$path = unslashify($assets_dir ?: RCUBE_INSTALL_PATH);
// Prefer .less files in devel_mode (assume less.js is loaded)
if ($devel_mode) {
$less = preg_replace('/\.css$/i', '.less', $fn);
if ($less != $fn && is_file("$path/plugins/$less")) {
$fn = $less;
}
}
else if (!preg_match('/\.min\.css$/', $fn)) {
$min = preg_replace('/\.css$/i', '.min.css', $fn);
if (is_file("$path/plugins/$min")) {
$fn = $min;
}
}
if (!is_file("$path/plugins/$fn")) {
return;
}
}
$src = $this->resource_url($fn);
$this->output->include_css($src);
}
}
/**
* Save the given HTML content to be added to a template container
*
* @param string $html HTML content
* @param string $container Template container identifier
*/
public function add_content($html, $container)
{
if (!isset($this->template_contents[$container])) {
$this->template_contents[$container] = '';
}
$this->template_contents[$container] .= $html . "\n";
}
/**
* Returns list of loaded plugins names
*
* @return array List of plugin names
*/
public function loaded_plugins()
{
return array_keys($this->plugins);
}
/**
* Returns loaded plugin
*
* @return rcube_plugin|null Plugin instance
*/
public function get_plugin($name)
{
return !empty($this->plugins[$name]) ? $this->plugins[$name] : null;
}
/**
* Callback for template_container hooks
*
* @param array $attrib
* @return array
*/
protected function template_container_hook($attrib)
{
$container = $attrib['name'];
$content = isset($attrib['content']) ? $attrib['content'] : '';
if (isset($this->template_contents[$container])) {
$content .= $this->template_contents[$container];
}
return ['content' => $content];
}
/**
* Make the given file name link into the plugins directory
*
* @param string $fn Filename
* @return string
*/
protected function resource_url($fn)
{
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
return $this->url . $fn;
else
return $fn;
}
}
diff --git a/tests/Actions/Settings/About.php b/tests/Actions/Settings/About.php
index 9a983297c..1b9d6a8b7 100644
--- a/tests/Actions/Settings/About.php
+++ b/tests/Actions/Settings/About.php
@@ -1,19 +1,30 @@
<?php
/**
* Test class to test rcmail_action_settings_about
*
* @package Tests
*/
class Actions_Settings_About extends ActionTestCase
{
/**
- * Class constructor
+ * Test run() method
*/
- function test_class()
+ function test_run()
{
- $object = new rcmail_action_settings_about;
+ $action = new rcmail_action_settings_about;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'settings', 'about');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame('about', $output->template);
+ $this->assertSame('About', $output->getProperty('pagetitle'));
+ $this->assertTrue(stripos($result, "<!DOCTYPE html>") === 0);
+ $this->assertTrue(strpos($result, "This program is free software") !== false);
}
}
diff --git a/tests/Actions/Settings/FolderCreate.php b/tests/Actions/Settings/FolderCreate.php
index 644a8ec7a..8cacffc04 100644
--- a/tests/Actions/Settings/FolderCreate.php
+++ b/tests/Actions/Settings/FolderCreate.php
@@ -1,19 +1,52 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_create
*
* @package Tests
*/
class Actions_Settings_FolderCreate extends ActionTestCase
{
/**
- * Class constructor
+ * Test run() method
*/
- function test_class()
+ function test_run()
{
- $object = new rcmail_action_settings_folder_create;
+ $action = new rcmail_action_settings_folder_create;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'settings', 'folder-create');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('get_capability', true)
+ ->registerFunction('get_capability', true)
+ ->registerFunction('folder_info', [
+ 'name' => 'Test',
+ 'is_root' => false,
+ 'noselect' => false,
+ 'special' => false,
+ 'namespace' => 'personal',
+ ])
+ ->registerFunction('list_folders', [
+ 'INBOX',
+ 'Test',
+ ])
+ ->registerFunction('mod_folder', 'Test')
+ ->registerFunction('mod_folder', 'Test')
+ ->registerFunction('folder_attributes', [])
+ ->registerFunction('count', 0)
+ ->registerFunction('get_namespace', null)
+ ->registerFunction('get_quota', false);
+
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame('folderedit', $output->template);
+ $this->assertSame('', $output->getProperty('pagetitle')); // TODO: It should have some title
+ $this->assertTrue(stripos($result, "<!DOCTYPE html>") === 0);
+ $this->assertTrue(strpos($result, "rcmail.gui_object('editform', 'form');") !== false);
}
}
diff --git a/tests/Actions/Settings/FolderDelete.php b/tests/Actions/Settings/FolderDelete.php
index de69ca3a9..195434131 100644
--- a/tests/Actions/Settings/FolderDelete.php
+++ b/tests/Actions/Settings/FolderDelete.php
@@ -1,19 +1,65 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_delete
*
* @package Tests
*/
class Actions_Settings_FolderDelete extends ActionTestCase
{
/**
- * Class constructor
+ * Test deleting a folder
*/
- function test_class()
+ function test_delete()
{
- $object = new rcmail_action_settings_folder_delete;
+ $action = new rcmail_action_settings_folder_delete;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-delete');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('delete_folder', true)
+ ->registerFunction('get_quota', false);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-delete', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Folder successfully deleted.","confirmation",0);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.remove_folder_row("Test");') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.subscription_select();') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.set_quota(') !== false);
+ }
+
+ /**
+ * Test handling errors
+ */
+ function test_delete_errors()
+ {
+ $action = new rcmail_action_settings_folder_delete;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-delete');
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('delete_folder', false)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-delete', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Unable to perform operation. Folder is read-only.","error",0);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.remove_folder_row("Test");') === false);
}
}
diff --git a/tests/Actions/Settings/FolderEdit.php b/tests/Actions/Settings/FolderEdit.php
index 532d84323..5be3778e4 100644
--- a/tests/Actions/Settings/FolderEdit.php
+++ b/tests/Actions/Settings/FolderEdit.php
@@ -1,19 +1,61 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_edit
*
* @package Tests
*/
class Actions_Settings_FolderEdit extends ActionTestCase
{
/**
- * Class constructor
+ * Test run() method
*/
- function test_class()
+ function test_run()
{
- $object = new rcmail_action_settings_folder_edit;
+ $action = new rcmail_action_settings_folder_edit;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'settings', 'folder-edit');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('get_capability', true)
+ ->registerFunction('get_capability', true)
+ ->registerFunction('folder_info', [
+ 'name' => 'Test',
+ 'is_root' => false,
+ 'noselect' => false,
+ 'special' => false,
+ 'namespace' => 'personal',
+ ])
+ ->registerFunction('list_folders', [
+ 'INBOX',
+ 'Test',
+ ])
+ ->registerFunction('mod_folder', 'Test')
+ ->registerFunction('folder_attributes', [])
+ ->registerFunction('count', 0)
+ ->registerFunction('get_namespace', null)
+ ->registerFunction('get_quota', false);
+
+ $_GET = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame('folderedit', $output->template);
+ $this->assertSame('', $output->getProperty('pagetitle')); // TODO: It should have some title
+ $this->assertTrue(stripos($result, "<!DOCTYPE html>") === 0);
+ $this->assertTrue(strpos($result, "Folder properties") !== false);
+ }
+
+ /**
+ * Test folder_form() method
+ */
+ function test_folder_form()
+ {
+ $this->markTestIncomplete();
}
}
diff --git a/tests/Actions/Settings/FolderPurge.php b/tests/Actions/Settings/FolderPurge.php
index de5b027be..e7483ccf6 100644
--- a/tests/Actions/Settings/FolderPurge.php
+++ b/tests/Actions/Settings/FolderPurge.php
@@ -1,19 +1,94 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_purge
*
* @package Tests
*/
class Actions_Settings_FolderPurge extends ActionTestCase
{
/**
- * Class constructor
+ * Test purging a folder
*/
- function test_class()
+ function test_purge()
{
- $object = new rcmail_action_settings_folder_purge;
+ $action = new rcmail_action_settings_folder_purge;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-purge');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('move_message', true)
+ ->registerFunction('get_quota', false);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-purge', $result['action']);
+ $this->assertSame(0, $result['env']['messagecount']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Message(s) moved successfully.","confirmation",0);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.show_folder("Test",null,true);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.set_quota') === false);
+ }
+
+ /**
+ * Test purging a Trash folder
+ */
+ function test_purge_trash()
+ {
+ $action = new rcmail_action_settings_folder_purge;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-purge');
+
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('delete_message', true)
+ ->registerFunction('get_quota', false);
+
+ $_POST = ['_mbox' => 'Trash'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-purge', $result['action']);
+ $this->assertSame(0, $result['env']['messagecount']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Folder has successfully been emptied.","confirmation",0);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.show_folder("Trash",null,true);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.set_quota') !== false);
+ }
+
+ /**
+ * Test handling errors
+ */
+ function test_purge_errors()
+ {
+ $action = new rcmail_action_settings_folder_purge;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-purge');
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('move_message', false)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-purge', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Unable to perform operation. Folder is read-only.","error",0);') !== false);
}
}
diff --git a/tests/Actions/Settings/FolderRename.php b/tests/Actions/Settings/FolderRename.php
index e0f1acaa8..8dbe80f7c 100644
--- a/tests/Actions/Settings/FolderRename.php
+++ b/tests/Actions/Settings/FolderRename.php
@@ -1,19 +1,62 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_rename
*
* @package Tests
*/
class Actions_Settings_FolderRename extends ActionTestCase
{
/**
- * Class constructor
+ * Test renaming a folder
*/
- function test_class()
+ function test_rename()
{
- $object = new rcmail_action_settings_folder_rename;
+ $action = new rcmail_action_settings_folder_rename;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-rename');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('rename_folder', true)
+ ->registerFunction('folder_info', [])
+ ->registerFunction('mod_folder', 'Test2');
+
+ $_POST = ['_folder_oldname' => 'Test', '_folder_newname' => 'Test2'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-rename', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.replace_folder_row("Test","Test2","Test2","Test2",false,"mailbox");') !== false);
+ }
+
+ /**
+ * Test handling errors
+ */
+ function test_rename_errors()
+ {
+ $action = new rcmail_action_settings_folder_rename;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-rename');
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('rename_folder', false)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY);
+
+ $_POST = ['_folder_oldname' => 'Test', '_folder_newname' => 'Test2'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-rename', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Unable to perform operation. Folder is read-only.","error",0);') !== false);
}
}
diff --git a/tests/Actions/Settings/FolderSave.php b/tests/Actions/Settings/FolderSave.php
index 08ef4fd61..967531f06 100644
--- a/tests/Actions/Settings/FolderSave.php
+++ b/tests/Actions/Settings/FolderSave.php
@@ -1,19 +1,59 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_save
*
* @package Tests
*/
class Actions_Settings_FolderSave extends ActionTestCase
{
/**
- * Class constructor
+ * Test folder creation
*/
- function test_class()
+ function test_new_folder()
{
- $object = new rcmail_action_settings_folder_save;
+ $action = new rcmail_action_settings_folder_save;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'settings', 'folder-save');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('get_capability', true)
+ ->registerFunction('get_capability', true)
+ ->registerFunction('folder_validate', true)
+ ->registerFunction('mod_folder', 'NewTest')
+ ->registerFunction('create_folder', true)
+ ->registerFunction('mod_folder', 'NewTest')
+ ->registerFunction('folder_options', []);
+
+ $_POST = ['_name' => 'NewTest'];
+
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame('iframe', $output->template);
+ $this->assertTrue(stripos($result, "<!DOCTYPE html>") === 0);
+ $this->assertTrue(strpos($result, 'display_message("Folder created successfully.","confirmation",0);') !== false);
+ $this->assertTrue(strpos($result, '.add_folder_row("NewTest"') !== false);
+ $this->assertTrue(strpos($result, '.subscription_select()') !== false);
+ }
+
+ /**
+ * Test folder update/rename
+ */
+ function test_folder_update()
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test error handling
+ */
+ function test_error_handling()
+ {
+ $this->markTestIncomplete();
}
}
diff --git a/tests/Actions/Settings/FolderSize.php b/tests/Actions/Settings/FolderSize.php
index 6a9c20455..13ac69ea4 100644
--- a/tests/Actions/Settings/FolderSize.php
+++ b/tests/Actions/Settings/FolderSize.php
@@ -1,19 +1,60 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_size
*
* @package Tests
*/
class Actions_Settings_FolderSize extends ActionTestCase
{
/**
- * Class constructor
+ * Test getting a folder size
*/
- function test_class()
+ function test_run()
{
- $object = new rcmail_action_settings_folder_size;
+ $action = new rcmail_action_settings_folder_size;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-size');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('folder_size', 100);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-size', $result['action']);
+ $this->assertSame('this.folder_size_update("100 B");', trim($result['exec']));
+ }
+
+ /**
+ * Test handling errors
+ */
+ function test_run_errors()
+ {
+ $action = new rcmail_action_settings_folder_size;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-size');
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('folder_size', false)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-size', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Unable to perform operation. Folder is read-only.","error",0);') !== false);
}
}
diff --git a/tests/Actions/Settings/FolderSubscribe.php b/tests/Actions/Settings/FolderSubscribe.php
index 098ec0c92..ceae6c9a8 100644
--- a/tests/Actions/Settings/FolderSubscribe.php
+++ b/tests/Actions/Settings/FolderSubscribe.php
@@ -1,19 +1,68 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_subscribe
*
* @package Tests
*/
class Actions_Settings_FolderSubscribe extends ActionTestCase
{
/**
- * Class constructor
+ * Test subscribing a folder
*/
- function test_class()
+ function test_subscribe()
{
- $object = new rcmail_action_settings_folder_subscribe;
+ $action = new rcmail_action_settings_folder_subscribe;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-subscribe');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('subscribe', true)
+ ->registerFunction('is_special_folder', false);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-subscribe', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Folder successfully subscribed.","confirmation",0);') !== false);
+
+ // TODO: Test a special folder subscription
+ }
+
+ /**
+ * Test handling errors
+ */
+ function test_subscribe_errors()
+ {
+ $action = new rcmail_action_settings_folder_subscribe;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-subscribe');
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('subscribe', false)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-subscribe', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Unable to perform operation. Folder is read-only.","error",0);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.reset_subscription("Test",false);') !== false);
+
+ // TODO: Test TRYCREATE error handling
}
}
diff --git a/tests/Actions/Settings/FolderUnsubscribe.php b/tests/Actions/Settings/FolderUnsubscribe.php
index 72da2f610..58949ad07 100644
--- a/tests/Actions/Settings/FolderUnsubscribe.php
+++ b/tests/Actions/Settings/FolderUnsubscribe.php
@@ -1,19 +1,62 @@
<?php
/**
* Test class to test rcmail_action_settings_folder_unsubscribe
*
* @package Tests
*/
class Actions_Settings_FolderUnsubscribe extends ActionTestCase
{
/**
- * Class constructor
+ * Test unsubscribing a folder
*/
- function test_class()
+ function test_unsubscribe()
{
- $object = new rcmail_action_settings_folder_unsubscribe;
+ $action = new rcmail_action_settings_folder_unsubscribe;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-unsubscribe');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('unsubscribe', true)
+ ->registerFunction('is_special_folder', false);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-unsubscribe', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Folder successfully unsubscribed.","confirmation",0);') !== false);
+ }
+
+ /**
+ * Test handling errors
+ */
+ function test_unsubscribe_errors()
+ {
+ $action = new rcmail_action_settings_folder_unsubscribe;
+ $output = $this->initOutput(rcmail_action::MODE_AJAX, 'settings', 'folder-unsubscribe');
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('unsubscribe', false)
+ ->registerFunction('get_error_code', -1)
+ ->registerFunction('get_response_code', rcube_storage::READONLY);
+
+ $_POST = ['_mbox' => 'Test'];
+
+ $this->runAndAssert($action, OutputJsonMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame(['Content-Type: application/json; charset=UTF-8'], $output->headers);
+ $this->assertSame('folder-unsubscribe', $result['action']);
+ $this->assertTrue(strpos($result['exec'], 'this.display_message("Unable to perform operation. Folder is read-only.","error",0);') !== false);
+ $this->assertTrue(strpos($result['exec'], 'this.reset_subscription("Test",true);') !== false);
}
}
diff --git a/tests/Actions/Settings/Folders.php b/tests/Actions/Settings/Folders.php
index a74a4fd8a..4f884900f 100644
--- a/tests/Actions/Settings/Folders.php
+++ b/tests/Actions/Settings/Folders.php
@@ -1,19 +1,90 @@
<?php
/**
* Test class to test rcmail_action_settings_folders
*
* @package Tests
*/
class Actions_Settings_Folders extends ActionTestCase
{
/**
- * Class constructor
+ * Test run() method
*/
- function test_class()
+ function test_run()
{
- $object = new rcmail_action_settings_folders;
+ $action = new rcmail_action_settings_folders;
+ $output = $this->initOutput(rcmail_action::MODE_HTTP, 'settings', 'folders');
- $this->assertInstanceOf('rcmail_action', $object);
+ $this->assertInstanceOf('rcmail_action', $action);
+ $this->assertTrue($action->checks());
+
+ // Set expected storage function calls/results
+ rcmail::get_instance()->storage
+ ->registerFunction('clear_cache', true)
+ ->registerFunction('get_capability', true)
+ ->registerFunction('get_capability', true)
+ ->registerFunction('folder_info', [
+ 'name' => 'Test',
+ 'is_root' => false,
+ 'noselect' => false,
+ 'special' => false,
+ 'namespace' => 'personal',
+ ])
+ ->registerFunction('list_folders', [
+ 'INBOX',
+ 'Test',
+ ])
+ ->registerFunction('list_folders_subscribed', [
+ 'INBOX',
+ 'Test',
+ ])
+ ->registerFunction('get_special_folders', [])
+ ->registerFunction('mod_folder', 'Test')
+ ->registerFunction('mod_folder', 'Test')
+ ->registerFunction('folder_attributes', [])
+ ->registerFunction('count', 0)
+ ->registerFunction('get_namespace', null)
+ ->registerFunction('get_quota', false);
+
+ $this->runAndAssert($action, OutputHtmlMock::E_EXIT);
+
+ $result = $output->getOutput();
+
+ $this->assertSame('folders', $output->template);
+ $this->assertSame('Folders', $output->getProperty('pagetitle'));
+ $this->assertTrue(stripos($result, "<!DOCTYPE html>") === 0);
+ $this->assertTrue(strpos($result, "treelist.js") !== false);
+ }
+
+ /**
+ * Test folder_subscriptions() method
+ */
+ function test_folder_subscriptions()
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test folder_filter() method
+ */
+ function test_folder_filter()
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test folder_options() method
+ */
+ function test_folder_options()
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test update_folder_row() method
+ */
+ function test_update_folder_row()
+ {
+ $this->markTestIncomplete();
}
}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 3de33facb..1fc69f548 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -1,65 +1,66 @@
<phpunit
backupGlobals="false"
bootstrap="bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="false"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="Framework">
<directory suffix=".php">Framework</directory>
</testsuite>
<testsuite name="Roundcube">
<directory suffix=".php">Rcmail</directory>
</testsuite>
<testsuite name="Actions">
<directory suffix=".php">Actions/*</directory>
</testsuite>
<testsuite name="Plugins">
<file>./../plugins/acl/tests/Acl.php</file>
<file>./../plugins/additional_message_headers/tests/AdditionalMessageHeaders.php</file>
<file>./../plugins/archive/tests/Archive.php</file>
<file>./../plugins/attachment_reminder/tests/AttachmentReminder.php</file>
<file>./../plugins/autologon/tests/Autologon.php</file>
<file>./../plugins/database_attachments/tests/DatabaseAttachments.php</file>
<file>./../plugins/debug_logger/tests/DebugLogger.php</file>
<file>./../plugins/emoticons/tests/Emoticons.php</file>
<file>./../plugins/enigma/tests/Enigma.php</file>
<file>./../plugins/example_addressbook/tests/ExampleAddressbook.php</file>
<file>./../plugins/filesystem_attachments/tests/FilesystemAttachments.php</file>
<file>./../plugins/help/tests/Help.php</file>
<file>./../plugins/hide_blockquote/tests/HideBlockquote.php</file>
<file>./../plugins/http_authentication/tests/HttpAuthentication.php</file>
<file>./../plugins/identicon/tests/Identicon.php</file>
<file>./../plugins/identity_select/tests/IdentitySelect.php</file>
<file>./../plugins/jqueryui/tests/Jqueryui.php</file>
<file>./../plugins/krb_authentication/tests/KrbAuthentication.php</file>
<file>./../plugins/managesieve/tests/Managesieve.php</file>
<file>./../plugins/managesieve/tests/Parser.php</file>
<file>./../plugins/managesieve/tests/Tokenizer.php</file>
<file>./../plugins/managesieve/tests/Vacation.php</file>
<file>./../plugins/markasjunk/tests/Markasjunk.php</file>
<file>./../plugins/new_user_dialog/tests/NewUserDialog.php</file>
<file>./../plugins/new_user_identity/tests/NewUserIdentity.php</file>
<file>./../plugins/newmail_notifier/tests/NewmailNotifier.php</file>
<file>./../plugins/password/tests/Password.php</file>
<file>./../plugins/redundant_attachments/tests/RedundantAttachments.php</file>
<file>./../plugins/show_additional_headers/tests/ShowAdditionalHeaders.php</file>
<file>./../plugins/squirrelmail_usercopy/tests/Squirrelmail_usercopy.php</file>
<file>./../plugins/subscriptions_option/tests/SubscriptionsOption.php</file>
<file>./../plugins/userinfo/tests/Userinfo.php</file>
<file>./../plugins/vcard_attachments/tests/VcardAttachments.php</file>
<file>./../plugins/virtuser_file/tests/VirtuserFile.php</file>
<file>./../plugins/virtuser_query/tests/VirtuserQuery.php</file>
<file>./../plugins/zipdownload/tests/Zipdownload.php</file>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../program/lib</directory>
<directory suffix=".php">../program/include</directory>
<directory suffix=".php">../program/actions</directory>
+ <directory suffix=".php">../plugins</directory>
</whitelist>
</filter>
</phpunit>

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 19, 9:38 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
435839
Default Alt Text
(488 KB)

Event Timeline